Unexpected “403 Forbidden” error due to a Laravel routing clash

I was tripped up by a pretty basic error in Laravel yesterday that I was very surprised I’d never encountered in 12 years of using Laravel. It all began when I finally got some time to launch a mapping feature on one of my side projects.

The problem didn’t appear on my local development machine, only on the production server. Ouch.

An NGINX 403 forbidden error page
An NGINX 403 forbidden error page.

In a typical Laravel/NGINX setup, a rewrite rule sends all requests to index.php so Laravel’s router can handle them. This rule first checks whether the requested path matches an existing file or folder. If it does, NGINX serves that file or folder directly instead of passing the request to Laravel. This approach is useful for serving static assets like JavaScript efficiently, without Laravel processing overhead. But it can cause problems: if you create a public folder with the same name as one of your routes, the folder will take precedence and your route will become inaccessible, returning a 403 Forbidden page instead.

This was the cause of my problem. I had a route, ‘maps’ which loads and page that loads some GeoJSON.

Route::view('/maps', 'pages.maps')->name('maps');

I had also created a folder to store GeoJSON files in /public called maps. NGINX correctly returned a 403 because the default configuration forbids accessing contents of folders.

The quick solution was to rename the folder to map-data. Problem solved.

The offending folder that matched the route

I hadn’t encountered this problem before because I usually create the standard symlink between public and storage by running the appropriate artisan command at the beginning of each project.

php artisan storage:link