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, TokenMismatchException
s 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.
1// app/Exceptions/Handler.php 2// Extra detail omitted for brevity 3 4namespace App\Exceptions; 5 6use Exception; 7use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; 8use Illuminate\Session\TokenMismatchException; 9 10class Handler extends ExceptionHandler11{12 protected $dontReport = [13 TokenMismatchException::class,14 ];15 16 public function render($request, Exception $e)17 {18 if ($e instanceof TokenMismatchException) {19 // If another route for this request exists, return a 405 Method Not Allowed20 // Else if a route does not exist for this request, return a 404 Not Found21 }22 23 // Laravel will otherwise return the TokenMismatchException24 return parent::render($request, $e);25 }26}
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:
1use Illuminate\Support\Facades\Route; 2 3class Handler extends ExceptionHandler 4{ 5 public function render($request, Exception $e) 6 { 7 if ($e instanceof TokenMismatchException) { 8 Route::getRoutes()->match($request); 9 }10 11 // Laravel will otherwise return the TokenMismatchException12 return parent::render($request, $e);13 }14}
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.