Releases

Changelog

Every notable change to the server, the PHP API and the images — curated highlights per release. The full, unabridged history lives in CHANGELOG.md.

latest v0.11.0 · 2026-08-20 · 11 releases · AGPL-3.0 CHANGELOG.md →
oxphp — releases bash
$ docker pull ghcr.io/oxphp/oxphp:0.11.0
0.11.0: Pulling from oxphp/oxphp
Status: Image is up to date
# opt-in PHP 8.5 preview:
$ docker pull ghcr.io/oxphp/oxphp:php8.5
v0.11.0
2026-08-20
LATEST
⚠ breaking changes
Release notes →
BREAKING

FRAME_OPTIONS defaults to SAMEORIGIN — Cross-origin framing stays blocked and same-origin embedding (admin previews, dashboard widgets) works out of the box; deployments that relied on the built-in DENY must now set it explicitly.

Worker mode: no event loop inside a request — A worker-mode request is now a fiber, and Revolt refuses to run its loop within one: Revolt\EventLoop::run() in a handler throws. Move it to the worker bootstrap; defer(), delay() and getSuspension() keep working.

Worker mode: request state ends with the request — ini_set(), set_time_limit() and error_reporting() roll back to the bootstrap baseline between requests; filter_input() answers only for the request asking; a php://input handle kept past its request reads as a closed resource instead of another request’s body.

query() and cookie() decode their values — Code that compensated with rawurldecode() now decodes twice and must drop the extra call. A cookie sent empty reads as an empty string rather than null, and query() parses at most 1000 parameters.

Strict MAX_CONNECTIONS / QUEUE_CAPACITY parsing — Malformed values abort startup instead of silently falling back to defaults, and QUEUE_CAPACITY=0 now means auto (workers × 128) instead of a zero-capacity queue.

ADDED

Queue wait budget — A request arriving at a full queue now waits for a slot up to QUEUE_WAIT_TIMEOUT_MS (default 1 s) instead of an instant 529; one deadline stamped on arrival covers the wait for a slot and in the queue. QUEUE_MAX_WAITING and QUEUE_MAX_WAITING_BYTES bound the waiting set in requests and in body bytes.

RUNTIME_HOOKS=streams — Blocking tcp:// socket reads and stream_select() suspend the fiber instead of the worker thread, covering mysqlnd (PDO_MySQL, mysqli), phpredis and the HTTP stream wrappers with no code changes. Connections shared between concurrent fibers are claimed per exchange, so requests cannot read each other’s replies.

oxphp_admission_refused_total — Requests shed before reaching a worker now leave a server-side trace, with a reason label separating the four overload reasons (529) from shutting_down (503) and pool_unavailable (500).

Automatic root-span exception capture — An unhandled exception or fatal that answers 5xx attaches an exception event (type, message, stacktrace, file, line) to the request’s root span in all four serving modes — no attribute and no oxphp_apm_error() call required.

Database span attributes — PDO and mysqli spans carry db.statement (literal values obfuscated to ?), db.operation, db.system, server.address/port, db.name and oxphp.db.slow; mysqli_stmt::prepare joins the hooked set as function number 34.

CHANGED

oxphp_queue_wait_us reaches one second — Four new buckets (100 ms to 1 s) quantify waits near the default budget instead of lumping them into +Inf; refused requests are excluded from the histogram.

Worker recycling counts what wrecks the worker — Only fatals, out-of-memory and stack overflows count toward the three-consecutive-failures recycle; uncaught exceptions and cancelled requests are neutral. The breaker now trips on the synchronous dispatch path too, where it never fired.

config --check sizing advisory — Warns, without failing, when PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING reaches MAX_CONNECTIONS, the point past which overload answers nothing instead of 529.

Documentation flattened — The repository docs moved from docs/en/ up to docs/; the Russian and Chinese translations moved to oxphp.dev, where French, Polish and Japanese already live.

FIXED

Worker mode: $_POST and $_FILES populated — Both were empty for every request a worker ever served, so HTML forms and file uploads silently did not work; JSON APIs reading php://input were unaffected, which is why it stayed hidden.

filter_input() cross-request leak — The filter extension’s own input copy accumulated for the life of the worker, answering one request with another client’s query values, session cookies and body fields; it is now given back at the start of every request.

Sleep timer bursts — More than 32 timers expiring in one scheduler poll left the extra sleepers parked forever; a large burst now wakes in batches of 32.

Closing a shared connection — fclose(), $mysqli->close() or a reconnect helper on a connection another request was parked reading resumed that request into freed memory; it now fails immediately with a 500 naming what happened.

Fatals no longer wipe shared state — Unwinding an aborted request double-released include variables, silently resetting the arrays and objects worker-mode applications keep their cross-request state in.

Async pool pacing — Task handoffs are no longer paced by the fixed 1 ms idle timer: a Shared\Channel transfer dropped from 0.69 ms to 0.14 ms per item, and idle wakeups from 777/s to 92/s.

Paused requests keep their start time — $request->startTime() and oxphp_server_info()[request_time] no longer read 0 or a concurrent request’s value after a suspension.

Body-read warnings reach the right request — post_max_size and upload-limit warnings now fire inside the request that sent the body, so error handlers see its superglobals, and one that throws answers 500 instead of closing the connection with nothing.

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

Privilege drop by default — Started as root where a www-data account exists, the server binds its listeners and drops to www-data before serving any traffic; the official image no longer runs as root. Pass --user=root to keep the old behavior.

Fail-closed TLS config — A half-configured pair (TLS_CERT without TLS_KEY, or vice versa) or a non-UTF-8 cert/key value now aborts startup instead of silently serving plain HTTP, naming the offending variable.

Strict ASYNC_* / SUPERGLOBALS parsing — Garbage in ASYNC_WORKERS, ASYNC_QUEUE_CAPACITY or ASYNC_MAX_FIBERS is a startup error instead of a silent default, and oxphp run no longer ignores an invalid SUPERGLOBALS_ENABLED.

QUERY without Content-Type returns 400, not 415 — The only server-generated 415 is gone; a custom ERROR_PAGES_DIR/415.html for this case must be renamed to 400.html.

Profiler export envelope resolution — PROFILER_EXPORT_XHGUI=false now wins even on a /run/import URL, and envelope auto-detection keys only on each tool’s canonical path (xhgui /run/import, Buggregator /api/profiler/store), no longer on a fuzzy "xhgui" substring.

ADDED

Buggregator profiler export — The profiler’s HTTP push wraps xhprof data in the envelope Buggregator’s POST /api/profiler/store expects; PROFILER_EXPORT_BUGGREGATOR toggles it, with tags and app_name set via env.

OTLP trace export over TLS — An https:// OTEL_EXPORTER_OTLP_ENDPOINT is now exported encrypted on both gRPC and http/protobuf, verified against the system trust store via rustls; the runtime image must ship a CA bundle (the official one does).

Full exception data on APM spans — The exception span event now carries exception.message and exception.stacktrace alongside exception.type, so New Relic Errors Inbox, Jaeger and Tempo render the message and stack; capped by OTEL_APM_MESSAGE_MAX_BYTES / OTEL_APM_STACKTRACE_MAX_BYTES.

Graceful drain of long-lived connections — On SIGTERM the server sends GOAWAY, closes idle keep-alives and ends open SSE/flush streams promptly; DRAIN_TIMEOUT_SECONDS default lowered 30 → 25 to fit Kubernetes’ 30s grace period.

TLS_MIN_VERSION — Minimum accepted protocol version: 1.2 (default) or 1.3. Anything else — including a foreign syntax like "TLSv1.2" — is a hard startup error, validated even when TLS is disabled and reported as tls_min_version in /config.

CHANGED

Cheap STATIC_REVALIDATE — stat() at most once every 3 seconds per file; a static hit is served from memory with at most one syscall, and on-disk changes become visible within 3s. Default stays off.

Clearer TLS startup errors — A typo’d path fails with "TLS_CERT: cannot read /etc/ssl/cert.pem: No such file or directory" instead of a bare OS error; PEM-parse and key-mismatch errors are prefixed the same way.

FIXED

Worker-mode cancellation targeting — A client abort or request timeout could cancel whichever request was most recently accepted on the worker instead of the one actually being cancelled, aborting an unrelated in-flight request.

finish_request() vs multiplexed streams — Once any request on a worker called oxphp_finish_request(), every other stream multiplexed onto that worker silently dropped its subsequent oxphp_stream_flush() output.

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

Timed-out await cancels the task — An abandoned task is force-cancelled: parked tasks resume into cancellation, CPU-bound ones are interrupted at an opcode boundary. Previously it ran to request end.

ADDED

Nested oxphp_async() — An async task can dispatch tasks of its own and suspend on await_all / await_any / await_race without blocking its worker thread. The “no nested async” restriction is gone.

In-flight task cap — ASYNC_MAX_FIBERS (default 256) bounds queued + running tasks; dispatch past the cap rejects immediately with AsyncException, so fan-out cannot deadlock.

Internal server allow-list — INTERNAL_ALLOW_IPS: a CIDR allow-list for /metrics and /config; health endpoints stay reachable for orchestrators.

Profiler sampling excludes — PROFILER_EXCLUDE_PATHS keeps framework self-traffic (/_profiler, /_wdt) out of statistical sampling; explicit triggers still profile.

FIXED

Channel sender wake-up — A fiber send-waiter parked on a full Shared\Channel wakes when recv_blocking frees a slot — sendTimeout no longer trips spuriously under saturation.

Async edge cases — A graceful-shutdown crash; await on a closed promise stalling instead of rejecting; await_all leaving promises running on early bail-out.

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

Worker routing contract — Static assets from disk, everything else to the worker ENTRY_FILE — arbitrary .php files, directory indexes and the root index.php fallback are no longer resolved per-request.

Honest CGI PATH_INFO — In Framework mode PATH_INFO is set only for an explicit /index.php/extra request; PHP_SELF reports the front controller. Route on REQUEST_URI.

ADDED

Range requests (RFC 9110) — 206 Partial Content for static files: video seeking, resumable downloads, partial PDF loading; If-Range honored with strong validators.

HTTP/2 connection limits — Five env vars bound per-connection resource use; resolved values are logged at startup.

CHANGED

Strong static ETags — "<size>-<mtime>" replaces W/"…" — required by If-Range; compressed representations keep a weak tag.

FIXED

Application headers win — X-Content-Type-Options, X-Frame-Options and CSP set from PHP are no longer overwritten — server values are insert-if-absent fallbacks.

Streaming vs compression — flush()-streamed responses pass through uncompressed instead of being buffered in full by the brotli layer until script end.

SECURITY

HTTP/2 DoS hardening — Rapid Reset (CVE-2023-44487), HPACK bombs and window-stall attacks are bounded; dead connections reclaimed via PING/PONG keepalive.

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

remote_ip in access logs — The JSON field remote_addr is renamed remote_ip and carries the IP without a port — matching nginx/Apache/Caddy.

ADDED

PHP-style CLI — oxphp run script.php executes a file under CLI semantics with the full engine underneath; #!/usr/bin/env oxphp scripts run directly; -d ini overrides accepted.

--user privilege drop — Bind privileged ports as root, then irreversibly drop to an unprivileged user before any request-handling thread spawns.

Forwarded port honored — Behind TRUSTED_PROXIES, SERVER_PORT honors X-Forwarded-Port; REMOTE_PORT is filled from RFC 7239 for= when the proxy supplies one.

FIXED

Decorator crashes — Two SIGSEGV in the function-decorator path (freed instance cache; dangling zval across resize) are gone; nesting overflow now throws StackOverflowException.

Profiler attributes honor arguments — #[SlowThreshold(ms:)], #[MemoryThreshold(kb:)] and #[Mark(label:)] no longer collapse to shared defaults; APM span events reach OpenTelemetry.

Upload object API wired — Request::file() / files() return UploadedFile objects for every $_FILES shape instead of null.

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

Trichotomous wait API — Channel and Mutex split every wait into try* / bare verb / *Timeout(int $ms), returning value-typed SendResult / RecvResult instead of mixed|null.

await_any means Promise.any — First fulfilled wins; all-rejected throws AggregateAsyncException. The old “first settled” behavior is now oxphp_async_await_race().

Honest cancel statuses — Timeout → 504, graceful drain → 503 + Retry-After, client disconnect → 499 (log-only). Supervisor kills and userland cancels keep 500.

Renames without shims — Async\Exception → AsyncException, Shared\Exception → SharedException; Worker drops get* prefixes; Counter goes minimal (add/set); Flag mirrors Atomic; Once gains getOrInit()/status().

ADDED

Shared\Atomic + Ordering — A generic int64 atomic: load/store/swap/CAS/fetch* with explicit memory orderings, one-to-one with std::sync::atomic.

Shared\Registry — Name-keyed process-global handles: every worker converges on the same entry; the factory runs at most once.

REMOVED

One timeout source — REQUEST_TIMEOUT_SECONDS and oxphp_request_heartbeat() are gone — max_execution_time and set_time_limit() remain.

FIXED

connection_aborted() in SSE — Returns true after a mid-stream disconnect, so while (!connection_aborted()) loops exit cleanly.

PERFORMANCE

Shared\* hot path — Per-op overhead lands within criterion noise of a raw atomic load; ≈4.7× geomean at 8 threads vs the previous shape.

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

Strict boolean env parsing — Only on/true/1/yes and off/false/0/no; a typo like ture refuses to start instead of silently defaulting.

ADDED

ENTRY_FILE + WORKER_MODE_ENABLED — One variable selects the routing mode by extension; an explicit toggle enables persistent workers. INDEX_FILE / WORKER_FILE become deprecated aliases.

OxPHP\Server\Worker class — A runtime introspection handle: id, request count, memory, RSS — plus application-driven recycling via scheduleExit().

CHANGED

Static cache renames — STATIC_CACHE_TTL → STATIC_MAX_AGE; STATIC_CACHE → STATIC_REVALIDATE with flipped polarity. Defaults unchanged.

DEPRECATED

WORKER_MAX_REQUESTS — Parsed and ignored with a startup WARN — migrate to WORKER_MAX_MEMORY_MIB or scheduleExit().

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

PHP 8.5 images — :php8.5, :php8.5-alpine and patch-pinned tags ship alongside 8.4; latest stays on 8.4 until the soak window closes.

FIXED

Chunked streaming across requests — The bridge’s per-request context leaked between worker requests, dropping chunked Transfer-Encoding after the first flush.

Shared\* inside user fibers — Channel::recv()/send() inside Fiber::start() no longer take the oxphp-fiber suspend path and crash with fiber_await rc=1.

Named arguments for the plugin API — Plugin-defined classes and functions now advertise real parameter names — named-argument calls work.

Shared handles in async closures — Channel/Map/Pool captured by an oxphp_async() closure no longer arrive as null on the receiving thread.

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

Async namespace — Async classes moved under OxPHP\Async\; the functions now ship in plugin-async.

ADDED

Seven Shared\* primitives — Counter, Flag, Once, Mutex, Channel, Map, Pool — atomic state shared across workers, with registry observability and a deadlock detector.

Per-request profiler — xhprof / speedscope / pprof / collapsed outputs; cookie, header, query and statistical triggers; HTTP push with an xhgui envelope.

APM auto-instrumentation — plugin-apm with a PHP tracing SDK and error capture; the async and APM subsystems moved into Rust plugins.

Production integrations — TRUSTED_PROXIES (RFC 7239), Kubernetes /readyz + /livez, security headers, per-IP rate limiting — all env-configured.

PERFORMANCE

Pool hot path — Uncontested acquire/release ≈0.9 µs in Docker, with per-thread affinity.

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

Fiber multiplexing — Concurrent I/O within a single worker thread; distributed tracing with W3C Trace Context and OpenTelemetry export.

oxphp_async() promises — Parallel PHP execution on a dedicated thread pool.

HTTP object API — OxPHP\Http\Request with lazy accessors, plus the attribute-based decorator system.

CHANGED

Port and backpressure — Default port 8080 → 80 (443 with TLS); the backpressure response 503 → 529.

FIXED

CGI correctness — SERVER_PROTOCOL reflects the real HTTP version; REQUEST_TIME is request start; IPv6 Host parsing; 408 on request timeout.

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

Async HTTP core — A Hyper + Tokio server with graceful shutdown, a custom oxphp SAPI with full superglobals, a ZTS worker pool with a bounded queue and 529 backpressure.

Three routing modes — Traditional, Framework and SPA; static files with an in-memory cache, ETag/304, brotli, TLS.

Worker mode — Persistent workers via oxphp_worker(), early response completion, SSE streaming, cooperative timeouts.

Observability — /metrics, /health, /config, structured JSON access logs, request IDs.

Docker-first — A multi-stage Alpine build; amd64/arm64 images on GHCR.