skip to content

In Laravel, what do the global TrimStrings and ConvertEmptyStringsToNull middleware do to request input, and how do you exclude a field or request?

level: middleimportance: must knowfreq 56%

answer

  1. query string, form body and JSON
  2. Str::trim, including invisible characters
  3. password fields are never trimmed
  4. empty string becomes null
  5. trimStrings(except:) and convertEmptyStringsToNull(except:)

basics

~10 s

TrimStrings strips leading and trailing whitespace from every string input except password fields; ConvertEmptyStringsToNull then turns '' into null. Exclude keys with $middleware->trimStrings(except: [...]), and skip whole requests with closures passed to either configurator.

solid answer

~30 s

Both run on the global stack and rewrite the query string, the form body and the JSON body before routing. `TrimStrings` applies `Str::trim()`, which also strips Unicode whitespace and invisible characters, to every string value, skipping `password`, `password_confirmation` and `current_password`. `ConvertEmptyStringsToNull` runs next and replaces `''` with `null`, so a whitespace-only field arrives as `null`. The practical effects: optional fields need the `nullable` rule, and `$request->input('note')` is `null`, not `''`. To exclude, use `bootstrap/app.php`: `$middleware->trimStrings(except: ['ballot_note', fn (Request $r) => $r->is('webhooks/*')])` takes key patterns and skip-closures, while `convertEmptyStringsToNull(except: [...])` takes closures only. Route parameters, headers, cookies and files are untouched.

code

php · 16 lines
php
<?php

use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->trimStrings(except: [
        'ballot_note',
        fn (Request $request) => $request->is('webhooks/*'),
    ]);

    $middleware->convertEmptyStringsToNull(except: [
        fn (Request $request) => $request->is('webhooks/*'),
    ]);
})

go deeper

for a junior

Recall that input is trimmed and blank strings become null before your controller runs, which is why optional fields need nullable.

for a middle

Explain which bags are rewritten, the default password exclusions, and the difference between key patterns and skip-closures in the configurators.

for a senior

Anticipate the knock-on effects on validation, nullable columns and webhook handling, and prefer targeted exclusions over removing the middleware.

for a principal

Treat input normalisation as a cross-cutting contract that the whole codebase relies on, and require review before changing it globally.

## Two input normalisers on the global stack Laravel's default global stack ends with two middleware that rewrite user input before any route or controller sees it: 1. `Illuminate\Foundation\Http\Middleware\TrimStrings` 2. `Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull` Both extend `TransformsRequest`, which walks three parameter bags and rewrites each value in place: - the **query string** (`$request->query`), - the **JSON body** when the request is JSON, - otherwise the **form body** (`$request->request`). Nested arrays are walked recursively, with keys joined by dots (`options.0.label`). Route parameters, headers, cookies and uploaded files are **not** touched. ## TrimStrings Every string value is passed through `Str::trim()`. Unlike PHP's `trim()`, `Str::trim()` also removes Unicode whitespace and invisible characters such as zero-width spaces, which users paste from word processors more often than you would expect. Three keys are never trimmed by default, because a password may legitimately start or end with a space: - `password` - `password_confirmation` - `current_password` ## ConvertEmptyStringsToNull Every value that is exactly `''` becomes `null`. Because it runs **after** `TrimStrings`, a field containing only spaces is first trimmed to `''` and then converted to `null`. ## What this changes for a voting app A poll-creation form has a required `title`, an optional `description` and a list of `options`: | Submitted value | After TrimStrings | After ConvertEmptyStringsToNull | |---|---|---| | `" Best lunch spot "` | `"Best lunch spot"` | `"Best lunch spot"` | | `" "` | `""` | `null` | | `""` | `""` | `null` | | `"0"` | `"0"` | `"0"` | Consequences you meet in practice: - The `required` rule rejects a whitespace-only title, because it now arrives as `null`. - An optional field must be validated as `nullable`, or a rule such as `string` fails on the `null`. - A database column receiving an optional field must accept `NULL`, or the insert fails. - Code that compares `$request->input('description') === ''` never matches. ## Excluding fields and requests Both behaviours are configured in `bootstrap/app.php` through the `Middleware` configurator: ```php ->withMiddleware(function (Middleware $middleware): void { $middleware->trimStrings(except: [ 'ballot_note', // key pattern fn (Request $request) => $request->is('webhooks/*'), // skip whole request ]); $middleware->convertEmptyStringsToNull(except: [ fn (Request $request) => $request->is('webhooks/*'), ]); }) ``` - `trimStrings(except:)` accepts both **strings** and **closures**. Strings are key patterns matched with `Str::is()`, so wildcards and dotted nested keys work; they add to the default password keys rather than replacing them. Closures receive the request and, when they return `true`, skip trimming for the entire request. - `convertEmptyStringsToNull(except:)` accepts **closures only**; there is no per-key exclusion. - Webhook endpoints are a common reason to skip both, so the handler sees payload values exactly as the sender produced them; signature checks themselves should hash the raw body, which these middleware never modify. ## Where they sit and why that matters Both run on the **global** stack, before routing, so they apply to web routes, API routes and even requests that end in a 404. `TrimStrings` runs before `ConvertEmptyStringsToNull`, and both run after `ValidatePostSize`. Everything downstream, including route middleware, form requests and controllers, sees the normalised values, because they replace the originals in the request's parameter bags; only the raw body from `$request->getContent()`, and PHP's own superglobals, still hold what was sent. ## Testing the behaviour - A feature test that posts `['title' => ' ']` to the poll-creation route should receive a validation error for `title`, proving the whitespace was trimmed and converted. - A test posting `['ballot_note' => ' keep ']` after excluding the key should see the spaces preserved in the stored record. ## Removing them entirely If an application genuinely wants raw input everywhere, `$middleware->remove(ConvertEmptyStringsToNull::class)` takes the class off the global stack. That is rarely a good idea: validation rules, nullable columns and form handling across the codebase are usually written assuming the normalised values.

  • Why does a Laravel form request's 'description' => 'string|max:500' rule fail when the user leaves the field blank?
    `ConvertEmptyStringsToNull` has turned the blank field into `null`, and `null` is not a string. Adding `nullable` tells the validator that `null` is acceptable, so the remaining rules are skipped for it.
  • Does TrimStrings change the value returned by $request->getContent() in Laravel?
    No. It rewrites the parsed parameter bags, including the decoded JSON bag, but the raw body string is left as it arrived. That is why webhook signature checks that hash the raw content are unaffected.

saying these in an interview costs you the question

  • Believes blank optional fields reach the controller as empty strings
  • Thinks TrimStrings also trims password fields
  • Assumes route parameters and headers are trimmed too
  • Tries to exclude a single key from ConvertEmptyStringsToNull by name
  • Thinks Str::trim behaves exactly like PHP's trim()