Versions

Changelog

Chaque changement notable du serveur, de l'API PHP et des images — une sélection de points marquants pour chaque version. L'historique complet et intégral se trouve dans CHANGELOG.md.

dernière v0.13.0 · 2026-10-08 · 13 versions · AGPL-3.0 CHANGELOG.md →
oxphp — releases bash
$ docker pull ghcr.io/oxphp/oxphp:0.13.0
0.13.0: Pulling from oxphp/oxphp
Status: Image is up to date
# PHP 8.4 variant:
$ docker pull ghcr.io/oxphp/oxphp:0.13.0-php8.4
v0.13.0
2026-10-08
LATEST
⚠ breaking changes
Release notes →
BREAKING

Mode worker : max_execution_time par requête — Une requête que le worker prend en charge alors qu'il en traite déjà d'autres reçoit désormais sa propre limite, comptée à partir de sa prise en charge, et set_time_limit() n'affecte que la requête qui l'appelle. Un flux SSE ou un long polling qui vivait sur le timer partagé doit appeler lui-même set_time_limit(0) ; l'image publiée n'embarque aucun php.ini, la limite est donc de 30 s.

RUNTIME_HOOKS=1, true et all incluent net — Dès la mise à jour, l'établissement des connexions, les poignées de main et lectures TLS, les écritures en attente de place dans le tampon et les lectures unix://, udp:// et udg:// parquent la Fiber, sans aucune modification de votre part. runtime_hooks rapporte désormais ["sleep","streams","net"], et un flux partagé entre Fibers est protégé quel que soit son transport. RUNTIME_HOOKS=sleep,streams conserve l'ancien ensemble.

Shareable est réservé aux types Shared — Une classe PHP ne peut plus implémenter OxPHP\Shared\Shareable ni étendre un type Shared, désormais déclaré final ; les deux provoquent une erreur fatale à la déclaration. Transmettre un tel objet à un autre thread faisait planter le serveur. createMock() de PHPUnit sur un type Shared ou sur Shareable échoue désormais ; utilisez votre propre interface marqueur.

Alias d'observabilité de Shared\* supprimés — oxphp_shared_channel_pending et oxphp_shared_pool_size disparaissent de /metrics, et pending/size de type_specific dans /__ox_shared/entry, de même que le WARN au démarrage qui les annonçait. Tableaux de bord et alertes doivent passer à oxphp_shared_channel_count, oxphp_shared_pool_count et count.

TRUSTED_PROXIES lit les entrées IP:port — Derrière un répartiteur de charge qui écrit IP:port dans X-Forwarded-For (Azure Application Gateway), REMOTE_ADDR est désormais le client et REMOTE_PORT son port. Là où un CDN placé devant une telle passerelle ajoute aussi son entrée, l'adresse est désormais celle du CDN lui-même ; déclarez aussi ses plages (pour Cloudflare, le mot-clé cloudflare).

DOCUMENT_ROOT derrière un lien symbolique de déploiement — La racine est résolue une seule fois au démarrage, et chaque fichier statique lu depuis le disque comme chaque script PHP est vérifié par rapport à elle à son ouverture : dès que /app/current est basculé, ils répondent 404 jusqu'au redémarrage du serveur. Pointez DOCUMENT_ROOT vers un chemin qui ne change pas d'un déploiement à l'autre, ou redémarrez dans le cadre de la bascule.

Scripts que le serveur ne peut pas ouvrir — Hors mode worker, un script supprimé répond 404 même si OPcache le conserve avec validate_timestamps désactivé ; un déploiement qui supprime puis recrée les fichiers sur place (git checkout, rsync --delete) ouvre donc une fenêtre de 404 : écrivez les nouveaux fichiers à côté des anciens et renommez-les par-dessus. Un script illisible répond 500 et journalise Cannot open script.

Les scripts liés symboliquement s'exécutent comme leur cible — Hors mode worker, un script atteint via un lien vers un autre répertoire s'exécute avec ce répertoire comme répertoire de travail, et le fait que PHP le traite ou non comme un phar dépend du nom de fichier résolu, et non de l'URL. Construisez les chemins relatifs à partir de __DIR__.

fiber.stack_size doit dépasser zend.reserved_stack_size — En mode worker et dans les tâches ASYNC_WORKERS, la limite de pile des Fibers laisse désormais libre la réserve de PHP (48 KiB dans un build release), si bien qu'un fiber.stack_size inférieur ou égal à celle-ci fait échouer chaque requête avec Maximum call stack size of 0 bytes, et qu'une récursion profonde s'arrête 44 KiB plus tôt qu'avant.

Profile\start() ne fait rien tant que le profileur est désactivé — Il ne fait plus renvoyer true à Profile\is_active() lorsque PROFILER_ENABLED est désactivé ; un test qui démarre un profil et vérifie is_active() doit donc activer le profileur. Avec OTEL_APM_ENABLED, les spans APM que l'appel supprimait sont désormais conservés dans la trace.

UploadedFile::type() après moveTo() lit la destination — La détection du type ne s'exécute plus avant le déplacement : un fichier déplacé ailleurs, supprimé ou réécrit au moment du premier appel à type() rapporte ce qui s'y trouve alors, ou application/octet-stream avec un avertissement. Appelez type() avant moveTo() lorsque le fichier déplacé ne reste pas en place ou se trouve derrière un wrapper de flux.

ADDED

Règles de refus de chemins .oxphpdeny — Un fichier à la syntaxe .gitignore placé à la racine de DOCUMENT_ROOT empêche que les chemins qu'il liste soient servis, dans tous les modes de routage, pour les fichiers statiques comme pour PHP, avant tout accès disque. Les chemins refusés répondent PHP_DENY_FALLBACK et une règle > confie la requête au script d'entrée. Lu une seule fois au démarrage ; les refus sont comptés dans oxphp_path_deny_total.

RUNTIME_HOOKS=net — L'établissement de connexions via les transports de flux de PHP, les poignées de main et lectures TLS, les écritures en attente de place dans le tampon d'envoi et les lectures sur les flux unix://, udp://, udg:// et issus de paires de sockets suspendent la Fiber au lieu de monopoliser le thread worker. Nécessite que streams soit aussi listé et ne fonctionne que sous Linux. Non couverts : libcurl, libpq, libmemcached, le DNS et l'établissement de connexions persistantes.

Mot-clé cloudflare pour TRUSTED_PROXIES — Mots-clés, CIDR et IP nues se combinent désormais dans une même liste (private,100.64.0.0/10 provoquait une erreur au démarrage). cloudflare accorde la confiance aux plages d'adresses des serveurs edge de Cloudflare et lit l'adresse du client dans CF-Connecting-IP lorsque le premier saut au-delà de vos propres proxys est un serveur edge Cloudflare ; private,cloudflare convient à Cloudflare → répartiteur de charge → OxPHP.

User-Agent dans les traces et les journaux d'accès — Le span serveur racine porte user_agent.original et chaque entrée du journal d'accès un champ user_agent, tronqués à 512 octets avec l'UTF-8 invalide remplacé, si bien qu'on distingue un robot d'indexation d'un navigateur à ses seules traces.

Pourquoi une requête a été délestée — Les lignes de log de surcharge répartissent désormais les refus entre refused_queue_full, refused_wait_timeout, refused_waiting_full et refused_waiting_bytes, et le span racine d'une requête refusée porte oxphp.shed_reason : la limite à relever se voit sans l'écouteur INTERNAL_ADDR.

Échecs d'export OTLP dans /metrics — oxphp_otel_export_failures_total, oxphp_otel_export_failed_spans_total et oxphp_otel_export_retries_total comptent les exports échoués, les spans qu'ils transportaient et les nouvelles tentatives après un échec rapide. Un export échoué est aussi journalisé sous la forme OTLP span export failed, avec la taille du lot et la cause.

Nombre de CPU au démarrage — Une ligne CPU count for default sizing rapporte cpus, cpu_source (host, affinity, cgroup quota ou unknown), host_cpus ainsi que les PHP_WORKERS et TOKIO_WORKERS qui en résultent. cpu_source=host sur une offre vendue avec moins de CPU, comme on l'observe sur DigitalOcean App Platform sous gVisor, signifie qu'il faut définir les deux explicitement.

Image nightly Debian (glibc) — :nightly-trixie, :php8.4-nightly-trixie et :php8.5-nightly-trixie exécutent le même serveur sur php:<minor>-zts-trixie et ont consommé 14–19 % de CPU en moins par réponse qu'Alpine sur une application Yii3. www-data y a l'UID 33 et les extensions se compilent dans une étape trixie ; les versions taguées restent sur Alpine.

PERFORMANCE

Appels de fonctions PHP moins coûteux — L'observateur du moteur qui comptait chaque appel, fonctions internes comprises, pour classer les requêtes bloquées a disparu. Sur une application Yii3, le CPU par requête baisse de 5 % en mode classique et de 7 % en mode worker ; un script effectuant 15 000 appels courts par requête en consomme 23–25 % de moins.

FIXED

APM : hGet, hSet, lPush et rPush de phpredis — Avec OTEL_APM_ENABLED activé, ces quatre méthodes renvoyaient NULL sans atteindre Redis : les écritures étaient perdues et les lectures revenaient vides, sans erreur ni span. Le hook compare désormais les noms de méthodes sans tenir compte de la casse, comme le fait PHP.

PHP 8.5 : memory_limit après une erreur de mémoire insuffisante — Un OOM survenu en profondeur de pile en mode worker ou dans une tâche asynchrone laissait le worker sans aucune limite, si bien que la requête incontrôlée suivante grossissait jusqu'à ce que le noyau tue le serveur et tous ses workers. La limite est rétablie à la fin de la requête, et un worker qui la dépasse encore est recyclé.

Un débordement de la pile d'appels ne provoque plus de segfault — Une récursion atteignant la limite de pile de PHP en mode worker ou dans une tâche asynchrone se termine par l'Error habituelle au lieu de faire tomber le serveur ; un realpath() au dernier niveau autorisé suffisait. La limite est désormais celle que PHP fixe pour chaque Fiber.

Mode worker : erreurs fatales dans une Fiber — Une erreur fatale dans une Fiber lancée par la requête, ou dans un destructeur exécuté par le ramasse-miettes, lisait des frames d'appel déjà libérées et pouvait faire planter le processus avec toutes ses connexions. La requête se termine désormais par un 500 et le worker continue de servir.

Mode worker : les délais d'expiration terminent la bonne requête — L'unique timer par thread terminait la requête en cours d'exécution, quelle qu'elle soit, et comptait celle qu'il visait comme un échec du gestionnaire, recyclant le worker au bout de trois. Chaque requête a désormais sa propre échéance ; une échéance qui passe pendant que la requête est suspendue la termine par un 504 qui ne compte pas pour le recyclage.

Mode worker : max_input_time après l'amorçage — Avec max_execution_time = 0 et max_input_time défini, le timer armé pour l'amorçage du worker terminait une requête max_input_time secondes plus tard (60 avec les modèles de php.ini fournis par PHP). Le worker l'arrête désormais avant de servir.

Tâches asynchrones et échéance du thread de pool — L'unique timer d'un thread de pool expirait une fois, environ max_execution_time après le démarrage du processus, terminant la tâche en cours, quelle qu'elle soit, avec Maximum execution time of 0 seconds. Les threads de pool ne portent désormais plus d'échéance, si bien que set_time_limit() ne borne plus une tâche : bornez-la avec le délai d'expiration de oxphp_async_await().

Connexions persistantes fermées à la sortie du worker — Chaque recyclage en mode worker et chaque réduction du pool dynamique laissaient ouvertes les connexions PDO::ATTR_PERSISTENT, mysqli p: et pfsockopen() jusqu'au redémarrage du processus, et elles s'accumulaient jusqu'à ce que la base de données refuse de nouveaux clients. Elles sont désormais fermées après la dernière requête du thread.

Mode worker : mémoire laissée par les appels internes annulés — Une requête terminée à l'intérieur d'une fonction interne (usort() au milieu de son comparateur, par exemple) faisait fuir ce que cette fonction avait alloué, pour toute la vie du worker. Un worker qui a grossi de plus de 4 MiB au cours d'une telle requête est désormais recyclé à son prochain moment d'inactivité, avec un avertissement.

Erreurs fatales dans les callables Shared — Une erreur fatale dans Mutex::withLock(), Once::getOrInit(), une fabrique de Registry ou Pool::with() laissait, pour toute la vie du processus, le mutex verrouillé, le Once en attente, la clé bloquée en cours de création ou le pool amputé d'un emplacement. Tout est désormais rendu à la fin de la requête.

RUNTIME_HOOKS=streams avec l'APM — Les wrappers de l'APM pour PDO, mysqli et Redis contournaient la protection de connexion, si bien que des Fibers partageant une connexion pouvaient lire les réponses Redis les unes des autres ou recevoir Cannot execute queries while other unbuffered queries are active. Les wrappers appellent désormais le gestionnaire protégé.

RUNTIME_HOOKS=streams : destructeurs entre les requêtes — Un destructeur exécuté par la collecte de cycles de la boucle du worker pouvait écrire au milieu de l'échange d'une autre requête sur une connexion partagée et en lire la réponse. Une attente de socket sur une connexion dont le détenteur y est parqué échoue désormais immédiatement, comme un délai d'expiration, et n'envoie rien.

Les constructeurs PDO persistants attendent par source de données — Un constructeur bloqué sur une base de données qui ne répond jamais retenait tous les autres constructeurs persistants du worker jusqu'à l'expiration de leur attente, après quoi ils poursuivaient sans sérialisation. Ils n'attendent désormais que les constructeurs vers le même DSN.

Libération d'un handle PDO sur une connexion persistante partagée — Avec RUNTIME_HOOKS=streams, une requête libérant son handle annulait la transaction d'une autre requête et, avec pdo_mysql, pouvait libérer un résultat en cours de lecture et provoquer un segfault. Seul le détenteur actuel de la connexion effectue désormais ce nettoyage, et PDO::rollBack() depuis une autre Fiber n'envoie rien.

Les recyclages planifiés ne sont pas des plantages — Un worker recyclé en raison de WORKER_MAX_MEMORY_MIB ou de Worker::scheduleExit() est journalisé en INFO sous la forme Worker mode thread recycled, avec la raison, au lieu d'un WARN Dead workers detected qui se lisait comme un plantage.

Export OTLP en http/protobuf — Aucun span n'avait jamais été exporté par ce protocole : le thread d'export paniquait et chaque span journalisait Spans are being emitted even after Shutdown. Les spans atteignent désormais /v1/traces sous OTEL_EXPORTER_OTLP_ENDPOINT, qui est une URL de base ; retirez un /v1/traces qui y figurerait déjà.

OTLP sur gRPC : établissement de connexion et connexions mortes — OTEL_EXPORTER_OTLP_TIMEOUT borne désormais aussi l'établissement de la connexion, et sous Linux une connexion qui cesse d'accepter des données est remplacée au lieu de faire échouer chaque export pendant environ 15 minutes. Un export qui échoue rapidement sur une connexion coupée pour inactivité est renvoyé une fois. Un OTEL_EXPORTER_OTLP_ENDPOINT défini mais vide est désormais refusé au démarrage.

Classification de oxphp_worker_stuck_total — Une boucle PHP sans aucun appel se lisait comme c_call, et la première mesure d'une requête bloquée pouvait être comparée à celle d'une requête antérieure. Le superviseur interroge désormais le moteur lui-même, via l'interruption par laquelle max_execution_time est délivré.

UploadedFile::moveTo() sans fileinfo — Sans mime_content_type(), ou sous un gestionnaire d'erreurs qui transforme les avertissements en exceptions, moveTo() levait une exception avant le déplacement et le fichier téléversé était supprimé à la fin de la requête. Il déplace désormais d'abord et laisse la détection du type à type().

Exceptions de destructeur à la fin d'une tâche asynchrone — Une exception levée par le destructeur d'un objet libéré pendant le démontage de la tâche disparaissait sans un mot ; elle est désormais signalée comme non rattrapée et atteint le journal d'erreurs.

index.json du profileur enregistre user_agent — Le champ documenté valait toujours null.

Documentation : guide SSE — L'exemple en mode worker lisait les événements avec BRPOP, si bien que chaque événement n'atteignait qu'un seul client ; il lit désormais un Redis Stream et reprend à partir de Last-Event-ID. Le guide n'affirme plus qu'une boucle while (!connection_aborted()) se termine par sa condition, et son exemple appelle ignore_user_abort(true) et set_time_limit(0).

SECURITY

Fichiers statiques : liens symboliques redirigés après le routage — Un lien situé dans DOCUMENT_ROOT, validé lors d'une requête puis redirigé ensuite, vers /etc/passwd par exemple, était suivi à la requête suivante. Les fichiers sont désormais vérifiés après leur ouverture, par rapport à l'emplacement que le noyau rapporte pour le descripteur ; un fichier encore présent dans le cache en mémoire conserve ses octets validés.

Scripts PHP : liens symboliques redirigés après le routage — Hors mode worker, un tel lien pouvait exécuter le script d'un attaquant, afficher un fichier texte ou bloquer un worker sur une FIFO une fois que le cache realpath de PHP avait lâché l'ancienne cible. Le worker ouvre désormais lui-même le script, vérifie le descripteur et transmet ce fichier à PHP ; un lien substitué répond 404. Coût : environ 3 % du débit sur un script trivial.

PHP 8.5 : error_get_last() d'une requête à l'autre — En mode worker et dans les tâches asynchrones, une requête en pause à un point de suspension pouvait lire la backtrace d'erreur fatale d'une autre requête, avec les arguments passés à ses fonctions : mots de passe, jetons, données clients. Chaque requête conserve désormais sa propre backtrace et la libère à sa fin.

TRUSTED_PROXIES : entrées de transfert non analysables — La règle de l'entrée non approuvée la plus à droite sautait les entrées qu'elle ne pouvait pas analyser et poursuivait sa lecture jusque dans ce que le client avait écrit, si bien que derrière une passerelle qui écrit IP:port, le client choisissait sa propre adresse. Une entrée non analysable ou une ligne d'en-tête illisible arrête désormais la règle.

En-têtes Cookie fractionnés — Les cookies répartis sur plusieurs lignes de champ, comme HTTP/2 le permet, sont désormais réunis selon la RFC 9113 avant toute lecture. Le cookie __oxp_* d'un plugin sur une ligne ultérieure atteignait l'application, et en retirer un de la première ligne faisait perdre les propres cookies de l'application sur les lignes suivantes.

v0.12.0
2026-09-25
⚠ breaking changes
Release notes →
BREAKING

SHARED_ENABLED=false désactive désormais Shared\* — Ce réglage était analysé puis ignoré ; désormais aucune classe OxPHP\Shared\* n'est enregistrée et la première construite lève Error: Class "OxPHP\Shared\Counter" not found. Il est aussi lu sous OX_SHARED_ENABLED et sous un ENABLED nu : vérifiez les trois avant la mise à jour ; class_exists() indique au code dans quel mode il s'exécute.

Mode worker : une déconnexion du client ne termine plus la requête — Le gestionnaire s'exécute jusqu'au bout pour que ses blocs finally et son nettoyage s'exécutent, comme si ignore_user_abort était toujours actif ; seul max_execution_time le borne. Appelez connection_aborted() pour arrêter plus tôt. Les réponses en streaming sont toujours terminées à leur écriture suivante.

ignore_user_abort(true) s'exécute jusqu'au bout dans tous les modes — Comme sous PHP-FPM, la sortie est jetée et connection_aborted() signale la déconnexion ; une boucle de flux qui comptait sur sa prochaine écriture pour s'arrêter doit vérifier connection_aborted(). ignore_user_abort = On dans php.ini ou dans un amorçage de worker s'applique désormais à chaque flux.

/health répond 503 quand le pool est perdu ou bloqué — Le corps gagne pool_stalled et indique "status": "degraded". Une sonde de liveness Kubernetes pointée sur l'agrégat /health redémarrera désormais les pods ; utilisez /health/liveness, /health/readiness et /health/startup.

ACCESS_LOG n'est plus supplanté par LOG_LEVEL — ACCESS_LOG=all avec LOG_LEVEL=warn n'écrivait rien ; il écrit désormais une ligne par requête, dimensionnez donc le volume de logs. RUST_LOG=warn,access_log=off fait toujours taire le seul journal d'accès.

payload() renvoie les corps de formulaire et les scalaires JSON — Les corps de formulaire sont désormais analysés avec SUPERGLOBALS_ENABLED=false, et un corps JSON valant "hi", 42 ou false renvoie cette valeur au lieu de null ou de $default. null ne signifie plus qu'un corps vide, un Content-Type non analysé, un JSON invalide ou un null littéral.

Déconnexions client comptées comme annulations — oxphp_request_cancelled_total{reason="client_abort"} se met à grimper tandis que admission_refused_total{reason="wait_timeout"} baisse, et la ligne ERROR Connection error d'un client qui a raccroché passe en debug ; les alertes sur l'un ou l'autre sont à revoir.

Exécutions de profil classées par déclencheur, sous leur propre identifiant — oxphp_profiler_runs_total{source} se répartit désormais en header, cookie, query, sample et le nouveau sdk ; les identifiants d'exécution prennent la forme <ts_ms>-<req_id_prefix>-<rand4>, si bien que /__profiler/runs/{id} répond 404 pour un identifiant de requête — cherchez-le par request_id dans /__profiler/runs.

PROFILER_MAX_DEPTH est appliqué — Il ne s'appliquait nulle part. La valeur par défaut est désormais 0 (sans limite) ; les valeurs au-delà de 32 sont plafonnées avec un WARN au démarrage, et les appels sous la limite sont absents du profil.

COMPRESSION_LEVEL déprécié — Utilisez COMPRESSION_BROTLI_LEVEL ; l'ancien nom fonctionne encore avec un WARN au démarrage. Une valeur non nulle ne signifie plus Brotli seul, puisque Zstandard et gzip sont désormais proposés aussi. /config rapporte brotli_level, gzip_level et zstd_level au lieu de compression_level.

SHARED_SOFT_LIMIT_RATIO et SHARED_POISON_STRICT ignorés — Aucun des deux n'a jamais été lu ; tous deux émettent désormais un WARN au démarrage, et limits.soft_ratio et diagnostics.poison_strict disparaissent de /__ox_shared/summary. Alertez sur oxphp_shared_capacity_saturation ; un Once terminal prend Once\FailureMode::Poison.

oxphp_queue_wait_us affiche moins — La mesure s'arrête désormais quand un worker prend la requête, soit environ 0,1 ms de moins par requête ; les seuils d'alerte sous la milliseconde se déclenchent moins facilement. oxphp_request_duration_us est inchangé.

ADDED

Compression Zstandard — Les réponses dynamiques partent en zstd (niveau 6) vers les clients qui l'acceptent : plus petites qu'en Brotli pour moins de la moitié du CPU. COMPRESSION_ENCODINGS (br,zstd,gzip par défaut, ou off) choisit les codages, avec COMPRESSION_BROTLI_LEVEL (5), COMPRESSION_ZSTD_LEVEL (6) et COMPRESSION_GZIP_LEVEL (6).

gzip pour les clients sans Brotli — Chromium en HTTP clair et la plupart des outils CLI recevaient des corps non compressés. Accept-Encoding est désormais classé par q-value selon la RFC 9110, q=0 étant respecté comme un refus ; en conséquence, les fichiers compressibles de moins de 3 MiB répondent aux requêtes Range par un 200 complet.

Cache statique précompressé — Un fichier statique en cache reçoit une copie Brotli de qualité maximale construite une seule fois en arrière-plan, 8–12 % plus petite que la sortie par requête, dans le même budget de cache.

Visibilité des blocages du pool — oxphp_pool_stalled, oxphp_queue_depth, oxphp_queue_capacity et oxphp_admission_slots_available exposent un pool qui a cessé de prendre du travail dans sa file, avec un WARN tant que cela dure et un INFO dès que le travail reprend (mode worker).

La surcharge dans les logs — Les requêtes délestées en 529 produisent désormais un WARN par épisode, répété au plus une fois par minute, et un INFO avec le total dès qu'une minute passe sans refus.

Épuisement de MAX_CONNECTIONS signalé — Une boucle d'acceptation parquée journalise un WARN nommant MAX_CONNECTIONS et un INFO à sa reprise ; oxphp_accept_stalled et oxphp_accept_stalls_total l'exportent.

oxphp_admission_wait_wasted_total — Compte les refus wait_timeout pendant lesquels aucun worker n'a démarré de requête, signe que QUEUE_WAIT_TIMEOUT_MS est trop long ou que l'application rappelle son propre serveur.

oxphp_worker_request_fibers_active — Fibers de requête par worker : 256 signifie que le worker a cessé de prendre du travail, et un compte qui ne revient pas à 0 au repos trahit une fuite de Fiber (mode worker).

RUNTIME_HOOKS visible — /config et oxphp_server_info() rapportent runtime_hooks comme la liste analysée des catégories installées, par ex. ["sleep","streams"], si bien qu'une variable mal orthographiée se lit [].

CHANGED

Budget d'attente de la file adaptatif — QUEUE_WAIT_TIMEOUT_MS est désormais un plafond : le serveur divise l'attente par deux tant qu'un quart ou plus du travail démarré est allé à des clients déjà partis (plancher 10 ms ou 1/64 du plafond) et la rétablit une fois la file vidée. oxphp_admission_wait_budget_us, oxphp_admission_wait_budget_ceiling_us et oxphp_abandoned_work_total l'exposent.

Brotli par requête en qualité 5 — Au lieu de 4, qui produisait une sortie plus volumineuse que gzip pour plus de CPU ; les corps Brotli de plus de 4 KB sont désormais compressés sur le pool bloquant. COMPRESSION_BROTLI_LEVEL=4 rétablit l'ancien comportement.

Niveaux de log de fin de connexion selon le type d'erreur — Un client qui ferme la connexion passe en debug, un délai imposé par le serveur reste en warn, et tout le reste reste en error, au lieu d'être décidé par le mot "timeout" dans le message.

FIXED

Mode worker : fuite de sessions entre requêtes — Une requête admise pendant qu'une autre était suspendue démarrait dans la session de sa devancière et, après session_write_close(), pouvait adopter son identifiant et le recevoir dans Set-Cookie. La session est désormais écrite, fermée et $_SESSION supprimé avant chaque requête.

Connexions persistantes répondant à la mauvaise requête — Hors mode worker, un abandon client pouvait terminer une requête entre l'envoi d'une commande sur pfsockopen() ou Predis persistant et la lecture de la réponse, désynchronisant la connexion jusqu'au redémarrage. Un client parti n'est désormais pris en compte qu'à l'écriture suivante, comme sous PHP-FPM.

Mode worker : pages changeant de composition après des abandons — Les requêtes abandonnées sautaient les blocs finally et, interceptées à une frontière d'opcode, désactivaient tous les destructeurs du worker, si bien que les gardes de récursion restaient positionnées et que des pages perdaient silencieusement des blocs. Les requêtes s'exécutent désormais jusqu'au bout.

Mode worker : valeurs ini par requête — Les modifications faites avec ini_set(), ignore_user_abort() ou error_reporting() ne fuient plus vers les requêtes qui s'exécutent à côté et ne survivent plus sur un worker occupé ; elles voyagent avec une requête suspendue et sont restaurées à sa fin. Une requête ne démarre plus non plus au niveau error_reporting laissé par la requête précédente sur sa Fiber.

PDO persistant sous RUNTIME_HOOKS=streams — Des constructeurs PDO::ATTR_PERSISTENT qui se chevauchent ne ferment plus une connexion sous la requête qui l'utilise (erreur fatale ou segfault), et une requête pendant le ping de vivacité d'une autre requête ne tue plus la connexion avec 2006 MySQL server has gone away.

Mode worker : les erreurs fatales ne font plus tourner un worker indéfiniment — Avec le profileur ou #[Trace] activé, deux erreurs fatales sur un même worker pouvaient faire boucler la chaîne d'observateurs à 100 % de CPU alors que le pool semblait inactif. Une erreur fatale levée pendant la récupération d'une autre ne fait plus non plus perdre toutes les requêtes du worker.

Mode worker : exceptions non rattrapées avec __destruct — Une classe d'exception dotée d'un destructeur transformait un 500 en erreur fatale comptée pour le recyclage (31 requêtes ont retiré 8 workers), désactivait tous les destructeurs du worker et faisait fuir l'exception.

Mode worker : les déconnexions client ne recyclent plus les workers — Les abandons après ignore_user_abort() ou pendant la prise en charge se lisaient comme des erreurs fatales et faisaient tourner le pool trois workers à la fois. Chaque requête que le disjoncteur compte journalise désormais un WARN, et une sortie par le disjoncteur est journalisée en WARN.

Mode worker : flux terminés par une écriture — Un tel flux ne fait plus fuir les variables de ses frames pour la vie du worker, et une erreur fatale dans un destructeur pendant ce nettoyage compte désormais pour le disjoncteur d'erreurs.

QUEUE_WAIT_TIMEOUT_MS borne les requêtes en file — Le budget n'était vérifié que lorsqu'un worker prenait une requête, si bien qu'un pool bloqué ne répondait jamais aux requêtes en file ; elles reçoivent désormais un 529 avec Retry-After: 3 à l'échéance.

Clients partis ni mis en file ni refusés — Le mode worker n'exécute plus une requête dont le client est parti pendant qu'elle était en file, et un budget épuisé après le départ du client répond 499 comme une annulation plutôt que 529. client_abort compte désormais les clients partis avant qu'un worker ne les prenne en charge.

/health/readiness reflète la santé du pool — La vérification de l'exécuteur passait toujours ; un pool sans aucun thread worker vivant, ou bloqué depuis une minute en mode worker, répond désormais 503. La surcharge, elle, ne le fait toujours pas.

oxphp_apm_trace() exécute son callback — Elle renvoyait null sans l'appeler. Le callback s'exécute désormais dans un span enfant, reçoit l'identifiant du span et renvoie sa valeur, le type de retour étant désormais mixed.

Messages des classes Error perdus — TypeError, ValueError et le reste de la hiérarchie Error atteignaient les spans et les motifs de poison de Once sans leur message ni leur code.

Types de retour mixed sous JIT — Des fonctions comme oxphp_async_await() et Shared\Map::get() déclaraient un type mixed malformé, si bien que les boucles chaudes sous le JIT tracing faisaient fuir chaque valeur renvoyée et que getReturnType() de Reflection provoquait un segfault.

oxphp_worker_memory_bytes rapporte la mémoire — Elle lisait 0 en permanence ; elle rapporte désormais le même tas PHP que celui confronté à WORKER_MAX_MEMORY_MIB, échantillonné à la fin de chaque requête.

Mode worker : profils mélangés entre requêtes — Les requêtes suspendues sur un même thread partageaient l'état des spans du profileur et de l'APM, produisant des profils et des spans #[Trace]/#[SlowThreshold] attribués à la mauvaise requête ; chaque requête porte désormais le sien.

Troncature du profileur et OXPROF — Les exécutions qui atteignent PROFILER_MAX_SPANS sont marquées truncated dans index.json, /__profiler/stats et oxphp_profiler_truncated_total, et le cookie OXPROF documenté démarre désormais une exécution.

Les attentes de Shared\Mutex::withLock() peuvent être interrompues — Une requête attendant un verrou cède désormais la main à max_execution_time, à l'arrêt et à l'annulation de tâche toutes les 100 ms, si bien qu'un verrouillage en ordre inverse ne bloque plus deux workers jusqu'au redémarrage.

RUNTIME_HOOKS=streams et extensions chargées tardivement — mysqli et phpredis activés depuis un fichier ini trié après extension.ini sont désormais protégés comme les autres ; RUNTIME_HOOKS est aussi lu une seule fois au démarrage au lieu d'un getenv() sans verrou à chaque requête.

Cas limites de la compression — Les fichiers statiques entre 1 MiB et 3 MiB sont compressés ; les valeurs Content-Type en casse mixte sont compressées ; les copies identity des réponses compressibles portent Vary: Accept-Encoding ; et les réglages de compression non-UTF-8 échouent au démarrage.

Erreurs de capacité de Shared\Counter, Atomic et Flag — Leurs constructeurs lèvent désormais Shared\CapacityException au-delà de SHARED_MAX_ENTRIES ou SHARED_MAX_BYTES, comme tous les autres types Shared.

Corrections de la documentation — Les valeurs de SHARED_LOCK_DIAGNOSTICS sont off, warn (par défaut) et strict ; le recyclage des workers par nombre de requêtes n'est plus annoncé ; la boucle du worker se termine aussi au retrait par le pool dynamique ; le 504 vient de max_execution_time ; ainsi que les bornes de l'arrêt gracieux, l'organisation des ini de l'image d'exécution et l'ordre des clés de /config.

SECURITY

rustls 0.23.45 — Corrige une faille de la poignée de main TLS 1.3 (RUSTSEC-2026-0285) qui acceptait des messages de poignée de main en clair au-delà d'une frontière de chiffrement, affectant l'écouteur TLS et les exportateurs OTLP et du profileur ; la transcription reste authentifiée.

v0.11.0
2026-08-20
⚠ breaking changes
Release notes →
BREAKING

FRAME_OPTIONS passe par défaut à SAMEORIGIN — Le framing inter-origines reste bloqué et l'intégration de même origine (aperçus d'admin, widgets de tableau de bord) fonctionne d'emblée ; les déploiements qui comptaient sur le DENY intégré doivent désormais le définir explicitement.

Mode worker : pas de boucle d'événements dans une requête — Une requête en mode worker est désormais une Fiber, et Revolt refuse d'exécuter sa boucle à l'intérieur : Revolt\EventLoop::run() dans un gestionnaire lève une exception. Déplacez-la vers l'amorçage du worker ; defer(), delay() et getSuspension() continuent de fonctionner.

Mode worker : l'état de requête se termine avec la requête — ini_set(), set_time_limit() et error_reporting() reviennent à la référence de l'amorçage entre les requêtes ; filter_input() ne répond que pour la requête qui demande ; un handle php://input conservé au-delà de sa requête se lit comme une ressource fermée au lieu du corps d'une autre requête.

query() et cookie() décodent leurs valeurs — Le code qui compensait avec rawurldecode() décode désormais deux fois et doit retirer l'appel superflu. Un cookie envoyé vide se lit comme une chaîne vide plutôt que null, et query() analyse au plus 1000 paramètres.

Analyse stricte de MAX_CONNECTIONS / QUEUE_CAPACITY — Les valeurs malformées interrompent le démarrage au lieu de retomber silencieusement sur les valeurs par défaut, et QUEUE_CAPACITY=0 signifie désormais auto (workers × 128) au lieu d'une file de capacité nulle.

ADDED

Budget d'attente de la file — Une requête arrivant sur une file pleine attend désormais une place jusqu'à QUEUE_WAIT_TIMEOUT_MS (1 s par défaut) au lieu d'un 529 instantané ; une seule échéance estampillée à l'arrivée couvre l'attente d'une place et dans la file. QUEUE_MAX_WAITING et QUEUE_MAX_WAITING_BYTES bornent l'ensemble en attente en requêtes et en octets de corps.

RUNTIME_HOOKS=streams — Les lectures bloquantes de sockets tcp:// et stream_select() suspendent la Fiber au lieu du thread worker, couvrant mysqlnd (PDO_MySQL, mysqli), phpredis et les wrappers de flux HTTP sans modification de code. Les connexions partagées entre Fibers concurrentes sont revendiquées par échange, si bien que les requêtes ne peuvent pas lire les réponses les unes des autres.

oxphp_admission_refused_total — Les requêtes délestées avant d'atteindre un worker laissent désormais une trace côté serveur, avec un label reason séparant les quatre motifs de surcharge (529) de shutting_down (503) et pool_unavailable (500).

Capture automatique des exceptions sur le span racine — Une exception non gérée ou une erreur fatale qui répond 5xx attache un événement exception (type, message, stacktrace, fichier, ligne) au span racine de la requête dans les quatre modes de service — aucun attribut ni appel à oxphp_apm_error() requis.

Attributs de span base de données — Les spans PDO et mysqli portent db.statement (valeurs littérales obfusquées en ?), db.operation, db.system, server.address/port, db.name et oxphp.db.slow ; mysqli_stmt::prepare rejoint l'ensemble intercepté comme fonction numéro 34.

CHANGED

oxphp_queue_wait_us atteint une seconde — Quatre nouveaux buckets (100 ms à 1 s) quantifient les attentes proches du budget par défaut au lieu de les fondre dans +Inf ; les requêtes refusées sont exclues de l'histogramme.

Le recyclage des workers compte ce qui casse le worker — Seuls les erreurs fatales, les dépassements de mémoire et les débordements de pile comptent pour le recyclage aux trois échecs consécutifs ; les exceptions non rattrapées et les requêtes annulées sont neutres. Le disjoncteur se déclenche désormais aussi sur le chemin de dispatch synchrone, où il ne se déclenchait jamais.

Avis de dimensionnement de config --check — Avertit, sans échouer, lorsque PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING atteint MAX_CONNECTIONS, le point au-delà duquel la surcharge ne répond plus rien au lieu d'un 529.

Documentation aplatie — Les docs du dépôt sont passées de docs/en/ à docs/ ; les traductions russe et chinoise ont déménagé sur oxphp.dev, où le français, le polonais et le japonais vivaient déjà.

FIXED

Mode worker : $_POST et $_FILES renseignés — Les deux étaient vides pour chaque requête qu'un worker ait jamais servie, si bien que les formulaires HTML et les téléversements de fichiers ne fonctionnaient silencieusement pas ; les API JSON lisant php://input n'étaient pas touchées, ce qui explique que le problème soit resté caché.

Fuite inter-requêtes de filter_input() — La copie d'entrée propre à l'extension filter s'accumulait pour la vie du worker, répondant à une requête avec les valeurs de requête, cookies de session et champs de corps d'un autre client ; elle est désormais rendue au début de chaque requête.

Rafales de timers de sleep — Plus de 32 timers expirant dans un même poll de l'ordonnanceur laissaient les dormeurs excédentaires parqués pour toujours ; une grande rafale se réveille désormais par lots de 32.

Fermeture d'une connexion partagée — fclose(), $mysqli->close() ou un assistant de reconnexion sur une connexion qu'une autre requête était parquée à lire reprenait cette requête dans de la mémoire libérée ; cela échoue désormais immédiatement avec un 500 nommant ce qui s'est passé.

Les erreurs fatales n'effacent plus l'état partagé — Le déroulement d'une requête interrompue libérait deux fois les variables d'include, réinitialisant silencieusement les tableaux et objets dans lesquels les applications en mode worker gardent leur état inter-requêtes.

Cadencement du pool asynchrone — Les transferts de tâches ne sont plus cadencés par le timer d'inactivité fixe de 1 ms : un transfert via Shared\Channel est passé de 0,69 ms à 0,14 ms par élément, et les réveils à vide de 777/s à 92/s.

Les requêtes en pause gardent leur heure de départ — $request->startTime() et oxphp_server_info()[request_time] ne lisent plus 0 ni la valeur d'une requête concurrente après une suspension.

Les avertissements de lecture de corps atteignent la bonne requête — Les avertissements post_max_size et de limite d'upload se déclenchent désormais dans la requête qui a envoyé le corps, si bien que les gestionnaires d'erreurs voient ses superglobales, et celui qui lève une exception répond 500 au lieu de fermer la connexion sans rien.

v0.10.0
2026-07-12
⚠ breaking changes
Release notes →
BREAKING

Abandon des privilèges par défaut — Démarré en tant que root là où un compte www-data existe, le serveur lie ses sockets d'écoute et bascule vers www-data avant de servir le moindre trafic ; l'image officielle ne s'exécute plus en tant que root. Passez --user=root pour conserver l'ancien comportement.

Configuration TLS fail-closed — Une paire à moitié configurée (TLS_CERT sans TLS_KEY, ou l'inverse) ou une valeur de certificat/clé non-UTF-8 interrompt désormais le démarrage au lieu de servir silencieusement du HTTP en clair, en nommant la variable fautive.

Analyse stricte de ASYNC_* / SUPERGLOBALS — Une valeur invalide dans ASYNC_WORKERS, ASYNC_QUEUE_CAPACITY ou ASYNC_MAX_FIBERS provoque une erreur au démarrage au lieu d'un repli silencieux sur la valeur par défaut, et oxphp run n'ignore plus un SUPERGLOBALS_ENABLED invalide.

QUERY sans Content-Type renvoie 400, et non 415 — Le seul 415 généré par le serveur a disparu ; une page ERROR_PAGES_DIR/415.html personnalisée pour ce cas doit être renommée en 400.html.

Résolution de l'enveloppe d'export du profileur — PROFILER_EXPORT_XHGUI=false l'emporte désormais même sur une URL /run/import, et l'auto-détection de l'enveloppe se fonde uniquement sur le chemin canonique de chaque outil (xhgui /run/import, Buggregator /api/profiler/store), et non plus sur une sous-chaîne approximative "xhgui".

ADDED

Export du profileur vers Buggregator — Le push HTTP du profileur encapsule les données xhprof dans l'enveloppe attendue par le POST /api/profiler/store de Buggregator ; PROFILER_EXPORT_BUGGREGATOR l'active, avec tags et app_name définis via l'environnement.

Export des traces OTLP via TLS — Un OTEL_EXPORTER_OTLP_ENDPOINT en https:// est désormais exporté chiffré aussi bien en gRPC qu'en http/protobuf, vérifié contre le magasin de confiance système via rustls ; l'image d'exécution doit embarquer un bundle de CA (c'est le cas de l'image officielle).

Données d'exception complètes sur les spans APM — L'événement d'exception du span porte désormais exception.message et exception.stacktrace en plus de exception.type, si bien que New Relic Errors Inbox, Jaeger et Tempo affichent le message et la pile ; plafonné par OTEL_APM_MESSAGE_MAX_BYTES / OTEL_APM_STACKTRACE_MAX_BYTES.

Drainage gracieux des connexions longue durée — À la réception de SIGTERM, le serveur envoie GOAWAY, ferme les keep-alive inactifs et termine promptement les flux SSE/flush ouverts ; la valeur par défaut de DRAIN_TIMEOUT_SECONDS est abaissée de 30 à 25 pour tenir dans la période de grâce de 30 s de Kubernetes.

TLS_MIN_VERSION — Version de protocole minimale acceptée : 1.2 (par défaut) ou 1.3. Toute autre valeur — y compris une syntaxe étrangère comme "TLSv1.2" — provoque une erreur fatale au démarrage, validée même lorsque TLS est désactivé et reportée sous tls_min_version dans /config.

CHANGED

STATIC_REVALIDATE économique — stat() au plus une fois toutes les 3 secondes par fichier ; un accès statique est servi depuis la mémoire avec au plus un appel système, et les modifications sur disque deviennent visibles sous 3 s. Reste désactivé par défaut.

Erreurs de démarrage TLS plus claires — Un chemin comportant une faute échoue avec "TLS_CERT: cannot read /etc/ssl/cert.pem: No such file or directory" au lieu d'une erreur brute du système d'exploitation ; les erreurs d'analyse PEM et de non-correspondance de clé sont préfixées de la même façon.

FIXED

Ciblage de l'annulation en mode worker — Un abandon client ou un délai d'expiration de requête pouvait annuler la requête la plus récemment acceptée par le worker au lieu de celle réellement visée par l'annulation, interrompant une requête en cours sans rapport.

finish_request() face aux flux multiplexés — Dès qu'une requête sur un worker appelait oxphp_finish_request(), tout autre flux multiplexé sur ce worker perdait silencieusement la sortie de ses appels ultérieurs à oxphp_stream_flush().

v0.9.0
2026-06-25
⚠ breaking changes
Release notes →
BREAKING

Un await expiré annule la tâche — Une tâche abandonnée est annulée de force : les tâches parquées reprennent pour être annulées, celles liées au CPU sont interrompues à une frontière d'opcode. Auparavant, elle s'exécutait jusqu'à la fin de la requête.

ADDED

oxphp_async() imbriqué — Une tâche asynchrone peut dispatcher ses propres tâches et se suspendre sur await_all / await_any / await_race sans bloquer son thread worker. La restriction interdisant l'async imbriqué a disparu.

Plafond de tâches en cours — ASYNC_MAX_FIBERS (256 par défaut) borne les tâches en file d'attente + en cours d'exécution ; toute distribution au-delà du plafond est rejetée immédiatement avec AsyncException, de sorte qu'un fan-out ne peut pas provoquer d'interblocage.

Liste d'autorisation du serveur interne — INTERNAL_ALLOW_IPS : une liste d'autorisation CIDR pour /metrics et /config ; les endpoints de santé restent accessibles aux orchestrateurs.

Exclusions d'échantillonnage du profileur — PROFILER_EXCLUDE_PATHS garde le trafic interne du framework (/_profiler, /_wdt) hors de l'échantillonnage statistique ; les déclencheurs explicites profilent toujours.

FIXED

Réveil de l'émetteur de Channel — Un Fiber en attente d'envoi parqué sur un Shared\Channel plein se réveille lorsque recv_blocking libère un emplacement — sendTimeout ne se déclenche plus de façon intempestive en cas de saturation.

Cas limites asynchrones — Un crash lors d'un arrêt gracieux ; un await sur une promesse fermée qui se bloque au lieu de rejeter ; await_all laissant des promesses en cours d'exécution lors d'un abandon anticipé.

v0.8.0
2026-06-13
⚠ breaking changes
Release notes →
BREAKING

Contrat de routage du worker — Les ressources statiques depuis le disque, tout le reste vers l'ENTRY_FILE du worker — les fichiers .php arbitraires, les index de répertoire et le repli sur l'index.php racine ne sont plus résolus par requête.

PATH_INFO CGI honnête — En mode Framework, PATH_INFO n'est défini que pour une requête /index.php/extra explicite ; PHP_SELF renvoie le front controller. Routez sur REQUEST_URI.

ADDED

Requêtes Range (RFC 9110) — 206 Partial Content pour les fichiers statiques : lecture vidéo avec repositionnement, téléchargements reprenables, chargement partiel de PDF ; If-Range honoré avec des validateurs forts.

Limites de connexion HTTP/2 — Cinq variables d'environnement bornent l'utilisation des ressources par connexion ; les valeurs résolues sont journalisées au démarrage.

CHANGED

ETags statiques forts — "<size>-<mtime>" remplace W/"…" — requis par If-Range ; les représentations compressées conservent un tag faible.

FIXED

Les en-têtes de l'application l'emportent — X-Content-Type-Options, X-Frame-Options et CSP définis depuis PHP ne sont plus écrasés — les valeurs du serveur sont des replis insérés uniquement s'ils sont absents.

Streaming face à la compression — Les réponses diffusées via flush() passent sans compression au lieu d'être intégralement mises en tampon par la couche brotli jusqu'à la fin du script.

SECURITY

Durcissement DoS HTTP/2 — Les attaques Rapid Reset (CVE-2023-44487), les bombes HPACK et les attaques par blocage de fenêtre sont bornées ; les connexions mortes sont récupérées via un keepalive PING/PONG.

v0.7.0
2026-06-05
⚠ breaking changes
Release notes →
BREAKING

remote_ip dans les journaux d'accès — Le champ JSON remote_addr est renommé remote_ip et porte l'IP sans port — à l'image de nginx/Apache/Caddy.

ADDED

CLI façon PHP — oxphp run script.php exécute un fichier avec la sémantique CLI et le moteur complet en dessous ; les scripts #!/usr/bin/env oxphp s'exécutent directement ; les surcharges ini via -d sont acceptées.

Abandon de privilèges via --user — Lie les ports privilégiés en tant que root, puis abandonne irréversiblement les privilèges vers un utilisateur non privilégié avant que le moindre thread de traitement des requêtes ne soit créé.

Port transféré honoré — Derrière TRUSTED_PROXIES, SERVER_PORT honore X-Forwarded-Port ; REMOTE_PORT est renseigné à partir du for= de la RFC 7239 lorsque le proxy en fournit un.

FIXED

Plantages de décorateur — Deux SIGSEGV dans le chemin du décorateur de fonction (cache d'instances libéré ; zval pendant lors d'un redimensionnement) ont disparu ; un dépassement d'imbrication lève désormais StackOverflowException.

Les attributs du profileur respectent leurs arguments — #[SlowThreshold(ms:)], #[MemoryThreshold(kb:)] et #[Mark(label:)] ne se réduisent plus à des valeurs par défaut partagées ; les événements de span APM parviennent à OpenTelemetry.

API d'objets d'upload câblée — Request::file() / files() renvoient des objets UploadedFile pour toutes les formes de $_FILES au lieu de null.

v0.6.0
2026-05-28
⚠ breaking changes
Release notes →
BREAKING

API d'attente trichotomique — Channel et Mutex scindent chaque attente en try* / verbe nu / *Timeout(int $ms), renvoyant des SendResult / RecvResult typés par valeur au lieu de mixed|null.

await_any signifie Promise.any — Le premier résolu l'emporte ; si tous sont rejetés, AggregateAsyncException est levée. L'ancien comportement « premier réglé » correspond désormais à oxphp_async_await_race().

Statuts d'annulation honnêtes — Délai d'expiration → 504, drainage gracieux → 503 + Retry-After, déconnexion client → 499 (journalisé uniquement). Les arrêts forcés par le superviseur et les annulations en espace utilisateur conservent 500.

Renommages sans shims — Async\Exception → AsyncException, Shared\Exception → SharedException ; Worker abandonne les préfixes get* ; Counter devient minimal (add/set) ; Flag reflète Atomic ; Once gagne getOrInit()/status().

ADDED

Shared\Atomic + Ordering — Un atomique int64 générique : load/store/swap/CAS/fetch* avec des ordres mémoire explicites, en correspondance directe avec std::sync::atomic.

Shared\Registry — Des handles globaux au processus indexés par nom : chaque worker converge vers la même entrée ; la fabrique s'exécute au plus une fois.

REMOVED

Une seule source de délai d'expiration — REQUEST_TIMEOUT_SECONDS et oxphp_request_heartbeat() ont disparu — max_execution_time et set_time_limit() subsistent.

FIXED

connection_aborted() en SSE — Renvoie true après une déconnexion en cours de flux, si bien que les boucles while (!connection_aborted()) se terminent proprement.

PERFORMANCE

Chemin critique de Shared\* — Le surcoût par opération se situe dans le bruit de mesure de criterion par rapport à une charge atomique brute ; ≈4,7× en moyenne géométrique sur 8 threads face à la forme précédente.

v0.5.0
2026-05-05
⚠ breaking changes
Release notes →
BREAKING

Analyse stricte des booléens d'environnement — Uniquement on/true/1/yes et off/false/0/no ; une faute comme ture refuse de démarrer au lieu de retomber silencieusement sur la valeur par défaut.

ADDED

ENTRY_FILE + WORKER_MODE_ENABLED — Une variable sélectionne le mode de routage selon l'extension ; un interrupteur explicite active les workers persistants. INDEX_FILE / WORKER_FILE deviennent des alias dépréciés.

Classe OxPHP\Server\Worker — Un handle d'introspection à l'exécution : id, nombre de requêtes, mémoire, RSS — plus le recyclage piloté par l'application via scheduleExit().

CHANGED

Renommages du cache statique — STATIC_CACHE_TTL → STATIC_MAX_AGE ; STATIC_CACHE → STATIC_REVALIDATE avec polarité inversée. Valeurs par défaut inchangées.

DEPRECATED

WORKER_MAX_REQUESTS — Analysé et ignoré avec un WARN au démarrage — migrez vers WORKER_MAX_MEMORY_MIB ou scheduleExit().

v0.4.0
2026-05-02
Release notes →
ADDED

Images PHP 8.5 — :php8.5, :php8.5-alpine et les tags épinglés au patch sont livrés aux côtés de 8.4 ; latest reste sur 8.4 jusqu'à la fin de la fenêtre de rodage.

FIXED

Streaming chunked entre requêtes — Le contexte par requête du bridge fuyait entre les requêtes du worker, abandonnant le Transfer-Encoding chunked après le premier flush.

Shared\* dans les Fibers utilisateur — Channel::recv()/send() à l'intérieur de Fiber::start() n'empruntent plus le chemin de suspension oxphp-fiber ni ne plantent avec fiber_await rc=1.

Arguments nommés pour l'API de plugins — Les classes et fonctions définies par les plugins exposent désormais de vrais noms de paramètres — les appels à arguments nommés fonctionnent.

Handles partagés dans les closures asynchrones — Channel/Map/Pool capturés par une closure oxphp_async() n'arrivent plus à null sur le thread récepteur.

v0.3.0
2026-04-22
⚠ breaking changes
Release notes →
BREAKING

Namespace Async — Les classes Async sont déplacées sous OxPHP\Async\ ; les fonctions sont désormais livrées dans plugin-async.

ADDED

Sept primitives Shared\* — Counter, Flag, Once, Mutex, Channel, Map, Pool — un état partagé atomique entre workers, avec une observabilité par registre et un détecteur d'interblocage.

Profileur par requête — Sorties xhprof / speedscope / pprof / collapsed ; déclencheurs par cookie, en-tête, chaîne de requête et statistique ; push HTTP avec une enveloppe xhgui.

Auto-instrumentation APM — plugin-apm avec un SDK de traçage PHP et la capture d'erreurs ; les sous-systèmes async et APM sont déplacés dans des plugins Rust.

Intégrations pour la production — TRUSTED_PROXIES (RFC 7239), Kubernetes /readyz + /livez, en-têtes de sécurité, limitation de débit par IP — le tout configuré via l'environnement.

PERFORMANCE

Chemin critique de Pool — acquire/release sans contention ≈0,9 µs dans Docker, avec affinité par thread.

v0.2.0
2026-03-27
Release notes →
ADDED

Multiplexage de Fibers — E/S concurrentes au sein d'un même thread worker ; traçage distribué avec W3C Trace Context et export OpenTelemetry.

Promesses oxphp_async() — Exécution PHP parallèle sur un pool de threads dédié.

API d'objets HTTP — OxPHP\Http\Request avec des accesseurs paresseux, plus le système de décorateurs basé sur les attributs.

CHANGED

Port et contre-pression — Port par défaut 8080 → 80 (443 avec TLS) ; la réponse de contre-pression 503 → 529.

FIXED

Exactitude CGI — SERVER_PROTOCOL reflète la version HTTP réelle ; REQUEST_TIME correspond au début de la requête ; analyse du Host IPv6 ; 408 en cas de délai d'expiration de requête.

v0.1.0
2026-03-08
Release notes →
ADDED

Cœur HTTP asynchrone — Un serveur Hyper + Tokio avec arrêt gracieux, un SAPI oxphp personnalisé avec des superglobales complètes, un pool de workers ZTS avec une file bornée et une contre-pression 529.

Trois modes de routage — Traditional, Framework et SPA ; fichiers statiques avec un cache en mémoire, ETag/304, brotli, TLS.

Mode worker — Workers persistants via oxphp_worker(), achèvement anticipé de la réponse, streaming SSE, délais d'expiration coopératifs.

Observabilité — /metrics, /health, /config, journaux d'accès JSON structurés, ID de requête.

Docker d'abord — Une build Alpine multi-étapes ; images amd64/arm64 sur GHCR.