Релізи

Історія змін

Кожна помітна зміна сервера, PHP API та образів — вибрані моменти кожного релізу. Повна, нескорочена історія — у CHANGELOG.md.

остання v0.13.0 · 2026-10-08 · 13 релізів · 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

Режим воркерів: max_execution_time для кожного запиту — Запит, який воркер бере, вже обслуговуючи інші, тепер отримує власний ліміт, що відлічується від моменту, коли його взято, а set_time_limit() впливає лише на запит, який його викликає. SSE-потік або довге опитування, що покладалися на спільний таймер, мають самі викликати set_time_limit(0); релізний образ не містить php.ini, тож ліміт становить 30 с.

RUNTIME_HOOKS=1, true і all включають net — Після оновлення встановлення зʼєднань, рукостискання й читання TLS, записи, що чекають на місце в буфері, а також читання з unix://, udp:// і udg:// призупиняють файбер без жодних змін з вашого боку. runtime_hooks тепер повідомляє ["sleep","streams","net"], а потік (stream), спільний для кількох файберів, захищено незалежно від його транспорту. RUNTIME_HOOKS=sleep,streams зберігає старий набір.

Shareable зарезервовано за типами Shared — PHP-клас більше не може реалізовувати OxPHP\Shared\Shareable чи успадковувати тип Shared, який тепер є final; і те, і інше дає фатальну помилку вже під час оголошення. Передача такого обʼєкта в інший потік раніше обвалювала сервер. createMock() з PHPUnit для типу Shared чи для Shareable тепер завершується помилкою; використовуйте власний інтерфейс-маркер.

Прибрано псевдоніми спостережуваності Shared\* — oxphp_shared_channel_pending і oxphp_shared_pool_size зникають із /metrics, а pending/size — із type_specific у /__ox_shared/entry, разом із WARN під час запуску, що про них повідомляв. Дашборди та сповіщення мають перейти на oxphp_shared_channel_count, oxphp_shared_pool_count і count.

TRUSTED_PROXIES читає записи IP:port — За балансувальником навантаження, що записує IP:port у X-Forwarded-For (Azure Application Gateway), REMOTE_ADDR тепер містить адресу клієнта, а REMOTE_PORT його порт. Якщо CDN перед таким шлюзом теж дописує свою адресу, тепер це буде власна адреса CDN; додайте до списку і її діапазони (для Cloudflare це ключове слово cloudflare).

DOCUMENT_ROOT за символьним посиланням на реліз — Корінь розвʼязується один раз під час запуску, і кожен статичний файл, що читається з диска, та кожен PHP-скрипт перевіряються щодо нього під час відкриття, тож щойно /app/current перемкнуто, вони відповідають 404 до перезапуску сервера. Вкажіть у DOCUMENT_ROOT шлях, що не змінюється між релізами, або перезапускайте сервер у межах перемикання.

Скрипти, які сервер не може відкрити — Поза режимом воркерів видалений скрипт відповідає 404, навіть якщо OPcache тримає його з вимкненим validate_timestamps, тож розгортання, яке видаляє й заново створює файли на місці (git checkout, rsync --delete), має вікно з відповідями 404; записуйте нові файли поруч зі старими й перейменовуйте їх поверх. Скрипт, який не вдається прочитати, відповідає 500 і пише в журнал Cannot open script.

Скрипти за символьними посиланнями виконуються як їхня ціль — Поза режимом воркерів скрипт, до якого ведуть через посилання в інший каталог, виконується з цим каталогом як робочим, а те, чи вважає PHP його phar-архівом, залежить від розвʼязаного імені файлу, а не від URL. Будуйте відносні шляхи від __DIR__.

fiber.stack_size має перевищувати zend.reserved_stack_size — У режимі воркерів і в задачах ASYNC_WORKERS межа стека файбера тепер лишає вільним резерв PHP (48 KiB у релізній збірці), тож за fiber.stack_size, що дорівнює йому або менший, кожен запит завершується помилкою Maximum call stack size of 0 bytes, а глибока рекурсія зупиняється на 44 KiB раніше, ніж до того.

Profile\start() нічого не робить, поки профайлер вимкнено — За вимкненого PROFILER_ENABLED виклик більше не змушує Profile\is_active() повертати true, тож тест, який запускає профіль і перевіряє is_active(), потребує увімкненого профайлера. З OTEL_APM_ENABLED спани APM, які цей виклик раніше відкидав, тепер зберігаються в трейсі.

UploadedFile::type() після moveTo() читає файл у місці призначення — Визначення типу більше не виконується до переміщення, тож для файлу, який до першого виклику type() перемістили далі, видалили чи перезаписали, повертається те, що там лежить зараз, або application/octet-stream із попередженням. Викликайте type() до moveTo(), якщо переміщений файл не лишається на місці або розташований за обгорткою потоку (stream wrapper).

ADDED

Правила заборони шляхів .oxphpdeny — Файл із синтаксисом .gitignore у корені DOCUMENT_ROOT не дає віддавати перелічені в ньому шляхи: у кожному режимі маршрутизації, однаково для статичних файлів і PHP, ще до будь-якого звернення до диска. Заборонені шляхи відповідають PHP_DENY_FALLBACK, а правило > передає запит вхідному скрипту. Файл читається один раз під час запуску; заборони рахує oxphp_path_deny_total.

RUNTIME_HOOKS=net — Встановлення зʼєднань через транспорти потоків PHP, рукостискання й читання TLS, записи, що чекають на місце в буфері надсилання, а також читання з потоків unix://, udp://, udg:// і socket-pair призупиняють файбер, а не тримають потік воркера. Потребує, щоб streams теж було в переліку, і працює лише на Linux. Не охоплено: libcurl, libpq, libmemcached, DNS і встановлення постійних зʼєднань.

Ключове слово cloudflare у TRUSTED_PROXIES — Ключові слова, CIDR і окремі IP-адреси тепер поєднуються в одному списку (private,100.64.0.0/10 раніше давав помилку під час запуску). cloudflare довіряє діапазонам edge-вузлів Cloudflare і читає адресу клієнта з CF-Connecting-IP, коли першим вузлом за вашими власними проксі є edge-вузол Cloudflare; private,cloudflare підходить для схеми Cloudflare → балансувальник навантаження → OxPHP.

User-Agent у трейсах і журналах доступу — Кореневий серверний спан містить user_agent.original, а кожен запис журналу доступу має поле user_agent; значення обрізаються до 512 байтів, а невалідний UTF-8 замінюється, тож пошукового робота можна відрізнити від браузера вже за самими трейсами.

Чому запит відкинуто — Рядки журналу про перевантаження тепер розділяють відмови на refused_queue_full, refused_wait_timeout, refused_waiting_full і refused_waiting_bytes, а кореневий спан відхиленого запиту містить oxphp.shed_reason, тож межу, яку слід підняти, видно й без слухача INTERNAL_ADDR.

Збої експорту OTLP у /metrics — oxphp_otel_export_failures_total, oxphp_otel_export_failed_spans_total і oxphp_otel_export_retries_total рахують невдалі експорти, спани, які вони несли, і повторні спроби після швидких збоїв. Невдалий експорт також журналюється як OTLP span export failed із розміром пакета та причиною.

Кількість CPU під час запуску — Рядок CPU count for default sizing повідомляє cpus, cpu_source (host, affinity, cgroup quota або unknown), host_cpus і отримані з них PHP_WORKERS та TOKIO_WORKERS. cpu_source=host на тарифі, що продається як менша кількість CPU, як це спостерігається на DigitalOcean App Platform під gVisor, означає, що обидві змінні слід задати явно.

Нічний образ на Debian (glibc) — :nightly-trixie, :php8.4-nightly-trixie і :php8.5-nightly-trixie запускають той самий сервер на php:<minor>-zts-trixie і на застосунку Yii3 витрачали на 14–19% менше CPU на відповідь, ніж Alpine. www-data там має UID 33, а розширення збираються на стадії trixie; релізи з тегами лишаються на Alpine.

PERFORMANCE

Дешевші виклики PHP-функцій — Спостерігача рушія, який для класифікації завислих запитів рахував кожен виклик, зокрема внутрішніх функцій, прибрано. На застосунку Yii3 витрати CPU на запит знижуються на 5% у класичному режимі та на 7% у режимі воркерів; скрипт, що робить 15 000 коротких викликів на запит, споживає на 23–25% менше.

FIXED

APM: hGet, hSet, lPush і rPush у phpredis — З увімкненим OTEL_APM_ENABLED ці чотири методи повертали NULL, не доходячи до Redis: записи губилися, а читання поверталися порожніми, без жодної помилки та без спану. Тепер хук зіставляє назви методів без урахування регістру, як і сам PHP.

PHP 8.5: memory_limit після помилки нестачі памʼяті — OOM на глибокому стеку в режимі воркерів чи в асинхронній задачі залишав воркер узагалі без ліміту, тож наступний некерований запит ріс, доки ядро не вбивало сервер разом з усіма його воркерами. Тепер ліміт відновлюється із завершенням запиту, а воркер, що досі його перевищує, перезапускається.

Переповнення стека викликів більше не спричиняє segfault — Рекурсія, що досягає межі стека PHP у режимі воркерів чи в асинхронній задачі, завершується звичайним Error, а не обвалює сервер; раніше для цього вистачало виклику realpath() на останньому дозволеному рівні. Тепер межа та сама, яку PHP встановлює для кожного файбера.

Режим воркерів: фатальні помилки всередині Fiber — Фатальна помилка у файбері, запущеному запитом, або в деструкторі, викликаному збирачем сміття, читала звільнені фрейми викликів і могла обвалити процес разом з усіма зʼєднаннями. Тепер запит завершується з 500, а воркер продовжує обслуговування.

Режим воркерів: тайм-аути завершують потрібний запит — Єдиний таймер на потік завершував той запит, що саме виконувався, а запит, для якого він призначався, зараховував як збій обробника, перезапускаючи воркер після трьох таких випадків. Тепер кожен запит має власний дедлайн; якщо він спливає, поки запит призупинено, той завершується з 504, що не враховується для перезапуску.

Режим воркерів: max_input_time після ініціалізації — За max_execution_time = 0 і заданого max_input_time таймер, запущений на час ініціалізації воркера, завершував запит через max_input_time секунд (60 за шаблонами php.ini, що постачаються з PHP). Тепер воркер зупиняє його перед початком обслуговування.

Асинхронні задачі та дедлайн потоку пулу — Єдиний таймер потоку пулу спрацьовував один раз, приблизно через max_execution_time після старту процесу, і завершував задачу, що саме виконувалася, з Maximum execution time of 0 seconds. Тепер потоки пулу не мають дедлайну, тож set_time_limit() більше не обмежує задачу: обмежуйте її тайм-аутом oxphp_async_await().

Постійні зʼєднання закриваються під час виходу воркера — Кожен перезапуск воркера в режимі воркерів і кожне зменшення динамічного пулу залишали зʼєднання PDO::ATTR_PERSISTENT, mysqli p: і pfsockopen() відкритими до перезапуску процесу, і вони накопичувалися, доки база даних не починала відмовляти новим клієнтам. Тепер вони закриваються після останнього запиту потоку.

Режим воркерів: памʼять, залишена скасованими внутрішніми викликами — Запит, завершений усередині внутрішньої функції (скажімо, usort() посеред компаратора), спричиняв витік виділеної цією функцією памʼяті на весь час життя воркера. Воркер, що за такий запит виріс більш ніж на 4 MiB, тепер перезапускається в найближчий момент простою з попередженням.

Фатальні помилки всередині колбеків Shared — Фатальна помилка в Mutex::withLock(), Once::getOrInit(), фабриці Registry чи Pool::with() залишала мʼютекс заблокованим, Once у стані очікування, ключ застряглим на етапі створення або пул з одним слотом менше на весь час життя процесу. Тепер усе це повертається із завершенням запиту.

RUNTIME_HOOKS=streams разом з APM — Обгортки APM для PDO, mysqli і Redis оминали захист зʼєднання, тож файбери, що ділять зʼєднання, могли читати відповіді Redis одне одного або отримувати Cannot execute queries while other unbuffered queries are active. Тепер обгортки викликають захищений обробник.

RUNTIME_HOOKS=streams: деструктори між запитами — Деструктор, запущений збиранням циклічних посилань у циклі воркера, міг вклинитися в обмін іншого запиту через спільне зʼєднання й прочитати його відповідь. Очікування на сокеті зʼєднання, власник якого сам призупинений на ньому, тепер одразу завершується невдачею, як за тайм-ауту, і нічого не надсилає.

Конструктори постійних PDO чекають у межах джерела даних — Конструктор, що застряг на базі даних, яка ніколи не відповідає, затримував усі інші постійні конструктори на воркері, доки їхнє очікування не спливало і вони не продовжували без серіалізації. Тепер вони чекають лише на конструктори до того самого DSN.

Звільнення дескриптора PDO на спільному постійному зʼєднанні — З RUNTIME_HOOKS=streams запит, що звільняв свій дескриптор, відкочував транзакцію іншого запиту, а з pdo_mysql міг звільнити результат посеред читання й спричинити segfault. Тепер це очищення виконує лише поточний власник зʼєднання, а PDO::rollBack() з іншого файбера нічого не надсилає.

Планові перезапуски не є збоями — Воркер, перезапущений через WORKER_MAX_MEMORY_MIB або Worker::scheduleExit(), журналюється на рівні INFO як Worker mode thread recycled із зазначенням причини, а не як WARN Dead workers detected, що виглядав як збій.

Експорт OTLP через http/protobuf — Через цей протокол не було експортовано жодного спану: потік експорту панікував, і кожен спан писав у журнал Spans are being emitted even after Shutdown. Тепер спани надходять на /v1/traces під OTEL_EXPORTER_OTLP_ENDPOINT, який є базовим URL; приберіть /v1/traces, якщо його вже дописано.

OTLP через gRPC: встановлення зʼєднань і мертві зʼєднання — OTEL_EXPORTER_OTLP_TIMEOUT тепер обмежує й встановлення зʼєднання, а на Linux зʼєднання, що перестало приймати дані, замінюється, а не зриває кожен експорт приблизно 15 хвилин. Експорт, що швидко зазнав невдачі на зʼєднанні, розірваному через простій, надсилається ще раз. Задану, але порожню OTEL_EXPORTER_OTLP_ENDPOINT тепер відхиляють під час запуску.

Класифікація oxphp_worker_stuck_total — Цикл PHP без жодних викликів визначався як c_call, а перше вимірювання завислого запиту могло порівнюватися з показником попереднього запиту. Тепер супервізор опитує сам рушій через те саме переривання, яким доставляється max_execution_time.

UploadedFile::moveTo() без fileinfo — Без mime_content_type() або з обробником помилок, що перетворює попередження на винятки, moveTo() викидав виняток ще до переміщення, і завантажений файл видалявся наприкінці запиту. Тепер він спершу переміщує файл, а визначення типу залишає type().

Винятки з деструкторів під час завершення асинхронної задачі — Виняток, викинутий деструктором обʼєкта, звільненого під час демонтажу задачі, зникав безслідно; тепер про нього повідомляється як про неперехоплений, і він потрапляє в журнал помилок.

index.json профайлера записує user_agent — Задокументоване поле завжди було null.

Документація: посібник з SSE — Приклад для режиму воркерів читав події через BRPOP, тож кожна подія доходила лише до одного клієнта; тепер він читає Redis Stream і продовжує з Last-Event-ID. Посібник більше не стверджує, що цикл while (!connection_aborted()) завершується через свою умову, а його приклад викликає ignore_user_abort(true) і set_time_limit(0).

SECURITY

Статичні файли: символьні посилання, перенаправлені після маршрутизації — Посилання всередині DOCUMENT_ROOT, перевірене під час одного запиту й згодом перенаправлене, скажімо, на /etc/passwd, під час наступного запиту сервер переходив за ним. Тепер файли перевіряються після відкриття за розташуванням, яке ядро повідомляє для дескриптора; файл, що досі є в кеші в памʼяті, зберігає свої перевірені байти.

PHP-скрипти: символьні посилання, перенаправлені після маршрутизації — Поза режимом воркерів таке посилання могло запустити скрипт зловмисника, вивести вміст текстового файлу або підвісити воркер на FIFO, щойно кеш realpath у PHP відпускав стару ціль. Тепер воркер сам відкриває скрипт, перевіряє дескриптор і передає PHP саме цей файл; підмінене посилання відповідає 404. Це коштує близько 3% пропускної здатності на тривіальному скрипті.

PHP 8.5: error_get_last() між запитами — У режимі воркерів і в асинхронних задачах запит, що зупинився в точці призупинення, міг прочитати трасування стека фатальної помилки іншого запиту разом з аргументами, з якими викликалися його функції: паролями, токенами, записами клієнтів. Тепер кожен запит зберігає власне трасування стека й звільняє його, коли завершується.

TRUSTED_PROXIES: записи пересилання, які неможливо розібрати — Правило «крайня права недовірена адреса» пропускало записи, яких не могло розібрати, і читало далі те, що записав сам клієнт, тож за шлюзом, що записує IP:port, клієнт обирав собі адресу сам. Тепер запис, який не вдається розібрати, або нечитабельний рядок заголовка зупиняє правило.

Розділені заголовки Cookie — Cookie, розділені на кілька рядків поля, як дозволяє HTTP/2, тепер обʼєднуються згідно з RFC 9113, перш ніж їх будь-що прочитає. Cookie плагіна __oxp_* у наступному рядку раніше доходила до застосунку, а вилучення такої cookie з першого рядка відкидало власні cookie застосунку в решті рядків.

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

SHARED_ENABLED=false тепер вимикає Shared\* — Раніше цей параметр розбирався й ігнорувався; тепер жоден клас OxPHP\Shared\* не реєструється, а перший же створений викидає Error: Class "OxPHP\Shared\Counter" not found. Параметр також читається як OX_SHARED_ENABLED і як просто ENABLED, тож перед оновленням перевірте всі три; class_exists() підкаже коду, у якому режимі він працює.

Режим воркерів: розрив зʼєднання клієнтом більше не завершує запит — Обробник виконується до кінця, тож його блоки finally й очищення спрацьовують так, ніби ignore_user_abort завжди увімкнено; межею слугує лише max_execution_time. Щоб зупинитися раніше, викличте connection_aborted(). Потокові відповіді, як і раніше, завершуються на наступному записі.

ignore_user_abort(true) виконується до кінця в кожному режимі — Як і в PHP-FPM, вивід відкидається, а connection_aborted() повідомляє про розрив; цикл потоку, який розраховував зупинитися на наступному записі, мусить перевіряти connection_aborted(). ignore_user_abort = On у php.ini чи в бутстрапі воркера тепер діє на кожен потік.

/health відповідає 503, коли пул втрачено або він завис — Тіло відповіді отримує поле pool_stalled і повідомляє "status": "degraded". Liveness-проба Kubernetes, спрямована на зведений /health, тепер перезапускатиме поди; використовуйте /health/liveness, /health/readiness і /health/startup.

LOG_LEVEL більше не переважає ACCESS_LOG — ACCESS_LOG=all разом із LOG_LEVEL=warn раніше не писав нічого; тепер він пише по рядку на кожен запит, тож заплануйте обсяг для журналів. RUST_LOG=warn,access_log=off і далі вимикає лише журнал доступу.

payload() повертає тіла форм і скаляри JSON — Тіла форм тепер розбираються й за SUPERGLOBALS_ENABLED=false, а JSON-тіло "hi", 42 або false повертає саме це значення замість null чи $default. Тепер null означає лише порожнє тіло, нерозібраний Content-Type, невалідний JSON або буквальний null.

Розриви зʼєднання клієнтом зараховуються як скасування — oxphp_request_cancelled_total{reason="client_abort"} починає зростати, тоді як admission_refused_total{reason="wait_timeout"} знижується, а рядок ERROR Connection error для клієнта, що відʼєднався, тепер має рівень debug; сповіщення на обидві метрики варто переглянути.

Запуски профілювання групуються за тригером і мають власний ідентифікатор — oxphp_profiler_runs_total{source} тепер розділяється на header, cookie, query, sample і новий sdk; ідентифікатори запусків мають вигляд <ts_ms>-<req_id_prefix>-<rand4>, тож /__profiler/runs/{id} відповідає 404 на ідентифікатор запиту — шукайте його за request_id у /__profiler/runs.

PROFILER_MAX_DEPTH застосовується — Раніше він не діяв ніде. Типове значення тепер 0 (без обмеження); значення понад 32 обрізаються з WARN під час запуску, а виклики, глибші за межу, до профілю не потрапляють.

COMPRESSION_LEVEL застарів — Використовуйте COMPRESSION_BROTLI_LEVEL; стара назва ще працює, але з WARN під час запуску. Ненульове значення більше не означає «лише Brotli», адже тепер пропонуються також Zstandard і gzip. /config повідомляє brotli_level, gzip_level і zstd_level замість compression_level.

SHARED_SOFT_LIMIT_RATIO і SHARED_POISON_STRICT ігноруються — Жоден із них ніколи не зчитувався; тепер обидва дають WARN під час запуску, а limits.soft_ratio і diagnostics.poison_strict зникли з /__ox_shared/summary. Налаштуйте сповіщення на oxphp_shared_capacity_saturation; для термінального Once використовуйте Once\FailureMode::Poison.

oxphp_queue_wait_us показує менші значення — Відлік тепер зупиняється, щойно воркер бере запит, — приблизно на 0,1 мс менше на запит; субмілісекундні пороги сповіщень спрацьовуватимуть рідше. oxphp_request_duration_us не змінився.

ADDED

Стиснення Zstandard — Динамічні відповіді надсилаються у zstd (рівень 6) клієнтам, які його приймають: компактніше за Brotli менш ніж за половину процесорного часу. COMPRESSION_ENCODINGS (типово br,zstd,gzip або off) задає кодування, разом із COMPRESSION_BROTLI_LEVEL (5), COMPRESSION_ZSTD_LEVEL (6) і COMPRESSION_GZIP_LEVEL (6).

gzip для клієнтів без Brotli — Chromium через звичайний HTTP і більшість CLI-інструментів раніше отримували нестиснені тіла. Тепер Accept-Encoding ранжується за q-значенням згідно з RFC 9110, а q=0 сприймається як відмова; як наслідок, стисливі файли до 3 MiB відповідають на Range-запити повною відповіддю 200.

Кеш попередньо стиснених статичних файлів — Для кешованого статичного файлу у фоні один раз створюється копія Brotli максимальної якості, на 8–12% менша за вивід, стиснений під час запиту, у межах того самого бюджету кешу.

Видимість зависання пулу — oxphp_pool_stalled, oxphp_queue_depth, oxphp_queue_capacity і oxphp_admission_slots_available показують пул, що перестав забирати роботу з черги, з WARN, поки це триває, та INFO, щойно робота знову рушить (режим воркерів).

Перевантаження в журналі — Запити, відкинуті з кодом 529, тепер дають WARN на кожен епізод, що повторюється щонайбільше раз на хвилину, і INFO з підсумком, коли хвилина минає без відмов.

Повідомлення про вичерпання MAX_CONNECTIONS — Призупинений цикл accept пише WARN із назвою MAX_CONNECTIONS і INFO, коли відновлюється; це експортують oxphp_accept_stalled і oxphp_accept_stalls_total.

oxphp_admission_wait_wasted_total — Рахує відмови wait_timeout, за час яких жоден воркер не почав жодного запиту, — ознака того, що QUEUE_WAIT_TIMEOUT_MS задовгий або застосунок звертається до власного сервера.

oxphp_worker_request_fibers_active — Файбери запитів на воркер: 256 означає, що воркер перестав брати роботу, а лічильник, що не повертається до 0 у простої, свідчить про витік файберів (режим воркерів).

RUNTIME_HOOKS видно — /config і oxphp_server_info() повідомляють runtime_hooks як розібраний список встановлених категорій, наприклад ["sleep","streams"], тож змінна з помилкою в назві читається як [].

CHANGED

Адаптивний бюджет очікування в черзі — QUEUE_WAIT_TIMEOUT_MS тепер є стелею: сервер удвічі скорочує очікування, поки чверть або більше розпочатої роботи припадає на клієнтів, які вже пішли (нижня межа — 10 мс або 1/64 стелі), і відновлює його, щойно черга спорожніє. Це показують oxphp_admission_wait_budget_us, oxphp_admission_wait_budget_ceiling_us і oxphp_abandoned_work_total.

Якість Brotli для запитів — 5 — Замість 4, що давала більший вивід, ніж gzip, ціною більшого процесорного часу; тіла Brotli понад 4 KB тепер стискаються в блокувальному пулі. COMPRESSION_BROTLI_LEVEL=4 повертає стару поведінку.

Рівні журналювання завершення зʼєднання за типом помилки — Закриття зʼєднання клієнтом має рівень debug, тайм-аут, накладений сервером, лишається warn, а все інше — error, замість того щоб рівень визначало слово «timeout» у повідомленні.

FIXED

Режим воркерів: сесії протікали між запитами — Запит, прийнятий, поки інший був призупинений, починався всередині сесії попередника, а після session_write_close() міг перейняти її ідентифікатор і отримати його в Set-Cookie. Тепер перед кожним запитом сесія записується й закривається, а $_SESSION видаляється.

Постійні зʼєднання відповідали на чужий запит — Поза режимом воркерів розрив клієнтом міг завершити запит між надсиланням команди через pfsockopen() чи постійний Predis і читанням відповіді, розсинхронізуючи зʼєднання до перезапуску. Тепер на відхід клієнта реагують лише на наступному записі, як у PHP-FPM.

Режим воркерів: сторінки змінювали склад після переривань — Перервані запити пропускали блоки finally, а коли переривання спрацьовувало на межі опкоду, вимикали всі деструктори на воркері, тож захист від рекурсії лишався встановленим і сторінки непомітно втрачали блоки. Тепер запити виконуються до кінця.

Режим воркерів: значення ini для окремого запиту — Зміни через ini_set(), ignore_user_abort() чи error_reporting() більше не протікають до паралельних запитів і не переживають зайнятий воркер; вони супроводжують призупинений запит і відновлюються, коли він завершується. Запит також більше не починається з рівнем error_reporting, який залишив попередній запит на його файбері.

Постійний PDO за RUNTIME_HOOKS=streams — Конструктори PDO::ATTR_PERSISTENT, що перекриваються, більше не закривають зʼєднання під запитом, який ним користується (фатальна помилка або segfault), а запит під час liveness-пінгу іншого запиту більше не вбиває зʼєднання з 2006 MySQL server has gone away.

Режим воркерів: фатальні помилки більше не зациклюють воркер назавжди — З увімкненим профайлером або #[Trace] дві фатальні помилки на одному воркері могли зациклити ланцюжок спостерігачів на 100% CPU, тоді як пул виглядав незайнятим. Фатальна помилка, що виникла під час відновлення після іншої, також більше не скидає всі запити на воркері.

Режим воркерів: неперехоплені винятки з __destruct — Клас винятку з деструктором перетворював 500 на фатальну помилку, що враховувалася для перезапуску (31 запит вивів з обігу 8 воркерів), вимикав усі деструктори на воркері й спричиняв витік винятку.

Режим воркерів: розриви зʼєднання клієнтом більше не перезапускають воркери — Переривання після ignore_user_abort() або під час підхоплення запиту сприймалися як фатальні помилки й обертали пул по три воркери за раз. Кожен запит, який рахує запобіжник, тепер пише WARN, і вихід через запобіжник теж журналюється на рівні WARN.

Режим воркерів: потоки, завершені записом — Такий потік більше не спричиняє витік змінних своїх фреймів на весь час життя воркера, а фатальна помилка в деструкторі під час цього очищення тепер враховується запобіжником помилок.

QUEUE_WAIT_TIMEOUT_MS обмежує запити в черзі — Бюджет перевірявся лише тоді, коли воркер брав запит, тож завислий пул ніколи не відповідав на запити в черзі; тепер після дедлайну вони отримують 529 із Retry-After: 3.

Клієнти, що пішли, більше не стоять у черзі й не отримують відмов — Режим воркерів більше не виконує запит, клієнт якого пішов, поки той чекав у черзі, а бюджет, що вичерпався після відходу клієнта, дає відповідь 499 як скасування, а не 529. client_abort тепер рахує клієнтів, які пішли до того, як їх підхопив воркер.

/health/readiness відображає стан пулу — Перевірка виконавця завжди проходила; пул без живих потоків воркерів або завислий на хвилину в режимі воркерів тепер відповідає 503. Перевантаження, як і раніше, до цього не призводить.

oxphp_apm_trace() викликає свій колбек — Функція повертала null, не викликаючи його. Тепер колбек виконується в дочірньому спані, отримує ідентифікатор спану й повертає своє значення, а тип повернення тепер mixed.

Втрачені повідомлення класів Error — TypeError, ValueError та решта ієрархії Error потрапляли в спани й причини отруєння Once без свого повідомлення та коду.

Типи повернення mixed під JIT — Функції на кшталт oxphp_async_await() і Shared\Map::get() оголошували некоректний тип mixed, тож гарячі цикли під трасувальним JIT спричиняли витік кожного повернутого значення, а getReturnType() з Reflection падав із segfault.

oxphp_worker_memory_bytes показує памʼять — Метрика завжди показувала 0; тепер вона повідомляє ту саму купу PHP, з якою звіряється WORKER_MAX_MEMORY_MIB, заміряну наприкінці кожного запиту.

Режим воркерів: профілі змішувалися між запитами — Призупинені запити на одному потоці ділили стан профайлера й спанів APM, через що профілі та спани #[Trace]/#[SlowThreshold] приписувалися не тому запиту; тепер кожен запит має власний стан.

Обрізання профілів і OXPROF — Запуски, що досягли PROFILER_MAX_SPANS, позначаються як truncated в index.json, /__profiler/stats і oxphp_profiler_truncated_total, а задокументований cookie OXPROF тепер справді запускає профілювання.

Очікування в Shared\Mutex::withLock() можна перервати — Запит, що чекає на блокування, тепер кожні 100 мс поступається max_execution_time, зупинці сервера та скасуванню задачі, тож блокування у зворотному порядку більше не тримає два воркери до перезапуску.

RUNTIME_HOOKS=streams і пізно завантажені розширення — mysqli і phpredis, увімкнені з ini-файлу, що за сортуванням іде після extension.ini, тепер захищені так само, як і решта; RUNTIME_HOOKS також читається один раз під час запуску, а не через незаблокований getenv() на кожен запит.

Граничні випадки стиснення — Статичні файли від 1 MiB до 3 MiB стискаються; значення Content-Type у змішаному регістрі стискаються; identity-копії стисливих відповідей мають Vary: Accept-Encoding; а налаштування стиснення не в UTF-8 спричиняють помилку під час запуску.

Помилки місткості Shared\Counter, Atomic і Flag — Їхні конструктори тепер викидають Shared\CapacityException понад SHARED_MAX_ENTRIES або SHARED_MAX_BYTES, як і всі інші типи Shared.

Виправлення документації — Значення SHARED_LOCK_DIAGNOSTICS — off, warn (типове) і strict; перезапуск воркерів за кількістю запитів більше не згадується як можливість; цикл воркера також завершується, коли динамічний пул виводить воркер з обігу; 504 походить від max_execution_time; а також межі плавної зупинки, структура ini-файлів образу середовища виконання та порядок ключів /config.

SECURITY

rustls 0.23.45 — Виправляє ваду рукостискання TLS 1.3 (RUSTSEC-2026-0285), яка дозволяла приймати незашифровані повідомлення рукостискання через межу шифрування; це стосувалося TLS-слухача, а також експортерів OTLP і профайлера. Транскрипт при цьому лишається автентифікованим.

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

FRAME_OPTIONS типово SAMEORIGIN — Вбудовування з інших джерел лишається заблокованим, а вбудовування в межах того самого джерела (попередній перегляд в адмінці, віджети дашбордів) працює одразу; розгортання, що покладалися на вбудований DENY, тепер мають задавати його явно.

Режим воркерів: жодного циклу подій усередині запиту — Запит у режимі воркерів тепер є файбером, а Revolt відмовляється запускати в ньому свій цикл: Revolt\EventLoop::run() в обробнику викидає виняток. Перенесіть його в бутстрап воркера; defer(), delay() і getSuspension() працюють і далі.

Режим воркерів: стан запиту завершується разом із запитом — ini_set(), set_time_limit() і error_reporting() між запитами повертаються до базових значень бутстрапу; filter_input() відповідає лише для запиту, що питає; дескриптор php://input, збережений довше за свій запит, читається як закритий ресурс, а не як тіло іншого запиту.

query() і cookie() декодують свої значення — Код, що компенсував це через rawurldecode(), тепер декодує двічі й мусить прибрати зайвий виклик. Cookie, надіслане порожнім, читається як порожній рядок, а не null, а query() розбирає не більше 1000 параметрів.

Суворий розбір MAX_CONNECTIONS / QUEUE_CAPACITY — Некоректні значення переривають запуск замість тихого повернення до типових, а QUEUE_CAPACITY=0 тепер означає «авто» (воркери × 128), а не чергу нульової місткості.

ADDED

Бюджет очікування в черзі — Запит, що надходить до заповненої черги, тепер чекає на слот до QUEUE_WAIT_TIMEOUT_MS (типово 1 с) замість миттєвого 529; один дедлайн, проставлений під час надходження, охоплює й очікування слота, й очікування в черзі. QUEUE_MAX_WAITING і QUEUE_MAX_WAITING_BYTES обмежують множину очікувальних у запитах і в байтах тіл.

RUNTIME_HOOKS=streams — Блокувальне читання сокетів tcp:// і stream_select() призупиняють файбер, а не потік воркера, що охоплює mysqlnd (PDO_MySQL, mysqli), phpredis і HTTP-обгортки потоків без жодних змін у коді. Зʼєднання, спільні для паралельних файберів, займаються на час обміну, тож запити не можуть читати відповіді одне одного.

oxphp_admission_refused_total — Запити, відкинуті до того, як дійшли до воркера, тепер лишають слід на боці сервера, а мітка reason відокремлює чотири причини перевантаження (529) від shutting_down (503) і pool_unavailable (500).

Автоматичне фіксування винятків у кореневому спані — Необроблений виняток або фатальна помилка, що дає відповідь 5xx, додає подію exception (type, message, stacktrace, file, line) до кореневого спану запиту в усіх чотирьох режимах обслуговування — без атрибута й без виклику oxphp_apm_error().

Атрибути спанів бази даних — Спани PDO і mysqli містять db.statement (буквальні значення замасковано як ?), db.operation, db.system, server.address/port, db.name і oxphp.db.slow; mysqli_stmt::prepare долучається до перехоплюваного набору як функція номер 34.

CHANGED

oxphp_queue_wait_us сягає однієї секунди — Чотири нові бакети (від 100 мс до 1 с) дають кількісну оцінку очікуванням поблизу типового бюджету замість того, щоб зсипати їх у +Inf; відхилені запити до гістограми не потрапляють.

Перезапуск воркерів враховує те, що ламає воркер — Лише фатальні помилки, нестача памʼяті й переповнення стека зараховуються до перезапуску після трьох послідовних збоїв; неперехоплені винятки та скасовані запити нейтральні. Запобіжник тепер спрацьовує й на синхронному шляху диспетчеризації, де раніше не спрацьовував ніколи.

Попередження config --check щодо розмірів — Попереджає, не спричиняючи помилки, коли PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING досягає MAX_CONNECTIONS — межі, за якою перевантаження залишається без жодної відповіді замість 529.

Документацію сплощено — Документація в репозиторії переїхала з docs/en/ до docs/; російський і китайський переклади переїхали на oxphp.dev, де вже є французька, польська та японська версії.

FIXED

Режим воркерів: $_POST і $_FILES заповнюються — Обидва були порожніми для кожного запиту, який будь-коли обслуговував воркер, тож HTML-форми й завантаження файлів мовчки не працювали; JSON API, що читають php://input, це не зачіпало, тому проблема й лишалася прихованою.

Міжзапитовий витік filter_input() — Власна копія вхідних даних розширення filter накопичувалася впродовж усього життя воркера, тож на один запит відповідала значеннями query, сесійними cookie й полями тіла іншого клієнта; тепер вона повертається на початку кожного запиту.

Сплески таймерів сну — Якщо за одне опитування планувальника спливало понад 32 таймери, зайві сплячі файбери лишалися припаркованими назавжди; тепер великий сплеск пробуджується порціями по 32.

Закриття спільного зʼєднання — fclose(), $mysqli->close() або допоміжна функція перепідключення на зʼєднанні, з якого читав припаркований інший запит, відновлювали той запит у звільненій памʼяті; тепер він одразу завершується помилкою 500 із поясненням, що сталося.

Фатальні помилки більше не стирають спільний стан — Розгортання стеку перерваного запиту двічі звільняло змінні include, мовчки скидаючи масиви й обʼєкти, у яких застосунки в режимі воркерів зберігають свій міжзапитовий стан.

Темп асинхронного пулу — Передавання задач більше не підлаштовується під фіксований таймер простою в 1 мс: передавання через Shared\Channel скоротилося з 0,69 мс до 0,14 мс на елемент, а пробудження в простої — з 777/с до 92/с.

Призупинені запити зберігають час початку — $request->startTime() і oxphp_server_info()[request_time] після призупинення більше не показують 0 чи значення паралельного запиту.

Попередження під час читання тіла потрапляють до потрібного запиту — Попередження post_max_size і лімітів завантаження тепер виникають усередині запиту, що надіслав тіло, тож обробники помилок бачать його суперглобальні змінні, а обробник, що викидає виняток, дає відповідь 500 замість закрити зʼєднання ні з чим.

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

Типове зниження привілеїв — Запущений від root там, де існує обліковий запис www-data, сервер привʼязує свої слухачі та знижує привілеї до www-data ще до обслуговування будь-якого трафіку; офіційний образ більше не працює від root. Передайте --user=root, щоб зберегти стару поведінку.

Безпечна відмова для конфігурації TLS — Напівналаштована пара (TLS_CERT без TLS_KEY чи навпаки) або значення сертифіката/ключа не в UTF-8 тепер переривають запуск із назвою проблемної змінної замість того, щоб мовчки обслуговувати звичайний HTTP.

Суворий розбір ASYNC_* / SUPERGLOBALS — Сміття в ASYNC_WORKERS, ASYNC_QUEUE_CAPACITY або ASYNC_MAX_FIBERS — це помилка запуску, а не мовчазне типове значення, і oxphp run більше не ігнорує невалідний SUPERGLOBALS_ENABLED.

QUERY без Content-Type повертає 400, а не 415 — Єдиний 415, який генерував сервер, зник; власний ERROR_PAGES_DIR/415.html для цього випадку слід перейменувати на 400.html.

Вибір обгортки експорту профайлера — PROFILER_EXPORT_XHGUI=false тепер має пріоритет навіть на URL /run/import, а автовизначення обгортки орієнтується лише на канонічний шлях кожного інструмента (xhgui /run/import, Buggregator /api/profiler/store), а не на нечіткий підрядок "xhgui".

ADDED

Експорт профайлера в Buggregator — HTTP-надсилання профайлера обгортає дані xhprof в обгортку, якої очікує POST /api/profiler/store у Buggregator; PROFILER_EXPORT_BUGGREGATOR вмикає це, а tags і app_name задаються через змінні середовища.

Експорт трас OTLP через TLS — OTEL_EXPORTER_OTLP_ENDPOINT зі схемою https:// тепер експортується зашифровано і через gRPC, і через http/protobuf, з перевіркою за системним сховищем довіри через rustls; образ середовища виконання мусить містити набір CA (офіційний містить).

Повні дані винятку в спанах APM — Подія спану exception тепер містить exception.message і exception.stacktrace поряд з exception.type, тож New Relic Errors Inbox, Jaeger і Tempo показують повідомлення та стек; обсяг обмежують OTEL_APM_MESSAGE_MAX_BYTES / OTEL_APM_STACKTRACE_MAX_BYTES.

Плавне завершення довготривалих зʼєднань — За SIGTERM сервер надсилає GOAWAY, закриває неактивні keep-alive-зʼєднання й оперативно завершує відкриті потоки SSE/flush; типове значення DRAIN_TIMEOUT_SECONDS знижено з 30 до 25, щоб вкластися в 30-секундний період очікування Kubernetes.

TLS_MIN_VERSION — Мінімальна прийнятна версія протоколу: 1.2 (типово) або 1.3. Будь-що інше — зокрема чужий синтаксис на кшталт "TLSv1.2" — є фатальною помилкою запуску; значення перевіряється, навіть коли TLS вимкнено, і відображається як tls_min_version у /config.

CHANGED

Дешевий STATIC_REVALIDATE — stat() щонайбільше раз на 3 секунди для кожного файлу; статичний файл із кешу віддається з памʼяті щонайбільше з одним системним викликом, а зміни на диску стають видимими протягом 3 с. Типово вимкнено, як і раніше.

Зрозуміліші помилки TLS під час запуску — Шлях з одруківкою дає помилку "TLS_CERT: cannot read /etc/ssl/cert.pem: No such file or directory" замість голої помилки ОС; помилки розбору PEM і невідповідності ключа отримують такий самий префікс.

FIXED

Цільове скасування в режимі воркерів — Розрив клієнтом або тайм-аут запиту міг скасувати запит, прийнятий на воркері останнім, замість того, який справді скасовувався, перериваючи сторонній запит, що саме виконувався.

finish_request() і мультиплексовані потоки — Щойно будь-який запит на воркері викликав oxphp_finish_request(), усі інші потоки, мультиплексовані на цьому воркері, мовчки відкидали свій подальший вивід oxphp_stream_flush().

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

Await із тайм-аутом скасовує задачу — Покинута задача примусово скасовується: припарковані задачі відновлюються в стан скасування, а ті, що навантажують CPU, перериваються на межі опкоду. Раніше вона виконувалася до кінця запиту.

ADDED

Вкладений oxphp_async() — Асинхронна задача може запускати власні задачі й призупинятися на await_all / await_any / await_race, не блокуючи потік свого воркера. Обмеження «жодного вкладеного async» знято.

Обмеження кількості активних задач — ASYNC_MAX_FIBERS (типово 256) обмежує задачі в черзі та ті, що виконуються; запуск понад ліміт одразу відхиляється з AsyncException, тож розгалуження не може спричинити взаємне блокування.

Список дозволених адрес внутрішнього сервера — INTERNAL_ALLOW_IPS: список дозволених CIDR для /metrics і /config; ендпоінти перевірки стану лишаються доступними для оркестраторів.

Винятки з вибіркового профілювання — PROFILER_EXCLUDE_PATHS виключає власний трафік фреймворку (/_profiler, /_wdt) зі статистичної вибірки; явні тригери все одно профілюють.

FIXED

Пробудження відправника каналу — Файбер, що чекає на відправлення на заповненому Shared\Channel, прокидається, коли recv_blocking звільняє слот, — sendTimeout більше не спрацьовує хибно під час насичення.

Граничні випадки async — Збій під час плавної зупинки; await на закритому промісі зависав замість відхилення; await_all залишав проміси виконуватися після дострокового виходу.

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

Контракт маршрутизації воркера — Статичні ресурси — з диска, усе інше — до ENTRY_FILE воркера; довільні .php-файли, індекси каталогів і резервний кореневий index.php більше не визначаються для кожного запиту.

Чесний CGI PATH_INFO — У режимі Framework PATH_INFO задається лише для явного запиту /index.php/extra; PHP_SELF вказує на фронт-контролер. Маршрутизуйте за REQUEST_URI.

ADDED

Range-запити (RFC 9110) — 206 Partial Content для статичних файлів: перемотування відео, докачування завантажень, часткове завантаження PDF; If-Range враховується для сильних валідаторів.

Обмеження зʼєднань HTTP/2 — Пʼять змінних середовища обмежують використання ресурсів на одне зʼєднання; остаточні значення журналюються під час запуску.

CHANGED

Сильні статичні ETag — "<size>-<mtime>" замінює W/"…" — цього вимагає If-Range; стиснені представлення зберігають слабкий тег.

FIXED

Заголовки застосунку мають пріоритет — X-Content-Type-Options, X-Frame-Options і CSP, задані з PHP, більше не перезаписуються — значення сервера лише доповнюють відсутні.

Потокова передача і стиснення — Відповіді, що передаються потоком через flush(), проходять нестисненими, а не буферизуються повністю шаром brotli до кінця скрипту.

SECURITY

Захист HTTP/2 від DoS — Rapid Reset (CVE-2023-44487), HPACK-бомби й атаки із затримкою вікна обмежено; мертві зʼєднання звільняються через keepalive PING/PONG.

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

remote_ip у журналах доступу — JSON-поле remote_addr перейменовано на remote_ip, і воно містить IP без порту — як у nginx/Apache/Caddy.

ADDED

CLI у стилі PHP — oxphp run script.php виконує файл із семантикою CLI на повноцінному рушії; скрипти з #!/usr/bin/env oxphp запускаються напряму; підтримуються перевизначення ini через -d.

Зниження привілеїв через --user — Привʼяжіть привілейовані порти від root, а потім незворотно перейдіть до непривілейованого користувача ще до створення будь-якого потоку обробки запитів.

Урахування переданого порту — За TRUSTED_PROXIES SERVER_PORT враховує X-Forwarded-Port; REMOTE_PORT заповнюється з for= за RFC 7239, якщо проксі його надає.

FIXED

Збої декораторів — Два SIGSEGV у шляху декораторів функцій (звільнений кеш екземплярів; висячий zval після зміни розміру) усунуто; переповнення вкладеності тепер викидає StackOverflowException.

Атрибути профайлера враховують аргументи — #[SlowThreshold(ms:)], #[MemoryThreshold(kb:)] і #[Mark(label:)] більше не зводяться до спільних типових значень; події спанів APM доходять до OpenTelemetry.

Обʼєктний API завантажень підключено — Request::file() / files() повертають обʼєкти UploadedFile для будь-якої форми $_FILES замість null.

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

Трихотомний API очікування — Channel і Mutex розділяють кожне очікування на try* / дієслово без префікса / *Timeout(int $ms), повертаючи типізовані значення SendResult / RecvResult замість mixed|null.

await_any означає Promise.any — Перемагає перший виконаний; якщо всі відхилено, викидається AggregateAsyncException. Стара поведінка «перший завершений» тепер — oxphp_async_await_race().

Чесні статуси скасування — Тайм-аут → 504, плавне завершення → 503 + Retry-After, відʼєднання клієнта → 499 (лише в журналі). Примусові завершення супервізором і скасування з коду користувача зберігають 500.

Перейменування без сумісних обгорток — Async\Exception → AsyncException, Shared\Exception → SharedException; Worker позбувається префіксів get*; Counter стає мінімальним (add/set); Flag повторює Atomic; Once отримує getOrInit()/status().

ADDED

Shared\Atomic + Ordering — Узагальнений атомарний int64: load/store/swap/CAS/fetch* з явними впорядкуваннями памʼяті, один до одного з std::sync::atomic.

Shared\Registry — Глобальні для процесу дескриптори з ключем-іменем: усі воркери сходяться на тому самому записі; фабрика виконується щонайбільше один раз.

REMOVED

Одне джерело тайм-аутів — REQUEST_TIMEOUT_SECONDS і oxphp_request_heartbeat() прибрано — лишаються max_execution_time і set_time_limit().

FIXED

connection_aborted() у SSE — Повертає true після розриву посеред потоку, тож цикли while (!connection_aborted()) коректно завершуються.

PERFORMANCE

Гарячий шлях Shared\* — Накладні витрати на операцію в межах шуму criterion порівняно з простим атомарним load; ≈4,7× за середнім геометричним на 8 потоках порівняно з попередньою реалізацією.

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

Суворий розбір булевих змінних середовища — Лише on/true/1/yes та off/false/0/no; одруківка на кшталт ture не дає запуститися замість мовчазного типового значення.

ADDED

ENTRY_FILE + WORKER_MODE_ENABLED — Одна змінна вибирає режим маршрутизації за розширенням; явний перемикач вмикає постійні воркери. INDEX_FILE / WORKER_FILE стають застарілими псевдонімами.

Клас OxPHP\Server\Worker — Дескриптор для інтроспекції під час виконання: id, кількість запитів, памʼять, RSS — а також керований застосунком перезапуск через scheduleExit().

CHANGED

Перейменування статичного кешу — STATIC_CACHE_TTL → STATIC_MAX_AGE; STATIC_CACHE → STATIC_REVALIDATE з оберненою полярністю. Типові значення не змінилися.

DEPRECATED

WORKER_MAX_REQUESTS — Розбирається та ігнорується з WARN під час запуску — перейдіть на WORKER_MAX_MEMORY_MIB або scheduleExit().

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

Образи PHP 8.5 — :php8.5, :php8.5-alpine і теги з фіксованим патчем постачаються поряд з 8.4; latest лишається на 8.4, доки не завершиться період обкатки.

FIXED

Chunked-передача між запитами — Контекст запиту в мості протікав між запитами воркера, через що chunked Transfer-Encoding зникав після першого flush.

Shared\* усередині користувацьких файберів — Channel::recv()/send() усередині Fiber::start() більше не йдуть шляхом призупинення oxphp-fiber і не падають із fiber_await rc=1.

Іменовані аргументи для API плагінів — Класи й функції, визначені плагінами, тепер оголошують справжні імена параметрів — виклики з іменованими аргументами працюють.

Спільні дескриптори в асинхронних замиканнях — Channel/Map/Pool, захоплені замиканням oxphp_async(), більше не надходять як null у потік-отримувач.

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

Простір імен Async — Асинхронні класи переїхали до OxPHP\Async\; функції тепер постачаються в plugin-async.

ADDED

Сім примітивів Shared\* — Counter, Flag, Once, Mutex, Channel, Map, Pool — атомарний стан, спільний для воркерів, зі спостережуваністю реєстру та детектором взаємних блокувань.

Профайлер для кожного запиту — Вивід у форматах xhprof / speedscope / pprof / collapsed; тригери через cookie, заголовок, query та статистичну вибірку; HTTP-надсилання з обгорткою xhgui.

Автоінструментування APM — plugin-apm із PHP SDK для трасування та фіксуванням помилок; підсистеми async і APM переїхали в плагіни на Rust.

Інтеграції для продакшну — TRUSTED_PROXIES (RFC 7239), Kubernetes /readyz + /livez, заголовки безпеки, обмеження частоти запитів для кожної IP-адреси — усе налаштовується змінними середовища.

PERFORMANCE

Гарячий шлях Pool — Неконкурентні acquire/release ≈0,9 мкс у Docker, з привʼязкою до потоків.

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

Мультиплексування файберів — Паралельний ввід-вивід у межах одного потоку воркера; розподілене трасування з W3C Trace Context та експортом в OpenTelemetry.

Проміси oxphp_async() — Паралельне виконання PHP в окремому пулі потоків.

Обʼєктний HTTP API — OxPHP\Http\Request з лінивими методами доступу, а також система декораторів на основі атрибутів.

CHANGED

Порт і зворотний тиск — Типовий порт 8080 → 80 (443 з TLS); відповідь зворотного тиску 503 → 529.

FIXED

Коректність CGI — SERVER_PROTOCOL відображає справжню версію HTTP; REQUEST_TIME — це час початку запиту; розбір Host з IPv6; 408 за тайм-аутом запиту.

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

Асинхронне ядро HTTP — Сервер на Hyper + Tokio з плавним завершенням роботи, власний SAPI oxphp з повним набором суперглобальних змінних, пул воркерів ZTS з обмеженою чергою та зворотним тиском 529.

Три режими маршрутизації — Traditional, Framework і SPA; статичні файли з кешем у памʼяті, ETag/304, brotli, TLS.

Режим воркерів — Постійні воркери через oxphp_worker(), дострокове завершення відповіді, потокова передача SSE, кооперативні тайм-аути.

Спостережуваність — /metrics, /health, /config, структуровані JSON-журнали доступу, ідентифікатори запитів.

Docker насамперед — Багатоетапна збірка на Alpine; образи amd64/arm64 у GHCR.