Introduction

At my day job, we recently enabled monitoring of our web server logs for 500 level error codes. Subsequent to this, we discovered that we started getting an influx of these errors from our Laravel sites.

This seemed strange, so I dug further to find that due to the layered manner in which middleware is processed, TokenMismatchExceptions were being generated for routes that probably shouldn't have.

The problem

In digging further into this issue, it became clear that as Laravel processes middleware before handling any route logic - and because Laravel 5.1 has CSRF protection applied globally by default - a route that either did not support POST requests or simply did not exist would generate a TokenMismatchException erroneously.

Further to that, because we were not explicitly catching the exception, it was being rendered with the standard 'Whoops' message via the default exception handler.

Solution

My first thought was simply to disable the global CSRF and apply it manually to the routes that actually needed it. I dismissed this in our case, as it means having to remember to add the CSRF middleware to any routes needing it in the future.

The first step in solving the issue, then, was to tell Laravel's exception handler class to not report the TokenMismatchException and instead handle it in some custom manner.

// app/Exceptions/Handler.php
// Extra detail omitted for brevity

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Session\TokenMismatchException;

class Handler extends ExceptionHandler
{
    protected $dontReport = [
        TokenMismatchException::class,
    ];

    public function render($request, Exception $e)
    {
        if ($e instanceof TokenMismatchException) {
            // If another route for this request exists, return a 405 Method Not Allowed
            // Else if a route does not exist for this request, return a 404 Not Found
        }

        // Laravel will otherwise return the TokenMismatchException
        return parent::render($request, $e);
    }
}

We can see, then, that we want to perform two checks in order to determine either that there is another route with a different verb (GET, DELETE, etc) or that the route simply doesn't exist.

Luckily, Laravel provides this functionality via its Route (Illuminate\Support\Facades\Route) facade, in the RouteCollection.

Changing our exception handler class, we add the following:

use Illuminate\Support\Facades\Route;

class Handler extends ExceptionHandler
{
    public function render($request, Exception $e)
    {
        if ($e instanceof TokenMismatchException) {
            Route::getRoutes()->match($request);
        }

        // Laravel will otherwise return the TokenMismatchException
        return parent::render($request, $e);
    }
}

That's it!

Under the hood, the RouteCollection's match method already does the check for alternate verbs using the checkForAlternateVerbs method and throw a MethodNotAllowedHttpException if one exists (and you are not performing an OPTIONS request on the route).

If a route with an alternate verb doesn't exist, the match method will simply throw a NotFoundHttpException.

Conclusion

We have identified that some requests will (perhaps) incorrectly throw a TokenMismatchException and decided to find a better way of handling this.

Within the exception handler, we tell Laravel to not report exceptions of type TokenMismatchException, and if one is thrown we will check its type and look for a matching route for a different verb - throwing a MethodNotAllowedHttpException if one exists, or a NotFoundHttpException if no matching route exists.

This allows us to return appropriate errors to clients accessing those routes, whether malicious or accidental, and not clutter our bug reporting tools with non-critical error messages.