In Laravel 13's bootstrap/app.php, how do you remove or replace a default middleware, and what can go wrong when you redefine the global stack with use()?
answer
- remove() and replace() for the global stack
- web(remove:, replace:) for the groups
- use() replaces the whole global list
- trustHosts() is ignored once use() is set
- copy the list from the framework source
basics
~10 sUse $middleware->remove(Class::class) or replace(Old::class, New::class) for the global stack, and web(remove: [...], replace: [...]) or api(...) for the groups. use([...]) replaces the entire global list, so anything you omit, including trustHosts(), silently stops running.
solid answer
~40 sFor the global stack, `$middleware->remove(ConvertEmptyStringsToNull::class)` drops an entry and `$middleware->replace(TrimStrings::class, TrimVoteInput::class)` swaps one in place, keeping its position. For groups, `web(remove: [...])` and `web(replace: [Old::class => New::class])`, or `removeFromGroup()` and `replaceInGroup()`, do the same. `use([...])` is different: it defines the global list wholesale, replacing the framework's defaults. Two traps follow. First, the list you pass is final: copy it from the docs and you may miss an entry, because the Laravel 13 framework source lists `ValidatePathEncoding` first while the docs' `use()` example omits it. Second, `trustHosts()` only adds `TrustHosts` to the default list, so after `use()` it has no effect unless you include the class yourself. Removals are applied last, so `remove()` also removes a class you `append()`ed.
code
php · 16 lines<?php
use App\Http\Middleware\TrimVoteInput;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
use Illuminate\Session\Middleware\StartSession;
use App\Http\Middleware\StartVoterSession;
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
$middleware->remove(ConvertEmptyStringsToNull::class);
$middleware->replace(TrimStrings::class, TrimVoteInput::class);
$middleware->web(replace: [StartSession::class => StartVoterSession::class]);
})go deeper
Recall remove() and replace() for the global stack and web(remove:, replace:) for the web group.
Explain the assembly order: base list, replacements, prepends and appends, then removals, and what that means for remove().
Avoid use() unless you will maintain the list, and know the ValidatePathEncoding and trustHosts() traps it introduces.
Decide whether the team owns the full stack explicitly or tracks framework defaults, weighing visibility against upgrade drift.
## Three tools of different strength The `Illuminate\Foundation\Configuration\Middleware` object passed to `withMiddleware()` offers three ways to change Laravel's default global stack, from narrow to wholesale: | Method | Effect | Keeps the rest of the defaults | |---|---|---| | `remove(Class::class)` | Drops one or more entries | Yes | | `replace(Old::class, New::class)` | Swaps one entry for another, in the same position | Yes | | `use([...])` | Defines the entire global list | **No**, you restate everything | The groups have their own equivalents: `web(remove: [...], replace: [Old::class => New::class])` and `api(...)` with the same arguments, or the underlying `removeFromGroup($group, ...)` and `replaceInGroup($group, $search, $replace)`. ## remove() and replace() in practice In a voting app, the team wants blank ballot comments stored as empty strings, and a custom trimmer that also collapses internal whitespace in poll option labels: ```php ->withMiddleware(function (Middleware $middleware): void { $middleware->remove(ConvertEmptyStringsToNull::class); $middleware->replace(TrimStrings::class, TrimVoteInput::class); }) ``` - `replace()` substitutes the class at the same position, so ordering relative to other defaults is preserved. The replacement usually **extends** the original, keeping its behaviour and configurator support. - `replace()` only rewrites entries in the base list, the defaults or your `use()` list; it does not touch classes you added with `append()` or `prepend()`. - `remove()` is applied **after** prepends and appends are merged, so it also strips a class you appended elsewhere in the same closure. ## How the global list is assembled When the HTTP kernel is resolved, the configurator builds the global list in this order: 1. Take your `use()` list if you called it, otherwise the framework defaults. `TrustHosts` is included in the defaults only if `trustHosts()` was called. 2. Apply `replace()` substitutions to that list. 3. Merge `prepend()` entries in front and `append()` entries at the end, dropping duplicates. 4. Remove everything named in `remove()`. ## The use() traps `use()` exists for teams that want the whole stack visible in one place. The cost is that the list is now yours to maintain: - **Omissions are silent.** Laravel 13's default list, in the framework's `Configuration\Middleware::getGlobalMiddleware()`, starts with `ValidatePathEncoding`, which rejects paths that are not valid UTF-8, then `InvokeDeferredCallbacks`. The `use()` example in the middleware documentation starts at `InvokeDeferredCallbacks`. Copying the docs example drops the path check without any error. - **trustHosts() stops working.** The `trustHosts()` configurator sets a flag that adds `TrustHosts` to the **default** list. Once `use()` supplies the list, that flag is never consulted, so you must include `\Illuminate\Http\Middleware\TrustHosts::class` yourself. - **Upgrades do not reach you.** When a future release adds a global middleware, apps using `use()` keep the old list until someone notices. - **Configurators still apply to classes you list.** `trustProxies(at:)`, `trimStrings(except:)` and similar calls configure static state on the middleware classes, so they keep working as long as the class is in your list. ## What stays out of scope here Removing or replacing an entry is a sharp tool. Some defaults exist for security or correctness, and each has its own reasons to keep it: - `TrustProxies`: without it, forwarded headers are never evaluated. - `ValidatePostSize`: without it, oversized bodies arrive with empty input. - `EncryptCookies` and `StartSession` in the web group: without them, sessions stop working. Prefer the built-in configurators (`trustProxies()`, `trimStrings(except:)`, `encryptCookies(except:)`, `preventRequestsDuringMaintenance(except:)`) over removal whenever the goal is an exception rather than a different behaviour. ## A complete use() list for Laravel 13 If the team decides to own the whole stack, start from the framework source rather than the docs: ```php $middleware->use([ \Illuminate\Http\Middleware\ValidatePathEncoding::class, \Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class, \Illuminate\Http\Middleware\TrustHosts::class, // only if you want it \Illuminate\Http\Middleware\TrustProxies::class, \Illuminate\Http\Middleware\HandleCors::class, \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ]); ``` Re-check this list against `getGlobalMiddleware()` on every framework upgrade. ## Verifying the result Global middleware does not appear in `php artisan route:list`, which shows route middleware only. A feature test asserting the behaviour, such as blank input staying an empty string, is the reliable way to confirm a removal took effect.
- In Laravel, you call $middleware->append(AuditVotes::class) and later $middleware->remove(AuditVotes::class); does AuditVotes run?No. The configurator merges prepends and appends into the list first and applies removals last, so `remove()` strips the appended entry too, regardless of the order the calls appear in the closure.
- Why prefer extending TrimStrings when you replace it?`replace()` keeps the position but not the behaviour. A subclass keeps the password exclusions and the static `except` and `skipWhen` hooks that `$middleware->trimStrings(except: ...)` configures, so existing configuration keeps working.
saying these in an interview costs you the question
- Copies the docs' use() list and assumes it matches the framework defaults
- Expects trustHosts() to keep working after calling use()
- Thinks remove() only affects the framework's default entries
- Removes a security default when a configurator exception would do
- Checks route:list to confirm a global middleware was removed