Andrea Gandino

Redirect on 404 in Laravel 11

How to programmatically attempt a redirect upon landing on a page not found, in Laravel 11 and above.

Published on Laravel

The other day I felt the need to change the URL of a post on this very website.

Not being fully sold on self-healing URLs for blog posts (I feel like they're better suited for product endpoints), I thought it'd be better to simply set up a redirection system that would allow to redirect any incoming visit on the old URL to the new one.

Initially, I though it would be a pretty straightforward task: simply throw a middleware in the stack, and move on.

Naively, I was wrong.

404 errors are exceptions that are caught when the routing system is not able to find a specific resource. For performance reasons, as with all routes, Laravel tries to avoid having to pass through the chain of configured middlewares altogether, and throwing an exception for non-existing endpoints is a perfect case for an early exit.

Luckily, Laravel allows you to handle exceptions that are being raised during the request lifecycle. In the boostrap/app.php file, you'll find a call to a withExceptions method, accepting a Closure as parameter to do pretty much whatever you want.

The component I originally set out to create as a middleware will be instead a simple handler class, that reads a configuration array of key/value pairs, each representing an origin path, and a destination path.

Long story short, here's the redirection handler component I've written:

// redirects.php config file
[
	'/old-url' => '/new-url',
]

// RedirectionHandler class
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class RedirectionHandler {

	public function handle( Request $request ): Response|null {
		$redirects = config( 'redirects' );
		$path      = Str::of( $request->path() )->start( '/' )->toString();

		if ( in_array( $path, array_keys( $redirects ) ) ) {
			return redirect( $redirects[ $path ] );
		}

		return null;
	}

}

And here's how I've used the RedirectionHandler component in conjunction with the handling of a NotFoundHttpException:

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Http\Request;

->withExceptions( function( Exceptions $exceptions ) {
	$exceptions->render( function( NotFoundHttpException $exception, Request $request ) {
		if ( 404 === $exception->getStatusCode() ) {
			$redirectResponse = ( new RedirectionHandler() )->handle( $request );

			if ( $redirectResponse ) {
				return $redirectResponse;
			}
		}
	} );
} )

To sum it all up, in case of a 404 exception being raised, we have intercepted the rendering of the exception, passed the current request object through our component, and checked if the current request path is configured to redirect somewhere else: if it is, we'll return a new redirect response, otherwise we'll return nothing and let Laravel do its thing, that is display the 404 error page.

Perhaps there's a better and more elegant way to do this, but I'm happy with an end result that simply allows to have redirects explicited as regular entries in a configuration array.