Some middleware has to run at a specific point in Laravel's default stack. A middleware that reads the client IP needs to run after TrustProxies. A middleware that reads the session needs to run after StartSession, and one that changes how route models resolve needs to run before SubstituteBindings.
The withMiddleware() callback in bootstrap/app.php could add middleware to the start or the end of the global stack and of a group, but not next to a specific built-in middleware. Laravel 13.40 adds four methods for that, and lets the existing replace() methods take an array.
Inserting Next to a Built-in Middleware
The insertBefore() and insertAfter() methods work on the global middleware stack. The first argument is the middleware to find, and the second is one middleware or an array of them:
use App\Http\Middleware\BlockBannedIps; use App\Http\Middleware\SanitizeInput; use Illuminate\Foundation\Http\Middleware\TrimStrings; use Illuminate\Http\Middleware\TrustProxies; ->withMiddleware(function (Middleware $middleware) { $middleware->insertAfter(TrustProxies::class, BlockBannedIps::class); $middleware->insertBefore(TrimStrings::class, SanitizeInput::class); })
The insertBeforeInGroup() and insertAfterInGroup() methods do the same inside a middleware group. They take the group name first:
use App\Http\Middleware\SetLocaleFromSession; use Illuminate\Session\Middleware\StartSession; ->withMiddleware(function (Middleware $middleware) { $middleware->insertAfterInGroup('web', StartSession::class, SetLocaleFromSession::class); })
The group methods work on any group, including ones you define with $middleware->group().
Replacing With an Array
The new methods are built on a smaller change from the same contributor. The replace() and replaceInGroup() methods now accept an array, and the array takes the place of the original middleware in the stack. To insert next to a middleware, include it in the array:
// Run StripCrossOriginReferer after TrustProxies $middleware->replace(TrustProxies::class, [ TrustProxies::class, StripCrossOriginReferer::class, ]); // Replace one middleware with two $middleware->replace(Foo::class, [Bar::class, Baz::class]);
The web() and api() helpers pass their replace argument to replaceInGroup(), so arrays work there too:
$middleware->web(replace: [ StartSession::class => [StartSession::class, SetLocaleFromSession::class], ]);
Example: Locale From the Session
Some applications store a user's language in the session and translate URL slugs. A /fr/articles/{article} route might resolve the article by its French slug, with a model that reads the current locale in resolveRouteBinding():
class Article extends Model { public function resolveRouteBinding($value, $field = null) { return $this->where('slug->'.app()->getLocale(), $value)->firstOrFail(); } }
The locale has to be set after the session starts and before SubstituteBindings resolves the model. The default web group runs in this order:
EncryptCookiesAddQueuedCookiesToResponseStartSessionShareErrorsFromSessionPreventRequestForgerySubstituteBindings
Before 13.40, neither helper put the middleware in the right place. With web(append: ...), the middleware runs after SubstituteBindings, so the model is looked up with the default locale and a French slug returns a 404. With web(prepend: ...), it runs before StartSession, and $request->session() throws Session store not set on request.
The workaround was to redefine the whole group:
$middleware->group('web', [ EncryptCookies::class, AddQueuedCookiesToResponse::class, StartSession::class, SetLocaleFromSession::class, ShareErrorsFromSession::class, PreventRequestForgery::class, SubstituteBindings::class, ]);
That copy does not follow framework changes. Laravel 12 used ValidateCsrfToken in this group, and Laravel 13 uses PreventRequestForgery, so an application that copied the Laravel 12 group would keep the old class after upgrading. The copied list also drops auth.session, which Laravel adds to the group when you call $middleware->authenticateSessions().
With 13.40, you only name the middleware you are adding:
->withMiddleware(function (Middleware $middleware) { $middleware->insertAfterInGroup('web', StartSession::class, SetLocaleFromSession::class); })
The middleware reads the session and sets the locale:
class SetLocaleFromSession { public function handle(Request $request, Closure $next): Response { if ($locale = $request->session()->get('locale')) { App::setLocale($locale); } return $next($request); } }
The resulting group is EncryptCookies, AddQueuedCookiesToResponse, StartSession, SetLocaleFromSession, ShareErrorsFromSession, PreventRequestForgery, SubstituteBindings.
Example: The Client IP
The global stack has the same problem. A middleware that checks $request->ip() against a list of banned addresses gets the load balancer's address if it runs before TrustProxies, and prepend() puts it before TrustProxies. With append(), it runs after HandleCors, the maintenance mode check, and the input middleware.
Insert it directly after TrustProxies instead:
$middleware->insertAfter(TrustProxies::class, BlockBannedIps::class);
class BlockBannedIps { public function handle(Request $request, Closure $next): Response { if (in_array($request->ip(), config('security.banned_ips'), true)) { abort(403); } return $next($request); } }
Further Reading
- Slow Job Alerts and Replica Commit IDs in Laravel 13.40
- How to Configure Middleware in Laravel
- Managing Proxy Trust in Laravel Applications
Jamie York contributed the array support for replace() in #62230 and the insert methods in #62261.