Skip to content

fix(error): return HTTP 500 for uncaught frontend errors - #16982

Open
Ibochkarev wants to merge 3 commits into
modxcms:3.xfrom
Ibochkarev:fix/http-500-on-uncaught-errors
Open

fix(error): return HTTP 500 for uncaught frontend errors#16982
Ibochkarev wants to merge 3 commits into
modxcms:3.xfrom
Ibochkarev:fix/http-500-on-uncaught-errors

Conversation

@Ibochkarev

@Ibochkarev Ibochkarev commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

What changed and why

When a snippet, plugin, TV, or other element throws during web rendering, MODX often returned a blank page with HTTP 200. Proxies cached that response. Uptime checks saw 200 and missed the failure.

This PR adds modUncaughtErrorHandler, registered from _initErrorHandler():

  • set_exception_handler() for uncaught \Throwable (TypeError, ParseError, etc.)
  • register_shutdown_function() with error_get_last() for E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR
  • E_USER_ERROR in modErrorHandler routes to LOG_LEVEL_FATAL on web requests when send_http_500_on_error is enabled

Each path calls the existing sendError('fatal') flow. fatal.include.php now sends no-cache headers so reverse proxies do not store 500 responses.

Opt out with system setting send_http_500_on_error (default true).

How to test

  1. Create a snippet that throws a TypeError (pass a string to a typed int parameter).
  2. Put [[snippetName]] on a published resource and open the page.
  3. Expect HTTP 500 and the generic fatal error page, not a blank 200.
  4. Check response headers for Cache-Control: no-store.
  5. Set send_http_500_on_error to false and confirm the handler no longer forces 500.

Unit tests:

core/vendor/bin/phpunit -c _build/test/phpunit.xml --group modUncaughtErrorHandler
core/vendor/bin/phpunit -c _build/test/phpunit.xml --group modErrorHandler

CI (phpcs, PHPUnit on MySQL 8.1–8.5) is green on this branch.

Related issue(s)/PR(s)

Resolves #16932

Compatibility notes

  • Web requests only. CLI skips handler registration; sendError() behavior in CLI is unchanged.
  • Applies to frontend, manager, and connector contexts that initialize modX.
  • PHP 7+ (\Throwable hierarchy). No new Composer dependencies.

Breaking change assessment

Behavior change: E_USER_ERROR on web requests now terminates with HTTP 500 when send_http_500_on_error is enabled (default). Code that logged a user error and kept running should use E_USER_WARNING or disable the setting.

No public API signature changes. Patch-safe for sites that do not rely on E_USER_ERROR continuing after the handler runs.

Test coverage

  • Added _build/test/Tests/Model/Error/modUncaughtErrorHandlerTest.php: fatal type detection, error formatting, exception logging, shutdown behavior for non-fatal errors, registration (web only).
  • Extended _build/test/Tests/Model/Error/modErrorHandlerTest.php: E_USER_ERRORLOG_LEVEL_FATAL when enabled (web only; skipped under CLI PHPUnit).

Contributors

Thanks to @pbowyer for #16932 and the documented workaround that shaped this approach.

AI tool use

Claude (Cursor) helped draft the implementation, tests, and PR text from the issue analysis and codebase review.

Register exception and shutdown handlers so TypeErrors, fatal errors,
and E_USER_ERROR during web requests produce HTTP 500 via sendError('fatal')
instead of a blank HTTP 200 response.

Fixes modxcms#16932
@Ibochkarev
Ibochkarev marked this pull request as ready for review August 2, 2026 05:19
Make the handler constructor public for direct unit testing and run the
fatal exception test in a separate process so headers_sent() stays false.
@pbowyer

pbowyer commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for this PR @Ibochkarev.

I haven't tested so this is a question where I don't know the answer: if I switch the logger implementation to Monolog and register its exception handler, which one wins here?

@Ibochkarev

Copy link
Copy Markdown
Collaborator Author

Thanks for this PR @Ibochkarev.

I haven't tested so this is a question where I don't know the answer: if I switch the logger implementation to Monolog and register its exception handler, which one wins here?

Good question. #16931 only adds setLogger() — core does not register Monolog's ErrorHandler.

This PR registers modUncaughtErrorHandler during initialize(), before OnMODXInit. If you call Monolog\ErrorHandler::register() in a plugin after init, Monolog runs first and chains to our handler, so you get Monolog logging plus sendError('fatal') / HTTP 500. If you register Monolog before initialize(), our handler replaces it and Monolog's exception handler never runs.

setLogger() alone does not conflict — it only changes log output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PHP errors return HTTP 200 instead of 500 (blank white page)

2 participants