Skip to content

Worker mode: all response status codes collapse to 200 #2

Description

@diolektor

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions