skip to content

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()?

level: seniorimportance: should knowfreq 30%

answer

  1. remove() and replace() for the global stack
  2. web(remove:, replace:) for the groups
  3. use() replaces the whole global list
  4. trustHosts() is ignored once use() is set
  5. copy the list from the framework source

basics

~10 s

Use $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 s

For 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
<?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

for a junior

Recall remove() and replace() for the global stack and web(remove:, replace:) for the web group.

for a middle

Explain the assembly order: base list, replacements, prepends and appends, then removals, and what that means for remove().

for a senior

Avoid use() unless you will maintain the list, and know the ValidatePathEncoding and trustHosts() traps it introduces.

for a principal

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