Délais d'expiration
OxPHP applique deux délais d'expiration indépendants qui protègent contre les clients lents et les requêtes incontrôlées. Le délai d'expiration des en-têtes protège la phase de connexion au niveau du serveur. Le temps d'exécution PHP est borné par la directive ini max_execution_time propre à PHP (et par la fonction d'exécution set_time_limit()). Une propriété de cette limite diffère de PHP-FPM d'une manière facile à manquer : OxPHP s'appuie sur un build PHP thread-safe (ZTS), où la limite mesure le temps réel écoulé et non le temps CPU. Voir Temps réel écoulé, pas temps CPU.
Fonctionnement
Chaque requête passe par les phases suivantes :
- Connexion acceptée — le délai d'expiration des en-têtes démarre. OxPHP attend que le client envoie un ensemble complet d'en-têtes HTTP.
- En-têtes reçus — le délai d'expiration des en-têtes se termine. La requête est distribuée à un worker PHP.
- PHP traite la requête — le code applicatif s'exécute sous le
max_execution_timepropre à PHP, armé sous la forme d'un minuteur POSIX par thread. Lorsque la limite est atteinte, la requête est annulée et l'erreur fatale unifiéeRequest cancelled (timeout)se déclenche. En mode worker, chaque requête conserve sa propre limite, y compris celle que le worker prend en charge alors qu'il en traite déjà d'autres, et une requête suspendue dans une attente au moment où sa limite expire est terminée à sa reprise (voir Mode worker → Échéances des requêtes). - Réponse envoyée — sur les connexions keep-alive, le cycle recommence à l'étape 1.
graph TD A["TCP connect<br/>(+ TLS handshake if enabled)"] -->|HEADER_TIMEOUT_SECONDS| B["Headers received"] B -->|max_execution_time| C["Response sent"] C -->|next request, keep-alive| A
Sur les connexions keep-alive, les deux délais d'expiration s'appliquent indépendamment à chaque requête de la connexion.
Lorsque TLS est activé, le délai d'expiration des en-têtes démarre une fois la poignée de main TLS terminée, et non au moment où la connexion TCP est acceptée.
Le délai d'expiration des en-têtes protège contre les attaques de type slowloris, où un client envoie les en-têtes octet par octet pour maintenir les connexions ouvertes indéfiniment.
La gestion du temps d'exécution PHP est entièrement déléguée à PHP. Lorsque max_execution_time est dépassé, le mécanisme d'annulation unifié d'OxPHP :
- Définit
connection_status() & PHP_CONNECTION_TIMEOUTpour que le code utilisateur puisse détecter la cause. - Exécute tous les callbacks
register_shutdown_function(), exactement comme le fait PHP-FPM. - Renvoie un HTTP
504 Gateway Timeoutavec le messageRequest cancelled (timeout)inscrit dans le journal des erreurs.
Temps réel écoulé, pas temps CPU
max_execution_time mesure depuis combien de temps la requête est en vie, et non la quantité de CPU qu'elle a consommée. Chaque seconde passée à attendre compte : une requête de base de données lente, un appel HTTP vers un autre service, un fread() bloquant sur un socket, un sleep().
C'est une différence avec PHP-FPM, et elle tient au build PHP plutôt qu'à OxPHP. Un build thread-safe (ZTS), qu'OxPHP exige parce qu'il exécute de nombreux interpréteurs PHP comme threads d'un même processus, arme la limite sous la forme d'un minuteur POSIX par thread sur une horloge monotone. Un build non thread-safe, celui qu'exécute PHP-FPM, l'arme avec setitimer(ITIMER_PROF), un minuteur qui n'avance que lorsque le processus est sur le CPU. C'est de là que vient la règle empirique bien connue selon laquelle « l'attente de la base de données ne compte pas dans max_execution_time ». Elle vaut pour PHP-FPM, pas ici.
La conséquence apparaît lors de la migration d'une application existante :
| Requête : 25 s d'attente sur une requête de rapport, 2 s de rendu | Décompté d'une limite de 30 s |
|---|---|
| PHP-FPM (build non thread-safe) | ~2 s (la limite est pratiquement inatteignable) |
| OxPHP (build thread-safe) | 27 s (une seule requête lente peut épuiser la limite) |
Une application qui n'a jamais atteint max_execution_time sous PHP-FPM peut se mettre à renvoyer des 504 sur ses endpoints les plus lents après le passage à OxPHP, sans aucune modification de la valeur de la directive. Dimensionnez la limite en fonction de la latence totale de la requête, y compris tout ce qu'elle attend, et relevez-la pour les endpoints qui se bloquent sur des systèmes externes.
Codes de statut d'annulation
OxPHP annule une requête pour plusieurs raisons distinctes. Chacune correspond à un statut transmis sur le réseau qui reflète la condition réelle plutôt qu'un 500 générique :
| Cause | Statut | Notes |
|---|---|---|
max_execution_time / set_time_limit() dépassé |
504 Gateway Timeout |
Épuisement du temps d'exécution côté serveur. |
| L'arrêt gracieux du serveur a purgé la requête | 503 Service Unavailable |
Ajoute Retry-After: 5 pour que les clients réessaient sur une instance rétablie ou de remplacement. |
| Le client a fermé la connexion en cours de requête | 499 |
« Client Closed Request » à la manière de nginx. La connexion a déjà disparu, ce statut n'apparaît donc que dans les journaux d'accès et les métriques — il n'est jamais transmis sur le réseau. Il expose les abandons initiés par le client comme des non-5xx afin qu'ils ne polluent pas les alertes d'erreur serveur. |
Ce sont les trois seules causes d'annulation. Une requête que le superviseur observe au-delà de son seuil est signalée via oxphp_worker_stuck_total et oxphp_worker_long_running_total et laissée en cours d'exécution ; le serveur ne l'annule jamais.
Si votre ERROR_PAGES_DIR ne fournit qu'un 500.html, ajoutez 504.html, 503.html et (facultativement) 499.html pour garder des pages stylisées cohérentes pour toutes les causes d'annulation.
Configuration
| Variable | Valeur par défaut | Description |
|---|---|---|
HEADER_TIMEOUT_SECONDS |
5 |
Nombre maximal de secondes pour recevoir les en-têtes de la requête après l'acceptation de la connexion. Protège contre les attaques slowloris. 0 n'est pas traité de façon particulière — il est transmis tel quel à hyper comme un délai d'expiration de zéro seconde, qui se déclenche immédiatement. Pour désactiver le délai d'expiration, laissez la variable non définie plutôt que de la fixer à 0 |
Le temps d'exécution PHP se configure via php.ini, et non via les variables d'environnement d'OxPHP :
; php.ini
max_execution_time = 30Ou par script à l'exécution :
set_time_limit(60); // 60 seconds from now
set_time_limit(0); // disable for this requestValeurs recommandées
| Scénario | Délai d'expiration des en-têtes | max_execution_time |
|---|---|---|
| Serveur d'API | 5 s | 30 s |
| Service web général | 5 s | 60 s |
| Téléversements de fichiers | 10 s | 300 s |
| SSE / long-polling | 5 s | 0 (désactivé, à définir par script) |
Ajustez ces valeurs en fonction des caractéristiques de votre application. Pour les endpoints SSE, appelez set_time_limit(0) en tête du script de streaming plutôt que de désactiver max_execution_time globalement.
Dépannage
Les clients reçoivent un 504 avec « Request cancelled (timeout) » de manière inattendue
La limite de temps d'exécution PHP s'est déclenchée avant que le script ne se termine.
Si ces requêtes fonctionnaient sous PHP-FPM et ont commencé à échouer après le passage à OxPHP, la cause est très probablement la sémantique du minuteur décrite dans Temps réel écoulé, pas temps CPU : le temps que la requête passe à attendre une base de données ou un service amont compte dans la limite.
Solution : Augmentez max_execution_time pour le script concerné, ou appelez set_time_limit($seconds) pour l'étendre à l'exécution :
// at the top of a slow script
set_time_limit(300);Pour les endpoints SSE ou de streaming où les connexions doivent rester ouvertes indéfiniment, désactivez le minuteur pour ce script :
set_time_limit(0);Les connexions sont abandonnées avant l'arrivée des en-têtes
Le délai d'expiration des en-têtes est trop court pour les clients sur des liaisons à forte latence ou derrière des répartiteurs de charge lents.
Solution : Augmentez le délai d'expiration des en-têtes :
HEADER_TIMEOUT_SECONDS=15OPcache provoque un délai d'expiration lors de la première requête après une modification de script
La recompilation OPcache ajoute de la latence lors de la première requête après une modification de fichier. C'est plus fréquent dans les environnements de développement comportant de nombreux fichiers. Augmentez max_execution_time ou désactivez-le pendant le développement.
Exemple Docker
services:
app:
image: ghcr.io/oxphp/oxphp:0.13.0
ports:
- "8080:80"
environment:
HEADER_TIMEOUT_SECONDS: "5"
volumes:
- ./app:/var/www/html:ro
- ./php.ini:/usr/local/etc/php/conf.d/zz-app.ini:roAvec php.ini contenant :
max_execution_time = 30Bonnes pratiques
- Ne définissez jamais
max_execution_time = 0globalement en production sauf si vous disposez d'endpoints SSE ou de long-polling qui nécessitent des connexions indéfinies. Préférezset_time_limit(0)par script. - Utilisez des limites plus courtes pour les serveurs d'API. Les API ont des temps de réponse prévisibles. Un
max_execution_timede 30 secondes détecte rapidement les requêtes bloquées sans affecter le trafic normal. Dimensionnez-le en fonction de votre dépendance en aval la plus lente plutôt que du travail CPU effectué par le script, car le temps passé à attendre compte. - Combinez avec la limitation de débit. Les délais d'expiration protègent les requêtes individuelles ; la limitation de débit protège contre un volume élevé de requêtes. Ensemble, elles couvrent aussi bien les schémas d'attaque lents que rapides.
Prompts pour votre agent
Ces prompts supposent un agent connecté au serveur MCP de la documentation OxPHP — il lira lui-même les pages dont il a besoin.
Règle les délais d'expiration d'OxPHP autour de ma requête la plus longue. Lis la page `features/timeouts` via le serveur MCP de la documentation OxPHP.
Trouve ce qui dure réellement le plus longtemps dans ce dépôt (téléversements, rapports, exports, ainsi que les attentes sur les bases de données et les services amont, puisque la limite compte le temps réel écoulé), puis donne-moi le délai d'expiration de lecture des en-têtes et le `max_execution_time` avec lesquels tourner, ainsi que les surcharges par script pour tout ce qui ne peut pas vivre sous une limite globale.Voir aussi
- Limitation de débit — limitation du débit de requêtes par IP
- Server-Sent Events (SSE) — conseils sur la désactivation des délais d'expiration pour les endpoints de streaming
- Référence de configuration — référence complète des variables d'environnement