In worker mode every response is emitted with status 200, whatever the application returned. Headers and body are correct — only the status is lost, so a RedirectResponse goes out as 200 with its Location header, and a 404 becomes a soft 200.
Traditional mode is unaffected.
Environment
|
|
oxphp/runtime |
v0.2.0 |
| OxPHP server |
0.10.0 (ghcr.io/oxphp/oxphp:0.10.0), features: php, plugin-apm, plugin-async, plugin-otel |
oxphp_sapi ext |
0.1.0 |
| PHP |
8.5.9 ZTS, Alpine |
| Runner |
HttpKernelRunner |
Reproduction
Minimal app, no framework — 30 lines total.
composer.json:
{
"require": {
"oxphp/runtime": "^0.2.0",
"symfony/http-foundation": "^7.0",
"symfony/http-kernel": "^7.0"
},
"config": { "allow-plugins": { "symfony/runtime": true } },
"extra": { "runtime": { "class": "OxPHP\\Runtime\\Runtime" } }
}
public/index.php:
<?php
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
$_SERVER['SCRIPT_FILENAME'] ??= __FILE__;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return static fn (): HttpKernelInterface => new class implements HttpKernelInterface {
public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response
{
return match ($request->getPathInfo()) {
'/missing' => new Response('missing', 404),
'/redirect' => new RedirectResponse('/ok', 302),
'/teapot' => new Response('teapot', 418),
default => new Response('ok', 200),
};
}
};
Run the same image twice, once per mode:
composer install
# traditional
docker run -d --name repro-trad -p 8110:80 -v "$PWD:/repro" \
-e DOCUMENT_ROOT=/repro/public -e ENTRY_FILE=index.php <image>
# worker
docker run -d --name repro-worker -p 8111:80 -v "$PWD:/repro" \
-e DOCUMENT_ROOT=/repro/public -e ENTRY_FILE=index.php \
-e WORKER_MODE_ENABLED=true <image>
for u in /ok /missing /redirect /teapot; do
curl -s -o /dev/null -w "%{http_code} $u\n" "http://localhost:8110$u" # traditional
curl -s -o /dev/null -w "%{http_code} $u\n" "http://localhost:8111$u" # worker
done
Result
| path |
returned by app |
traditional |
worker |
/ok |
200 |
200 |
200 |
/missing |
404 |
404 |
200 |
/redirect |
302 |
302 |
200 |
/teapot |
418 |
418 |
200 |
Headers survive — this is /redirect in worker mode, Location present under a 200:
HTTP/1.1 200 OK
content-type: text/html; charset=UTF-8
content-type: text/html; charset=utf-8
cache-control: no-cache, private
location: /ok
server: OxPHP
x-request-id: 6a70c7cc717d00000005
content-length: 258
(Also visible above: content-type is emitted twice in worker mode, once by the app and once by the server. Probably a separate, much smaller issue.)
Diagnosis
Internal\Emitter\ResponseEmitter::emit() sets the status with http_response_code($status). That call has no effect inside the oxphp_worker() / Worker::serve() loop — the status stays at the default 200, while header() calls from the same block do land.
I checked whether a raw status line would get through: mounting a patched ResponseEmitter that additionally calls
\header('HTTP/1.1 ' . $status, true, $status);
changes nothing — still 200 on all four paths. So PHP's status API as a whole looks like it isn't consulted on the worker path, and the status likely has to be set through OxPHP's own response API instead.
Reproduced with SUPERGLOBALS_ENABLED both true and false, so it is independent of that setting. headers_sent() is false at emit time (the if (!\headers_sent()) guard is entered — the headers above prove it).
Impact
For a normal web app in worker mode this is not a degraded edge case: / redirects answer 200 instead of 302, every 404 becomes a soft 404, and error pages are served as successes. Found while moving a Symfony 8.1 site (oxphp.dev) onto the adapter — the site looks healthy in a browser and is quietly wrong for crawlers and HTTP clients.
Unrelated finding, happy to file separately
Second worker-mode issue in the same run: Dotenv::bootEnv() writes into $_SERVER/$_ENV once at worker boot, and the soft reset between requests clears them, so from the second request on a given worker every %env(...)% lookup throws EnvNotFoundException. It surfaces as an intermittent 22-byte Internal Server Error body from AbstractHttpRunner::emitFallback500() — under 200, per the bug above. Say the word and I'll open a separate issue with its own repro.
In worker mode every response is emitted with status
200, whatever the application returned. Headers and body are correct — only the status is lost, so aRedirectResponsegoes out as200with itsLocationheader, and a404becomes a soft200.Traditional mode is unaffected.
Environment
oxphp/runtimeghcr.io/oxphp/oxphp:0.10.0), features: php, plugin-apm, plugin-async, plugin-oteloxphp_sapiextHttpKernelRunnerReproduction
Minimal app, no framework — 30 lines total.
composer.json:{ "require": { "oxphp/runtime": "^0.2.0", "symfony/http-foundation": "^7.0", "symfony/http-kernel": "^7.0" }, "config": { "allow-plugins": { "symfony/runtime": true } }, "extra": { "runtime": { "class": "OxPHP\\Runtime\\Runtime" } } }public/index.php:Run the same image twice, once per mode:
Result
/ok/missing/redirect/teapotHeaders survive — this is
/redirectin worker mode,Locationpresent under a200:(Also visible above:
content-typeis emitted twice in worker mode, once by the app and once by the server. Probably a separate, much smaller issue.)Diagnosis
Internal\Emitter\ResponseEmitter::emit()sets the status withhttp_response_code($status). That call has no effect inside theoxphp_worker()/Worker::serve()loop — the status stays at the default200, whileheader()calls from the same block do land.I checked whether a raw status line would get through: mounting a patched
ResponseEmitterthat additionally callschanges nothing — still
200on all four paths. So PHP's status API as a whole looks like it isn't consulted on the worker path, and the status likely has to be set through OxPHP's own response API instead.Reproduced with
SUPERGLOBALS_ENABLEDbothtrueandfalse, so it is independent of that setting.headers_sent()isfalseat emit time (theif (!\headers_sent())guard is entered — the headers above prove it).Impact
For a normal web app in worker mode this is not a degraded edge case:
/redirects answer200instead of302, every 404 becomes a soft 404, and error pages are served as successes. Found while moving a Symfony 8.1 site (oxphp.dev) onto the adapter — the site looks healthy in a browser and is quietly wrong for crawlers and HTTP clients.Unrelated finding, happy to file separately
Second worker-mode issue in the same run:
Dotenv::bootEnv()writes into$_SERVER/$_ENVonce at worker boot, and the soft reset between requests clears them, so from the second request on a given worker every%env(...)%lookup throwsEnvNotFoundException. It surfaces as an intermittent 22-byteInternal Server Errorbody fromAbstractHttpRunner::emitFallback500()— under200, per the bug above. Say the word and I'll open a separate issue with its own repro.