After an off-list discussion with Tim Düsterhus, I'm retracting some of the examples from my first email: http_get_last_response_headers(), the libxml errors, error_get_last() and strtok(). They don't depend on the incoming HTTP request, so a worker SAPI shouldn't reset them between "logical" requests. Like any other state shared in worker mode, userland can clear them when needed with the existing functions (http_clear_last_response_headers(), libxml_clear_errors(), error_clear_last()).
The rule I'm proposing is narrower: reset only the state built from the incoming request or tied to its response. The session module and output_add_rewrite_var() still fall under it. The main point of the proposal stands: hooks for "logical" requests give extensions one abstraction that works with every SAPI that has a worker mode, instead of special-casing each one. FrankenPHP will follow the same rule in its next release. Tim suggested a short RFC. I'll write it soon. On Thu, Oct 29, 2026 at 6:47 PM Kévin Dunglas <[email protected]> wrote: > > Hello internals, > > As you may know, FrankenPHP and some other SAPIs (like the > experimental ones written in Rust) have optional support for a "worker > mode". The main idea behind these worker modes is to use long-running > scripts, started when the web server starts, to handle multiple HTTP > requests in a loop. > > This allows keeping states in memory across requests - such as the > Composer autoloader or Symfony and Laravel services - instead of > re-computing them at the beginning of each new request. This also > allows maintaining persistent connections to remote services across > requests. > > Worker mode dramatically improves performance, at the cost of no > longer isolating state between requests and losing the memory leak > tolerance provided by the traditional "fire and forget" model. This > mode has become increasingly popular since the release of FrankenPHP, > and is now natively supported by a large part of the ecosystem > (Symfony, Laravel, Yii...). > > Currently, the engine and extensions have no knowledge of this, and > have no way to detect when a "real" HTTP request starts or ends. As > expected, they are only aware of the start (and end) of the main, > long-running script. FrankenPHP (and other SAPIs with a similar mode) > reset "real HTTP request" related data by poking into the engine and > first-party extensions > (https://github.com/php/frankenphp/blob/7f50be9d86261bf7cff9b051ab4fb572e95020ed/frankenphp.c#L400-L646). > This approach is fragile, and the internal code resetting these states > can change without warning in a new PHP version. > > Each omission results in a cross-request leak or a memory safety bug. > FrankenPHP fixed several of them this year: > > - a session ID leaking to other clients when ext/session is a shared > module, because FrankenPHP can only reset the session globals when it > is built against the session headers: > https://github.com/php/frankenphp/security/advisories/GHSA-v3ph-cgqh-r8p5 > - a use-after-free of php://temp streams, because nothing releases > SG(request_info).request_body between worker requests and FrankenPHP's > own cleanup freed streams it didn't own: > https://github.com/php/frankenphp/security/advisories/GHSA-cm3q-jw2q-9rh5 > - a double free because sapi_deactivate() frees > SG(request_info).content_type_dup and current_user without clearing > them (fixed for PHP 8.6: https://github.com/php/php-src/pull/22974): > https://github.com/php/frankenphp/security/advisories/GHSA-h764-4wh7-m7j2 > - http_get_last_response_headers() returning headers fetched by a > previous request: > https://github.com/php/frankenphp/security/advisories/GHSA-pmvg-8483-mw84 > - libxml_get_errors() returning errors of a previous request, with > parse-error fragments and server paths: > https://github.com/php/frankenphp/security/advisories/GHSA-4gfw-9v29-gp9h > > Third-party extensions detect the boundaries themselves, each in its > own way. Xdebug and the Datadog profiler replace sapi_module.activate > and sapi_module.deactivate when the SAPI is FrankenPHP. Datadog's > tracer and AppSec, and New Relic in an open pull request, hook > frankenphp_handle_request(). Blackfire's Symfony bridge, Tideways and > Sentry wrap each request with start and stop calls in userland. > > Calling RSHUTDOWN and RINIT between worker requests doesn't work: > these handlers mix state that needs resetting with state the script > keeps. For instance, the standard extension's RSHUTDOWN releases the > strtok() state, but also destroys register_tick_function() callbacks > and user stream filters. The session extension's RSHUTDOWN writes and > closes the session, but also frees the save handlers registered at > boot. The libxml extension's RSHUTDOWN drops the external entity > loader. Only each extension's maintainers know which of their globals > belong to which kind. > > I'd like to add two optional module hooks, called by the SAPI between > two logical requests of the same script: > > - request_end: release the state tied to the finished request, but > keep everything still reachable from userland; > - request_begin: set up the next request once its headers and > superglobals are available (session.auto_start, Xdebug's triggers). > > Two engine functions, php_request_end() and php_request_begin(), would > run the output and SAPI steps that worker SAPIs copy today from > php_request_shutdown() and php_request_startup(), import the > superglobals of the new request (except $_ENV, which comes from the > process), set the execution timer, and call the hooks. > > Releasing the request body takes two small changes: php://input > streams hold a reference to the body, and freeing the body, for > instance when the script closes it, clears > SG(request_info).request_body. Core extensions would implement the > hooks first; third-party extensions could adopt them at their pace. > SAPIs that don't call these functions see no behavior change. > > I drafted a working patch for master, with the standard and session > hooks: https://github.com/dunglas/php-src/tree/request-boundary-hooks. > FrankenPHP doesn't build against master yet, so I tested it with a > patched version of FrankenPHP and it works. > > As this is an internal API with no userland or BC impact, an RFC isn't > strictly needed. > > I made a similar but broader proposal almost 3 years ago > (https://externals.io/message/122027). I think time has shown that > worker modes are here to stay, and this less intrusive proposal could > fix several maintenance issues affecting the whole ecosystem. > > So, I would like your opinion on this before working more on it. > > Best, > -- > Kévin Dunglas > > https://dunglas.dev
