Skip to content

Worker mode: request-parsing failures (max_input_vars, post_max_size) raise E_WARNING between requests and kill the worker script under a throwing error handler #2631

Description

@PatNowak

What happened?

When a request arrives that PHP cannot fully parse — more form fields than max_input_vars, or a body larger than post_max_size — the warning PHP raises for it is attributed to frankenphp_handle_request() and fires between requests, inside the long-running worker script but outside the handler callback.

Under a classic SAPI (Apache mod_php, php-fpm) this warning is harmless: input parsing happens at request startup, before the script has installed any userland error handler, so the warning goes to the log and the script runs with truncated input. In worker mode the same warning fires inside a script that already has a persistent userland error handler installed. With a throwing error handler — symfony/error-handler, i.e. effectively every Symfony app running in worker mode via symfony/runtime's FrankenPhpWorkerRunner — the warning becomes an uncaught ErrorException thrown out of frankenphp_handle_request():

PHP Fatal error:  Uncaught ErrorException: Warning: frankenphp_handle_request():
Input variables exceeded 5000. To increase the limit change max_input_vars in php.ini.
in vendor/symfony/runtime/Runner/FrankenPhpWorkerRunner.php:72

Two things go wrong. The in-flight request is abandoned before the application ever sees it: the client receives an HTTP 200 whose body is the fatal-error dump when display_errors is on, or an empty body when it is off — never a proper 4xx. And the worker script dies with the uncaught fatal, so FrankenPHP has to boot a replacement worker.

Because input parsing happens before any application code runs — including authentication — any unauthenticated client can kill worker scripts at will by POSTing a form with more than max_input_vars fields. We hit this in production after migrating from Apache to worker mode: a single customer with a legitimately large import form restarted a worker thread on every submit (~35 fatals over a few days across three servers). A post_max_size overflow behaves identically.

To be clear about where the bug sits: the warning itself is standard PHP behaviour in every SAPI. What is FrankenPHP-specific is where it is delivered in worker mode — inside a long-running script with a live userland error handler, and outside the request callback, at a point where the worker script has no sanctioned way to catch it, associate it with the failed request, or answer that request.

Steps to reproduce

  1. Run a Symfony app in worker mode with the stock symfony/runtime setup (FrankenPhpWorkerRunner) and the default symfony/error-handler.
  2. Lower the limits so the repro is cheap: max_input_vars=10, post_max_size=1M.
  3. POST a form with 20 fields (or a 2 MB body) to any route.
  4. The client gets HTTP/1.1 200 OK with the fatal-error text as the body (empty with display_errors=Off), the log shows PHP Fatal error: Uncaught ErrorException: Warning: frankenphp_handle_request(): Input variables exceeded 10 ..., and the worker script is restarted. The application is never invoked.

Reproduced against FrankenPHP 1.12 / PHP 8.5 ZTS; also seen in production on FrankenPHP 1.12.7.

What I expected

A bad request should cost that one request an error response, not a worker restart and a bogus 200. Concretely, the worker script needs a sanctioned way to learn that request parsing failed so it can answer with a 400 and keep looping — or the warning should be delivered in a way that cannot escape as an exception from frankenphp_handle_request() itself.

Prior discussion

We first proposed handling this in symfony/runtime: masking E_WARNING while the runner is parked in frankenphp_handle_request() and answering a recorded parse warning with a plain 400 without invoking the application — symfony/symfony#65761. Nicolas Grekas's feedback was that this is a rare concern pushed onto the hot path and that a solution, possibly a new API, may belong in FrankenPHP itself — hence this issue.

In case it helps, some shapes such an API could take: frankenphp_handle_request() reports the parsing failure to the worker script (a return value, a dedicated catchable exception, or an accessor like frankenphp_request_error()), so the script can respond 400 and continue; or FrankenPHP itself answers clearly-unparsable requests with a 400 before invoking the callback, perhaps opt-in. We currently ship the error-mask-plus-400 workaround as a custom runtime runner in our application, so this is not urgent for us, but every Symfony app in worker mode is exposed to the unauthenticated worker-restart behaviour by default.

Build Type

Static binary

Worker Mode

Yes

Operating System

Ubuntu 24.04

CPU Architecture

x86_64

PHP configuration

FrankenPHP v1.12.7 PHP 8.5 Caddy v2.11.4
PHP 8.5.9 (cli) (built: Jul 31 2026 08:35:54) (ZTS gcc 16.1.0 x86_64)
Built by Static PHP <https://static-php.dev>
Zend Engine v4.5.9
    with Zend OPcache v8.5.9

Relevant directives:
max_input_vars = 5000
post_max_size = 15M
upload_max_filesize = 10M
display_errors = Off (production; with On the fatal dump becomes the 200 response body)

Relevant log output

[27-Aug-2026 20:01:36 UTC] PHP Fatal error:  Uncaught ErrorException: Warning: frankenphp_handle_request(): Input variables exceeded 5000. To increase the limit change max_input_vars in php.ini. in /var/www/app/vendor/symfony/runtime/Runner/FrankenPhpWorkerRunner.php:72
[27-Aug-2026 18:02:31 UTC] PHP Fatal error:  Uncaught ErrorException: Warning: frankenphp_handle_request(): Input variables exceeded 5000. To increase the limit change max_input_vars in php.ini. in /var/www/app/vendor/symfony/runtime/Runner/FrankenPhpWorkerRunner.php:72
[26-Aug-2026 10:01:40 UTC] PHP Fatal error:  Uncaught ErrorException: Warning: frankenphp_handle_request(): Input variables exceeded 5000. To increase the limit change max_input_vars in php.ini. in /var/www/app/vendor/symfony/runtime/Runner/FrankenPhpWorkerRunner.php:72

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions