Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Whoops Middleware Laravel Package

franzl/whoops-middleware

PSR-15 middleware that drops the Whoops error handler into any middleware pipeline, turning uncaught exceptions into rich, stack-trace-friendly debug pages during development while staying out of the way in production.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require franzl/whoops-middleware

The package pulls in whoops/whoops, psr/http-server-middleware, and — new in 2.0 — middleware/utils for PSR-17 response-factory discovery. It no longer drags in Laminas/Diactoros, so you must supply a PSR-7/PSR-17 implementation yourself (most apps already have one: nyholm/psr7 with Slim, laminas/laminas-diactoros with Mezzio, guzzlehttp/psr7 elsewhere):

composer require nyholm/psr7   # or whichever your stack already uses

2.0 requires PHP >= 7.2 and supports PHP 8. On PHP 7.0/7.1 you are stuck on the 1.x line — pin ^1.0 explicitly if you're maintaining an old app, otherwise the default ^2.0 resolution will fail.

Read the source first, not the README. There is still one meaningful class (src/WhoopsMiddleware.php, a few dozen lines). Before wiring it in, confirm four things:

  1. The constructor takes a Whoops\Run instance (you own the Whoops configuration).
  2. It sets allowQuit(false) and writeToOutput(false) — otherwise Whoops would exit mid-request instead of returning a response.
  3. Which PSR-7 response it builds the 500 from. In 2.0 this is Middleware\Utils\Factory::getResponseFactory() — i.e. whatever PSR-17 implementation it discovers on your machine. That is now an app-level install decision, not a package decision.
  4. What status/headers it sets (still a flat 500 + Whoops' content type). That's the part you'll want to override eventually.

Minimal wiring

use Franzl\Middleware\Whoops\WhoopsMiddleware;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;

$whoops = new Run();
$whoops->pushHandler(new PrettyPageHandler());

// PSR-15 stack (Relay, Mezzio, Slim, custom dispatcher), outermost app middleware
$stack->add(new WhoopsMiddleware($whoops));
$response = $stack->handle($request);

First use case: local development. Drop the middleware in as the outermost PSR-15 layer and every exception thrown by routing, controllers, or downstream middleware becomes a readable HTML error page instead of a blank 500. Register it outside your router so routing failures are also caught.

Implementation Patterns

Gate registration by environment. Construct and register the middleware only when APP_DEBUG/APP_ENV=local is set. The package has no built-in environment switch — that decision belongs in your container/bootstrap:

if ($config->get('app.debug')) {
    $whoops = new Run();
    $whoops->pushHandler(new PrettyPageHandler());
    $stack->add(new WhoopsMiddleware($whoops));
}

Pin the PSR-17 implementation deliberately. 2.0's factory discovery is convenient but implicit. If your dependency tree ends up with two PSR-17 implementations (common after adding a client library that pulls Guzzle), discovery order decides which Response class you get. In a monorepo or shared CI image, assert it once:

assert(Middleware\Utils\Factory::getResponseFactory() instanceof \Nyholm\Psr7\Factory\Psr17Factory);

Handler-swapping per context. The middleware renders whatever Whoops\Run is configured to render, so use it in two places with two configurations rather than trying to branch inside one:

  • HTML browser traffic → PrettyPageHandler (set setPageTitle(), editor links, and addDataTable() for app-specific context like request ID or user ID).
  • JSON APIs / Accept: application/json → JsonResponseHandler, so clients get structured data instead of HTML. Choose the handler when building the stack (a small Accept-header check at the edge is usually enough), or push multiple handlers in the order Whoops should try them.

Ordering against other middleware. Whoops converts exceptions into responses, so nothing outside it will ever see an exception. Consequences:

  • Register it as early/outermost as possible so it wraps routing and the rest of the stack.
  • Frameworks differ: Slim runs middleware in reverse registration order, so add it last; Mezzio/Stratigility pipe() early to make it outermost; Relay's middleware() chain is outermost-first.
  • If you have an error-reporting/APM middleware that needs the raw exception, put it inside Whoops (so it doesn't swallow) or — better — attach a Whoops CallbackHandler that logs and rethrows/ignores, keeping one exception path.

Logging without a second error path. Don't add a separate try/catch logger; push a Whoops handler that logs and returns HandlerInterface::DONE so the pretty handler still renders. This keeps "log once, render once".

Custom status codes and headers. The middleware's response is still fixed at 500 with whatever Whoops produces; 2.0 does not add withStatus()/withHeader() hooks. If your API contract needs application/problem+json, a correlation ID header, or differentiated status codes, either wrap the middleware with a response-mutating middleware or — the pragmatic option — write the ~20-line equivalent yourself. In 2.x you can even keep the auto-discovered factory behaviour:

use Middleware\Utils\Factory;

final class Whoops implements MiddlewareInterface
{
    public function __construct(private Run $whoops) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        try {
            return $handler->handle($request);
        } catch (Throwable $e) {
            $this->whoops->allowQuit(false);
            $this->whoops->writeToOutput(false);
            $body = $this->whoops->handleException($e);

            $response = Factory::getResponseFactory()->createResponse(500)
                ->withHeader('Content-Type', 'text/html; charset=utf-8');
            $response->getBody()->write($body);

            return $response;
        }
    }
}

With a single-class surface, one constructor argument, and PHP 8 support now declared, owning this snippet is often a better day-to-day trade than depending on a wrapper you'll need to patch for a status code — and it makes your PSR-7 choice explicit rather than discovered.

Testing. Either don't register the middleware in testing, or assert on it explicitly: $response->getStatusCode() === 500 and the body contains the exception message. Don't let pretty pages leak into failing CI snapshots. If you test in a container that lacks a PSR-17 implementation, the middleware will blow up inside its catch block — see gotchas.

Gotchas and Tips

  • No PSR-17 implementation = no error page. 2.0 removed the hard Laminas dependency. If nothing discoverable is installed, Factory::getResponseFactory() throws while you are already handling an exception, and you get a bare 500 or a fatal with no Whoops output at all. Always require a PSR-7/17 implementation explicitly in your root composer.json, even though it feels redundant.
  • Multiple PSR-17 implementations = non-deterministic response class. Discovery picks in a fixed order, not by your preference. If a test asserts header casing, stream behaviour, or getReasonPhrase(), it can pass on your machine and fail in CI. Pin the set.
  • PrettyPageHandler is a secret-leaking dev tool. It dumps $_ENV/$_SERVER, source excerpts, and request data. Guard registration hard; never ship a config where debug is defaulted to true.
  • Errors before the middleware runs are not covered. Bootstrap failures, container resolution errors during stack construction, and PHP fatal/parse errors never reach a PSR-15 layer. Keep a global set_exception_handler/set_error_handler as a backstop.
  • display_errors must be off. If PHP prints the error first, Whoops output is garbage-prefixed and headers may already be sent.
  • Editor links need Xdebug. PrettyPageHandler's "open in editor" file/line links rely on xdebug.file_link_format; without it they're dead text. Set setEditor('phpstorm://open?file=%f&line=%l') or similar explicitly if you want them.
  • Don't double-configure Run. If you also use Whoops in CLI scripts or a queue worker, the allowQuit/writeToOutput flags set by the middleware apply per-instance. Build a fresh Run per context or you'll inherit unexpected behaviour.
  • Whoops catches Throwable, so TypeError, Error, and engine errors are rendered too — usually a feature, but don't rely on it to render E_ERROR fatals from a PHP_INT_MAX-style crash.
  • Whoops handler ordering matters. Handlers return DONE/QUIT; verify your custom logging handler doesn't short-circuit the pretty handler (return DONE to continue).
  • Upgrading 1.x → 2.x is a dependency-graph change, not an API change. new WhoopsMiddleware($whoops) still works; what changes is that a Laminas response is no longer guaranteed. Grep your tests and any response-mutating wrapper for Laminas\Diactoros assumptions before bumping, and check nothing else in the stack relies on 1.x having installed it transitively.
  • Maintenance risk is reduced but not gone. 2.0 adds PHP 8 support and drops the brittle forced dependency; the package is still a thin single-class wrapper with a small dependent count. Still audit on PHP 8.2/8.3 for dynamic-property and nullable-parameter deprecations in Whoops 2.x and middleware/utils, not just in this package.
  • Extension points worth knowing: Run::pushHandler, PrettyPageHandler::addDataTable, setEditor, setPageTitle, and custom HandlerInterface implementations for Slack/Sentry/log channels — all configured on the Run you pass in, not on the middleware. Response shape is controlled at the PSR-17 layer or by forking.
  • Fork-friendly by design. Because the surface area is one class and one constructor argument, pinning a fork with a withStatus()/withHeader() fluent addition is still cheap if you need it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route