Skip to content

core: the prepared environment -- RequestContext.env, registerPreparedEnv, and the $_SERVER merge #207

Description

@melihucar

Gate: default
Agent: opencode

Nothing in this port can deliver a per-request/per-worker environment variable
to PHP. Upstream calls this the prepared environment, and it is not a nicety:
it is the only mechanism by which FRANKENPHP_WORKER=1 reaches
$_SERVER['FRANKENPHP_WORKER'], which is what
vendor/frankenphp/testdata/_executor.php:4 branches on to decide whether to
call frankenphp_handle_request() at all. Without it, 44 upstream fixtures
cannot be run in worker mode, because every one of them silently takes the
regular-mode branch.

Split out of #14, which cannot implement this without touching four files
outside its lane.

Why there is no back door

$_SERVER in this SAPI is built only by frankenphp_register_variables
(vendor/frankenphp/frankenphp.c:1371-1383), which calls
go_register_server_variables and then
frankenphp_register_variables_from_request_info. The OS environment reaches
$_ENV and getenv() (frankenphp.c:1117 swaps php_import_environment_variables
for get_full_env), and never $_SERVER. So exporting FRANKENPHP_WORKER=1
from the process, or putenv()-ing it, does not work. The prepared env is the
mechanism, and there is no substitute.

What exists upstream

  • frankenphp.c:127static THREAD_LOCAL HashTable *prepared_env.
  • frankenphp_add_to_prepared_env(name, name_len, val, val_len, size)
    (frankenphp.c:1461-1469, declared frankenphp.h:205-206) — populates it.
    Upstream calls it from registerPreparedEnv (cgi.go), reached from
    go_update_request_info (cgi.go:295-297) when len(fc.env) != 0.
  • frankenphp_merge_with_prepared_env(track_vars_array)
    (frankenphp.c:1270-1275, declared frankenphp.h:207) — copies it into
    $_SERVER. Upstream calls it from go_register_server_variables
    (cgi.go:185-187) last, after the known variables and the headers:
    "The Prepared Environment is registered last and can overwrite any previous
    values."
  • Keys are NUL-terminated in the Go map (worker.go:138 writes
    "FRANKENPHP_WORKER\x00"), because registerPreparedEnv hands the key
    straight through to C. Decide deliberately whether our env map stores the
    NUL or appends it at the boundary, and say which in a comment — do not
    reproduce the Go representation by accident.

What to build

  1. crates/frankenrust-sys/build.rs — additive only: two
    .allowlist_function entries, frankenphp_add_to_prepared_env and
    frankenphp_merge_with_prepared_env, with a comment naming this issue the
    way the existing frankenphp_update_local_thread_context entry names server: regular-thread handler, hyper HTTP/1.1 front end, and the async/pthread bridge #13.
    Hazard: core: worker mode -- the frankenphp_handle_request park/resume loop #14 adds one entry (frankenphp_shutdown_dummy_request) to the
    same list. Append yours at the end of the function block rather than
    interleaving with the existing comment blocks, so the two diffs do not
    collide on the same hunk.
  2. crates/frankenrust-core/src/context.rsRequestContext gains
    pub env: Option<...> (upstream fc.env, a PreparedEnv; nil and empty
    both mean "do not call into C", per the len(fc.env) != 0 guards at both
    call sites — pick one representation and pin it in the doc comment).
    Set through RequestContext::new's existing shape or a builder-style
    setter; do not break the existing four-argument constructor's call sites in
    crates/frankenrust-server/src/server.rs and the existing tests.
  3. crates/frankenrust-core/src/callbacks/servervars.rs
    go_update_request_info calls frankenphp_add_to_prepared_env once per
    entry when env is non-empty, before the rest of its work, mirroring
    cgi.go:295-297. The two "fc.env (PreparedEnv) is not part of this issue's RequestContext ... there is nothing to check" comments (around :584 and
    :639) are now false and must be replaced, not left standing.
  4. crates/frankenrust-sys/shim.c's go_register_server_variables — call
    frankenphp_merge_with_prepared_env(track_vars_array) after the batch
    the Rust side supplied, guarded on the batch reporting a non-empty prepared
    env. Ordering is the behaviour: last wins.

Acceptance

  • A unit test in servervars.rs in the shape of the existing slot tests:
    install a RequestContext carrying env = {"FRANKENPHP_WORKER": "1"}, call
    go_update_request_info, and assert the C side saw the add (a test double
    over the extern, or the same technique the neighbouring tests already use for
    C-facing calls — do not weaken an existing test to make room).
  • An end-to-end test that actually reads $_SERVER from PHP: a fixture PHP
    script that echoes $_SERVER['FRANKENPHP_WORKER'] ?? 'unset', served with
    a RequestContext carrying that prepared env, asserting the body is 1; and
    the same script with no prepared env asserting unset. This is the criterion
    that can actually fail — the unit test alone cannot see the $_SERVER merge.
  • A test pinning the ordering: a prepared-env key that collides with a
    known CGI variable (e.g. SERVER_SOFTWARE) overwrites it, not the reverse
    (cgi.go:186).
  • bash scripts/gate.sh default passes.

Out of scope

  • getenv()/$_ENV sandboxing (frankenphp.c:661-665's sandboxed_env),
    go_putenv, and the putenv()-per-worker-script-run release.
  • WithRequestPreparedEnv as a public request-option API surface — a plain
    public field on RequestContext is enough for now.
  • Worker mode itself (core: worker mode -- the frankenphp_handle_request park/resume loop #14), and wiring the worker's FRANKENPHP_WORKER=1 into
    a running server — that belongs to the server-side worker configuration
    issue, which consumes this.

Recoveries: 1

Metadata

Metadata

Assignees

No one assigned

    Labels

    fr:blockedFailed repeatedly; awaiting recoveryfr:followupFiled by an agent mid-taskfr:p1On the critical path to the next milestone

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions