skip to content

In Laravel, how do you exempt one route from a middleware applied to its group, and why can withoutMiddleware() not remove a global middleware?

level: middleimportance: should knowfreq 44%

answer

  1. chained on the route or on Route::
  2. stored as excluded middleware on the route
  3. filtered when the router resolves middleware
  4. the global stack never reaches that filter
  5. parameters must match exactly

basics

~10 s

Chain ->withoutMiddleware(EnsureAgentIsVerified::class) on the route, or wrap routes in Route::withoutMiddleware([...])->group(). The router drops excluded classes when it resolves route middleware; global middleware runs in the kernel before routing, so it is never filtered.

solid answer

~40 s

`->withoutMiddleware(EnsureAgentIsVerified::class)` on a route records the class as excluded; `Route::withoutMiddleware([...])->group(...)` does the same for a block of routes. When the router gathers a route's middleware, it resolves aliases and groups in both lists and rejects any entry that equals an excluded name or is a subclass of an excluded class. That filter lives in the router, which only sees route and group middleware. The global stack runs in the HTTP kernel's pipeline before the router is even called, so `withoutMiddleware()` has nothing to act on; the docs say it only removes route middleware. For a global middleware that one endpoint must skip, move it into a group or let it bail out on a path check. Also note that excluding `role` does not exclude `role:agent`; the exclusion must match the resolved string including parameters.

code

php · 13 lines
php
<?php

use App\Http\Controllers\AgentBookingController;
use App\Http\Controllers\ShareItineraryController;
use App\Http\Middleware\EnsureAgentIsVerified;
use Illuminate\Support\Facades\Route;

Route::middleware(EnsureAgentIsVerified::class)->group(function () {
    Route::get('/agent/bookings', [AgentBookingController::class, 'index']);

    Route::get('/agent/itineraries/{itinerary}/share', ShareItineraryController::class)
        ->withoutMiddleware(EnsureAgentIsVerified::class);
});

go deeper

for a junior

Recall the two forms: ->withoutMiddleware() on a route and Route::withoutMiddleware([...])->group() for a block.

for a middle

Explain that the router filters resolved names at dispatch, why the global stack is out of reach, and the exact-match rule for parameters.

for a senior

Treat exclusions as audit risk: verify them with route:list -v, prefer restructuring groups over many exclusions, and never exempt security middleware silently.

for a principal

Set a convention that exclusions of access-control middleware need review, since a mistyped parameter leaves a check in place and a broad one removes it everywhere.

## The problem withoutMiddleware() solves Route groups make it easy to attach a middleware to many routes at once. On a travel-agency site, every route under `/agent` might require `EnsureAgentIsVerified`. Then one route in that block, say a public share link for an itinerary, must be reachable without it. Moving that route out of the group is one option; **`withoutMiddleware()`** is the other. ## The two forms ```php Route::middleware(EnsureAgentIsVerified::class)->group(function () { Route::get('/agent/bookings', [AgentBookingController::class, 'index']); Route::get('/agent/itineraries/{itinerary}/share', ShareItineraryController::class) ->withoutMiddleware(EnsureAgentIsVerified::class); }); Route::withoutMiddleware([EnsureAgentIsVerified::class])->group(function () { // every route here skips it }); ``` - On a **single route**, `->withoutMiddleware()` accepts a class, an alias, a group name or an array of them. - On a **block of routes**, `Route::withoutMiddleware([...])->group()` applies the exclusion to everything defined inside. - Calls accumulate: two `withoutMiddleware()` calls on one route exclude both lists. ## How the router applies the exclusion `Illuminate\Routing\Route::withoutMiddleware()` stores the names under the route action's `excluded_middleware` key. Nothing is removed at definition time. The work happens in `Router::resolveMiddleware()` each time the route's middleware is gathered: 1. The route's middleware list (its own, its groups', its controller's) is expanded through `MiddlewareNameResolver`: aliases become class names, group names become their member lists, and parameters are kept as `Class:params`. 2. The excluded list is expanded the same way, so excluding an alias or a whole group name works. 3. Each resolved entry is rejected if it is **strictly equal** to an excluded entry, or if it is a real class that is a **subclass** of an excluded class. 4. The survivors are sorted by the middleware priority list and run. Two consequences come straight from step 3: - **Parameters must match.** A group that applies `'role:agent'` resolves to `...\EnsureUserHasRole:agent`. Excluding `'role'` resolves to `...\EnsureUserHasRole`, which is not equal, and the string with `:agent` is not a class name, so the subclass check is skipped. The middleware still runs. Exclude `'role:agent'` to remove it. - **Subclasses are covered.** Excluding a base class also removes middleware that extend it, which is how a test or route can drop every variant of a framework middleware at once. ## Why the global stack is out of reach The HTTP kernel handles a request in two stages: | Stage | What runs | Can `withoutMiddleware()` affect it? | |---|---|---| | Kernel pipeline | The global stack, in registration order | No | | Router dispatch | Route, group and controller middleware, after exclusion and sorting | Yes | The exclusion list is read inside the router, and the global stack has already run by the time the router is called. No route is matched while global middleware runs, so there is not even a route whose exclusions could be consulted. The documentation says it plainly: `withoutMiddleware` can only remove route middleware and does not apply to global middleware. ## What to do instead for a global middleware If one endpoint must skip a middleware that is currently global: - **Move it off the global stack** into the group that actually needs it, then exclude it per route as above. - **Make the middleware skip itself** by checking the path or host early in `handle()` and returning `$next($request)` unchanged. - **Use the framework's own configurators** where a built-in global middleware offers an exception list; that is configured in `bootstrap/app.php` rather than on the route. ## Common mistakes - Calling `$middleware->remove(...)` in `bootstrap/app.php` to exempt one route: `remove()` edits the global stack and never touches route middleware. - Excluding the alias without its parameters, as described above, and assuming the check is gone. - Excluding a whole group such as `web` to drop one class, which also drops the session, cookies and CSRF check for that route. - Stacking many exclusions inside one group: when more routes opt out than opt in, the group is the wrong shape and should be split. ## Checking the result `php artisan route:list -v` prints each route's resolved middleware after exclusion and priority sorting. If the class still appears under the share route, the exclusion did not match, and a parameter mismatch is the usual cause.

  • A route group applies 'role:agent' and one route calls ->withoutMiddleware('role'); does the role check still run?
    Yes. The router compares resolved strings strictly, and `role:agent` resolves to the class name with `:agent` appended, which does not equal the bare class name from `role`. The subclass check does not apply because the string with parameters is not a class. Exclude `'role:agent'` exactly.
  • Can withoutMiddleware() take a group name such as web?
    Yes. The router expands excluded names through the same resolver as the route's own middleware, so a group name becomes its list of classes and every member is rejected. That strips the session, cookies and CSRF check from the route, which is rarely what you want on a browser page.

saying these in an interview costs you the question

  • Believes withoutMiddleware() can skip a middleware registered with append()
  • Excludes 'role' and expects 'role:agent' to be removed too
  • Thinks the exclusion edits the group definition for every route
  • Uses $middleware->remove() in bootstrap/app.php to exempt one route
  • Assumes an alias cannot be used in withoutMiddleware()