Skip to content

Conversation

@innocenzi
Copy link
Member

@innocenzi innocenzi commented Dec 15, 2025

Supersedes #1629
Fixes #1619

This pull request attempts to properly fix the error handling to allow for more flexibility regarding how responses are returned when there are errors.

New features

Exception renderers

Previously, we had the rendering logic in our exception handler. The exception handler is now responsible for finding the right exception renderer to render an exception as a response.

Exception renderers implement the ExceptionRenderer interface, which require a canRender and a render method.

We now provide a JsonExceptionRenderer and a HtmlExceptionRenderer, which check for the Accept header to decide whether they can be used. For now, the HTML renderer has priority because browsers send */* as an Accept value, but I feel like we should have the equivalent of a wantsJson and also use that as a heuristic.

Users may now implement their own 404 or other error pages by implementing their own renderer:

final class NotFoundExceptionRenderer implements ExceptionRenderer
{
    public function canRender(Throwable $throwable, Request $request): bool
    {
        if (! $request->accepts(ContentType::HTML)) {
            return false;
        }

        if (! $throwable instanceof HttpRequestFailed) {
            return false;
        }

        return $throwable->status === Status::NOT_FOUND;
    }

    public function render(Throwable $throwable): Response
    {
        return new NotFound(
            body: view('./404.view.php'),
        );
    }
}

Note that while used internally, HttpRequestFailed is now also intended for userland usage. It's not always convenient to return a response (like NotFound) for errors due to nesting or return types.

The optional message that this exception accepts will be displayed in the error pages in production, too. When generating our own HttpRequestFailed internally, we use generic error messages and don't leak exception details.

 

Exception page

I got rid of Whoops and implemented our own exception page (highly inspired by Laravel's design). It's not feature-complete, but it's a good basis with more features and quality of life that Whoops.

CleanShot 2025-12-17 at 23 02 55

Note

Its front-end makes a lot of noise in the diff. It's in packages/router/src/> Exceptions/local, so unless you want to review Vue stuff, you can ignore that. Also note that the distribution files must be built whenever a change is made to the front-end.

 

ExceptionsConfig

It adds a convenient way to disable the default LoggingExceptionReporter, instead of having to write a kernel boot event listener to remove it manually.

It also contains the discovered exception reporters instead of the AppConfig. They needed their own home.

 

Breaking changes

Renamed throwHttpExceptions in tests

This feature was a bit of a work around the inability to catch exceptions in tests. The issue is that it introduced inconsistent behavior during tests and in production, and it was weird to reason about.

The new workaround is simply to catch exceptions during HttpRouterTester#sendRequest. When an exception is thrown, the exception handler is manually called to render the response, which is passed down to the TestResponseHelper.

Users that used throwExceptions() manually before will need to adapt their tests.

 

Removed Invalid

I removed Invalid as a response. It felt out of place and made it hard to have proper code for HTML and JSON handling.

Plus, its constructor expected "internal" properties (request, failed rules, failing class FQCN). Its logic is now in dedicated exception renderers when ValidationFailed is thrown.

 

Renamed ExceptionProcessor and ExceptionReporter

I renamed swapped their names because it makes more sense:

  • The ExceptionProcessor is responsible for processing exceptions. It's not discoverable, replacing it requires an initializer.

  • The ExceptionReporter classes are discoverable userland classes meant for reporting exceptions. By default, we still offer a LoggingExceptionReporter which write exceptions in log files.

It's a breaking change only if users implemented their own ExceptionProcessor, which they have to rename now.

 

Renamed HasContext

I renamed it to ProvidesContext. I took the opportunity to give it a better name.

 

To-dos

  • Get rid of throwHttpExceptions
  • Support JSON responses
  • Make HTML and JSON exception renders customizable in userland
  • Ensure ability to override error pages
  • Documentation
  • More tests
  • Custom development exception page
  • Add an option to display vendor code snippets

@innocenzi innocenzi changed the title refactor(core): improve http error handling feat(core): overhaul exception handling Dec 15, 2025
@innocenzi innocenzi marked this pull request as ready for review December 17, 2025 22:10
Copy link
Contributor

@NeoIsRecursive NeoIsRecursive left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I was thinking about, and I might have missed, translations on the "built-in" error messages?

Really looking forward to using this 👌

@innocenzi
Copy link
Member Author

One thing I was thinking about, and I might have missed, translations on the "built-in" error messages?

That made me go through an unexpected clean-up rabbit hole! It's now implemented for HTML responses 👍

@innocenzi innocenzi changed the title feat(core): overhaul exception handling feat(core)!: overhaul exception handling Dec 18, 2025
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.

4 participants