Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Building Web Apps with Laravel  Module 3 – Requests, routing and middleware

Requests, input and uploaded files

Read Laravel 13 input with typed helpers, see what trimming and null conversion change, negotiate JSON, trust proxies and hosts, and treat uploads as claims.

  • Beginner
  • 10 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

What you will learn

  • Read query, form and JSON input with input, query, post and json, and say which reader sees which part
  • Explain how TrimStrings and ConvertEmptyStringsToNull change input, and what integer, boolean, date and enum return for odd values
  • Predict whether a request gets a JSON or an HTML answer from its Accept header and its path
  • Configure trusted proxies and trusted hosts so the application reads the real caller and refuses a forged host
  • Check the name and type of an uploaded file as claims and read what its bytes say

Before you start

What you’ll learn · Before you start

What you will learn

  • Read query, form and JSON input with input, query, post and json, and say which reader sees which part
  • Explain how TrimStrings and ConvertEmptyStringsToNull change input, and what integer, boolean, date and enum return for odd values
  • Predict whether a request gets a JSON or an HTML answer from its Accept header and its path
  • Configure trusted proxies and trusted hosts so the application reads the real caller and refuses a forged host
  • Check the name and type of an uploaded file as claims and read what its bytes say

Before you start

On this page

A request for /fees?qty=12abc&status=refunded reaches a Laravel controller as a quantity of 12 and a status of null, and nothing complains. Both values passed through helpers that are polite about bad input, and a filter page that trusts them quietly shows the wrong list. Knowing what each reader returns for a value that is wrong tells you where a check must be added.

This lesson follows input from the request to your code. You will see which reader sees the query string, the form and a JSON body, what two global middleware do before your code starts, how a request chooses between an HTML and a JSON answer, how the application learns the caller’s real address when a load balancer sits in front, and why the name of an uploaded file proves nothing. Every example runs on Laravel 13.34.

Input has three doors and several readers

A request can carry data in the query string after the question mark, in a form body, or in a JSON body. The Request object has a reader for each, and one reader that merges them. Post a form with a query string, then a JSON body with the same query string, and ask every reader what it sees:

routes/web.php of the probe project PHP · input-probe/routes/web.php
<?php

use App\Enums\FeeStatus;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// The probes are scripts with no browser, so they have no forgery token. Real form routes keep PreventRequestForgery.

// Which door each helper reads: the query string, the form body, or a JSON body.
Route::post('/sources', fn (Request $request) => response()->json([
    'input(name)' => $request->input('name'),
    'input(batch)' => $request->input('batch'),
    'query(name)' => $request->query('name'),
    'query(batch)' => $request->query('batch'),
    'post(name)' => $request->post('name'),
    'post(batch)' => $request->post('batch'),
    'json(qty)' => $request->json('qty'),
    'input(qty)' => $request->input('qty'),
    'only(name)' => $request->only('name'),
]))->withoutMiddleware(PreventRequestForgery::class);

// What the controller receives after TrimStrings and ConvertEmptyStringsToNull, read with the typed helpers.
Route::get('/filters', fn (Request $request) => response()->json([
    'raw query' => $request->server->get('QUERY_STRING'),
    'input(name)' => $request->input('name'),
    'input(note)' => $request->input('note'),
    'string(name)->upper()' => $request->string('name')->upper()->toString(),
    'integer(qty)' => $request->integer('qty'),
    'boolean(notify)' => $request->boolean('notify'),
    'date(from)' => $request->date('from', '!Y-m-d', 'Asia/Kolkata')?->toIso8601String(),
    'enum(status)' => $request->enum('status', FeeStatus::class)?->value,
]));
A form post and a JSON post, each with ?batch=7 Python · sources_session.py
from session import run

# One form post and one JSON post, each with a query string: which reader sees which part.
run(
    """
php sources.php
""",
    app=True,
    overlay=("input-probe",),
)

Output

$ php sources.php
== form body "name=Meera" with ?batch=7
  input(name)  "Meera"
  input(batch) "7"
  query(name)  null
  query(batch) "7"
  post(name)   "Meera"
  post(batch)  null
  json(qty)    null
  input(qty)   null
  only(name)   {"name":"Meera"}
== JSON body {"qty":2,"name":"Meera"} with ?batch=7
  input(name)  "Meera"
  input(batch) "7"
  query(name)  null
  query(batch) "7"
  post(name)   null
  post(batch)  null
  json(qty)    2
  input(qty)   2
  only(name)   {"name":"Meera"}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sources_session.py

In the form post, input('name') and post('name') both return Meera, but query('name') is null, because the name was not in the query string. input('batch') returns 7 and post('batch') returns null, since the batch came from the query. input reads the query and the body together, while query and post read one door each.

The JSON post shows a difference. json('qty') reads the JSON body and input('qty') returns the same 2, because for a JSON request input includes the JSON. post('name') is null here, as the name is in a JSON body and not a form. That is why a controller that must serve both forms and API clients should use input, and one that wants only the query string should say query. only('name') returns an array with just the named keys.

Passing everything the request carries

Model::create($request->all()) hands every key a caller sends to the model, including keys you never put in a form. Use only([...]) for the fields you expect, or the validated data of a form request, and keep the model’s fillable list as a second guard.

Checkpoint

Check: Which reader sees the query, the form and the JSON

  1. One question A form posts name=Meera to /sources?batch=7. What do post('batch') and input('batch') return?

    Choose one answer.

    Show the answer: One question

    Answer: null and "7"

    post reads the form body, query reads the query string, and input reads both, so a value from the query string shows only in query and input.

Two middleware clean input, and typed readers choose the type

Query and form values are text. Before your action runs, two global middleware from the first lesson of this module change that text: TrimStrings removes spaces from the ends of every string, and ConvertEmptyStringsToNull turns an empty string into null. Then a typed reader turns what is left into a type. Follow one query string through:

Interactive · stage by stage From text to values Step through one query string and see what each stage does to the values.
From the raw query string to the controller's values, step 6 of 6. Showing Raw query string, TrimStrings (name is Meera), ConvertEmptyStrings (note is null), Typed helpers (null, 12 or a 500), Your action. In focus: Your action.Raw query stringname has spacesTrimStringsname is MeeraConvertEmptyStringsnote is nullTyped helpersnull, 12 or a 500Your actiongets clean values

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The text as it arrived. The visitor's browser sent name with two spaces before and one after Meera, and note with nothing after the equals sign. Nothing has changed it yet.
  2. Spaces are trimmed. TrimStrings removes spaces from the ends of every string in the query and the body. The name becomes Meera before any of your code reads it.
  3. Empty becomes null. ConvertEmptyStringsToNull turns each empty string into null, so the empty note arrives as null and not as an empty string. A rule or a column that expects text must allow null.
  4. A helper chooses the type. Query values are always text. integer('qty') gives the number 12, boolean('notify') reads on as true, date('from', '!Y-m-d', 'Asia/Kolkata') builds a date at midnight in India, and enum('status', FeeStatus::class) gives the enum case paid.
  5. Bad value, not always an error. An unknown status gives null from enum(), qty=12abc gives 12 from integer(), and a date in the wrong format throws and ends in a 500. Check the value before you trust a helper to reject it.
  6. The action gets the values. The action runs with trimmed text, nulls instead of empty strings and typed values. It still has to decide what a missing or odd value means.

Run it for three query strings, with a route that prints what the readers return:

Padded text, an empty field, odd numbers, a date and an enum Python · typed_session.py
from session import run

# Padded text, an empty field, odd numbers, flags, a date and an enum value, read with the typed helpers.
run(
    """
php typed.php 'name=%20%20Meera%20&note=&qty=12&notify=on&from=2026-01-05&status=paid' 'qty=12abc&notify=off&from=2026-01-05&status=refunded' 'qty=-3&notify=0&from=05/01/2026'
""",
    app=True,
    overlay=("input-probe",),
)

Output

$ php typed.php 'name=%20%20Meera%20&note=&qty=12&notify=on&from=2026-01-05&status=paid' 'qty=12abc&notify=off&from=2026-01-05&status=refunded' 'qty=-3&notify=0&from=05/01/2026'
== ?name=%20%20Meera%20&note=&qty=12&notify=on&from=2026-01-05&status=paid
  input(name)           "Meera"
  input(note)           null
  string(name)->upper() "MEERA"
  integer(qty)          12
  boolean(notify)       true
  date(from)            "2026-01-05T00:00:00+05:30"
  enum(status)          "paid"
== ?qty=12abc&notify=off&from=2026-01-05&status=refunded
  input(name)           null
  input(note)           null
  string(name)->upper() ""
  integer(qty)          12
  boolean(notify)       false
  date(from)            "2026-01-05T00:00:00+05:30"
  enum(status)          null
== ?qty=-3&notify=0&from=05/01/2026
  the action threw, so the visitor got status 500

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typed_session.py

In the first request the name " Meera " arrived as Meera, the empty note as null, qty=12 as the number 12, notify=on as true, the date as midnight in India, and status=paid as the enum case paid. The second request is the interesting one. qty=12abc became 12, because integer casts the text the way PHP does and keeps the digits it finds. notify=off became false. status=refunded is not a case of the enum, so enum returned null and did not complain. The missing name came back as null and string('name') as an empty text.

The third request used from=05/01/2026 with the format !Y-m-d, and the action threw, so the visitor got a 500. date with a format parses strictly and throws for text that does not fit. The ! at the start of the format resets every field the text does not give to its starting value, which gives midnight. Without it, PHP fills the time of day from the clock, and two requests for the same date would get different times.

The typed readers are forgiving for numbers and booleans, strict for dates, and silent for enums. A filter that must reject an unknown status needs to notice the null, for example by checking has('status') and returning 422 when the enum is null. The module on validation, later in this track, covers rules that do this in one line.

Key idea

A typed reader never says that the input was wrong. It returns a value in its type, and that can be 12 for 12abc or null for an unknown enum case, so the check that decides is yours.

Checkpoint

Check: Trimming, null conversion and the typed readers

  1. Question 1 of 2 A form has an optional text box "note", and the visitor leaves it empty. What does input('note') return in the action?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: null

    The empty string becomes null before the action runs. An optional field therefore needs a rule that allows null.

  2. Question 2 of 2 A request carries qty=12abc and status=refunded, and the enum has the cases due, paid and late. What do integer('qty') and enum('status', FeeStatus::class) return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 12 and null

    Neither reader rejects the value, so a page that must refuse a bad quantity or status has to check for it.

The Accept header and the path decide JSON or HTML

A browser asks for HTML. A mobile app asks for JSON. Laravel decides which answer to build from the request, in two methods. wantsJson() is true when the first type in the Accept header is JSON. expectsJson() is true in that case too, and also for requests marked as Ajax that accept anything. The skeleton’s bootstrap/app.php uses expectsJson() along with the path, in shouldRenderJsonWhen, so errors of an API come back as JSON:

A route that reports what it was asked for PHP · negotiation-probe/routes/web.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/page', fn (Request $request) => response()->json([
    'path' => $request->path(),
    'wantsJson' => $request->wantsJson(),
    'expectsJson' => $request->expectsJson(),
    'prefers' => $request->prefers(['text/html', 'application/json']),
]));
The same URLs asked for as HTML and as JSON Python · negotiation_session.py
from session import run

# The same unknown URL asked for as a browser and as a JSON client, then under /api.
run(
    """
export APP_DEBUG=false
php negotiate.php
""",
    app=True,
    overlay=("negotiation-probe",),
)

Output

$ export APP_DEBUG=false
$ php negotiate.php
GET /page               application/json  200  application/json  {"path":"page","wantsJson":true,"expectsJson":true,"prefers":"application\/json"}
GET /page               text/html         200  application/json  {"path":"page","wantsJson":false,"expectsJson":false,"prefers":"text\/html"}
GET /nothing-here       (no Accept)       404  text/html         (an HTML error page)
GET /nothing-here       application/json  404  application/json  { "message": "The route nothing-here could not be found." }
GET /api/nothing-here   (no Accept)       404  application/json  { "message": "The route api/nothing-here could not be found." }

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 negotiation_session.py

The first two lines show a route asked for application/json and for text/html: wantsJson and expectsJson are true and false, and prefers picks the type the client listed first among the two the action offers. The third and fourth lines are a URL that no route handles. With no Accept header the 404 is an HTML error page, and with Accept: application/json it is JSON with a message. The last line asks for the same missing URL under /api with no Accept header, and the answer is JSON anyway, because the skeleton’s rule is $request->is('api/*') || $request->expectsJson().

Debug mode was off for this run. With it on, the JSON error would carry a stack trace, as the middleware lesson showed. For a mobile client, send the Accept: application/json header on every call, so that even an error from a route outside /api is not an HTML page that the app cannot parse.

Other readers of the request come from the same object. header('X-Request-Id') reads a header, bearerToken() reads the token of an Authorization: Bearer header, path() returns the path without the host, is('batches/*') matches it against a pattern, and userAgent() and ip() describe the caller. Treat all of them as input from outside: the documentation says IP addresses should be considered untrusted and used for information only.

Checkpoint

Check: JSON or HTML from Accept and the path

  1. One question A mobile app calls GET /api/nothing-here with no Accept header, and no route matches. What does the skeleton answer?

    Choose one answer.

    Show the answer: One question

    Answer: A JSON 404 with a message, because the path starts with api/

    JSON is chosen by the path under api/ or by an Accept header that asks for it, so the app gets JSON either way.

Behind a load balancer, the caller’s address is a claim

In production Pathshala’s application usually sits behind a load balancer or a reverse proxy, and the application sees every connection as coming from the balancer, say 10.0.0.5. The balancer passes the visitor’s address in an X-Forwarded-For header and the scheme it terminated in X-Forwarded-Proto. The application believes those headers only for proxies it has been told to trust. This is the trusted proxy: A load balancer or reverse proxy whose forwarded headers, such as X-Forwarded-For, the application is told to believe. Headers from any other address are ignored. setting, and it is why the rate limiters of the middleware lesson need it. The harness asks one request through a balancer and one from a caller who writes a header of their own, each under three settings:

ip() and isSecure() under three trust settings Python · proxy_session.py
from session import run

# What the application believes about the caller under three trust settings.
run(
    """
php proxy_requests.php
""",
    app=True,
    overlay=("proxy-probe",),
)

Output

$ php proxy_requests.php
== through the balancer
  trust nobody (the default)            ip 10.0.0.5     http
  trust only the balancer, '10.0.0.5'   ip 203.0.113.9  https
  trust everybody, '*'                  ip 203.0.113.9  https
== straight from a liar
  trust nobody (the default)            ip 198.51.100.7 http
  trust only the balancer, '10.0.0.5'   ip 198.51.100.7 http
  trust everybody, '*'                  ip 192.0.2.1    http

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 proxy_session.py

Through the balancer, with the default of trusting nobody, ip() is 10.0.0.5 for everyone and the request counts as plain http. Trusting only 10.0.0.5 gives the visitor’s address 203.0.113.9 and https. Trusting everybody gives the same, which looks right until you read the second block. A caller at 198.51.100.7 who connects directly and writes X-Forwarded-For: 192.0.2.1 is believed only when every caller is trusted. Then ip() returns the made-up address, and a rate limit keyed by address, a log line or a block list all see the lie.

Interactive · message by message The same header under three settings Step through X-Forwarded-For under three settings.
Who is the caller when a proxy is in front?, step 6 of 6. Messages so far: Caller to Application: +fwd headers; Caller to Application: forged header.Callervia load balancerApplicationip: 192.0.2.1+fwd headersforged header

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Through the balancer. The visitor at 203.0.113.9 connects to a load balancer at 10.0.0.5, which opens its own connection to the application and adds X-Forwarded-For with the visitor's address.
  2. No trust, ip is proxy. With no trusted proxy the application ignores the header, so ip() returns 10.0.0.5 for every visitor and isSecure() is false.
  3. Trust the balancer. With trustProxies set to 10.0.0.5, the application believes the header from that address. ip() returns 203.0.113.9 and isSecure() is true.
  4. A liar connects directly. A caller at 198.51.100.7 skips the balancer and writes X-Forwarded-For with somebody else's address 192.0.2.1.
  5. Liar not trusted. Because 198.51.100.7 is not a trusted proxy, the header is ignored and ip() stays 198.51.100.7.
  6. Trust star believes lies. With trustProxies set to the star, every caller is a proxy. The same forged request gives ip() as 192.0.2.1.

You set the proxies in bootstrap/app.php, with $middleware->trustProxies(at: ['10.0.0.5']) for one balancer or a list of addresses or ranges. Use the star only when the application is reachable through the balancer alone, for example when a firewall blocks every other path to it. A balancer that runs on a platform with addresses that change is the usual reason for the star, and then the firewall carries the responsibility.

Checkpoint

Check: Trusted proxies and the caller's address

  1. One question trustProxies is set to the star. A visitor connects directly and sends X-Forwarded-For with someone else's address. What does $request->ip() return?

    Choose one answer.

    Show the answer: One question

    Answer: The address in the header

    Trusting everybody lets any caller choose the address that logs and limits see. Trust the balancer's address unless a firewall guarantees that every request comes through it.

The Host header names the site the caller wanted, and Laravel uses it to build absolute URLs. An attacker who can send a request with Host: evil.example to a password-reset or receipt form may get the application to put that host into the link in the email. The defence is a list of hosts the application answers to. The trusted host: A host name the application agrees to answer to. A request with any other Host header is refused with a 400, so links are never built from a forged name. middleware is off until you switch it on, and it also does nothing in the local environment, so the session sets the environment to production:

Two Host headers, no trusted hosts Python · hosts_before_session.py
from session import run

# A production server, with no trusted hosts configured: the link is built from whatever Host the caller sends.
run(
    """
export APP_ENV=production
export APP_DEBUG=false
php host_requests.php
""",
    app=True,
    overlay=("hosts-probe",),
)

Output

$ export APP_ENV=production
$ export APP_DEBUG=false
$ php host_requests.php
Host: pathshala.example   200  http://pathshala.example/receipts/1042
Host: evil.example        200  http://evil.example/receipts/1042

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hosts_before_session.py

Without a list, the request for evil.example answered 200 and the link in the body begins http://evil.example/. Now register the hosts in bootstrap/app.php:

bootstrap/app.php with trustHosts PHP · hosts-fix/bootstrap/app.php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        // Only these hosts, and subdomains of APP_URL's host, may reach the application.
        $middleware->trustHosts(at: ['pathshala.example']);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();
The same requests with trustHosts Python · hosts_after_session.py
from session import run

# The same two requests once the application lists the hosts it answers to.
run(
    """
export APP_ENV=production
export APP_DEBUG=false
php host_requests.php
""",
    app=True,
    overlay=("hosts-probe", "hosts-fix"),
)

Output

$ export APP_ENV=production
$ export APP_DEBUG=false
$ php host_requests.php
Host: pathshala.example   200  http://pathshala.example/receipts/1042
Host: evil.example        400

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hosts_after_session.py

The real host still answers 200 with its own link, and evil.example answers 400 Bad Request. trustHosts(at: [...]) takes names or patterns, and by default it also trusts subdomains of the host in APP_URL; pass subdomains: false to turn that off. If the list must come from configuration or the database, at accepts a closure.

Defensive setting, tested with a request

Check this on the server you own: send a request with a made-up Host header and expect a 400. A test like this one fails when the list is missing, and it passes after you add it.

Checkpoint

Check: Trusted hosts

  1. One question trustHosts(at: ['pathshala.example']) is set on a production server. What does a request with Host evil.example get?

    Choose one answer.

    Show the answer: One question

    Answer: 400 Bad Request

    Trusted hosts turn a forged Host header into a 400, so a mail link cannot be built from it.

An uploaded file’s name and type are claims by the sender

A file arrives as an UploadedFile. The browser fills in its name and its type, and a caller who is not a browser can write anything there. The probe makes a file whose bytes are PHP source, with a client name receipt.png and a client type image/png, and asks the object both ways:

What the sender claims, and what the bytes say Python · upload_session.py
from session import run

# What a file's own claims say, and what its bytes say.
run(
    """
php upload_facts.php
""",
    app=True,
    overlay=("upload-probe",),
)

Output

$ php upload_facts.php
getClientOriginalName()       receipt.png
getClientOriginalExtension()  png
getClientMimeType()           image/png
getMimeType()                 text/x-php
extension()
getSize()                     28 bytes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 upload_session.py

getClientOriginalName(), getClientOriginalExtension() and getClientMimeType() repeat the claim: a PNG called receipt.png. getMimeType() reads the file’s content and returns text/x-php, and extension(), which comes from that content, has nothing for a PHP source. The two answers disagree, and the second one is the better evidence. Storing the file under its client name, or deciding that it is safe because of its client type, is how an upload becomes a script on the server.

For now the rule is short: never build a path from the client’s name, never trust the client’s type, and read the type from the content. The later module on validation and uploads covers allowed types, size limits and where to store a file.

Checkpoint

Check: Uploaded file names and types are claims

  1. One question A file arrives with client name receipt.png and client type image/png, and getMimeType() says text/x-php. Which do you believe?

    Choose one answer.

    Show the answer: One question

    Answer: getMimeType(), which reads the file's content

    The client fields are claims. Decide from the content, and never build a stored name from the client's name.

Practise

Check these on your own Pathshala project:

  1. Write a students index that reads batch, status (an enum), joined_from (a date, in Asia/Kolkata) and q. Send status=refunded and decide what the page does when enum returns null while the key was present.
  2. Add a feature test that posts the same value as a form and as JSON and asserts that input returns it both times and post only for the form.
  3. Set trustProxies to the address of your own balancer, write a test that sends X-Forwarded-For from an address that is not the balancer, and assert that ip() ignores it.

Then work through the self-check.

Practice that adapts to you

Practise this lesson

16 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 16 Which reader returns a value from a JSON request body?

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: json('qty'), or input('qty')

    A JSON body is read by json, and also by input.

  2. Question 2 of 16 A controller runs Student::create($request->all()). What is the risk?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: A caller can send keys that your form never offered and have them stored

    Use only() or validated data for the fields you expect, and a fillable list on the model as a second guard.

  3. Question 3 of 16 What does $request->only(['name', 'batch']) return when the request carries name, batch and is_admin?

    Choose one answer.

    Show the answer: Question 3 of 16

    Answer: An array with just name and batch

    only is the allowlist reader: the keys you name, nothing else.

  4. Question 4 of 16 A visitor types two spaces before and one after Meera in a name field. What does input('name') return?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: Meera

    TrimStrings is a global middleware that trims every string in the query and the body.

  5. Question 5 of 16 Which values does boolean('notify') read as true?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: 1, true, on and yes

    A checkbox sends on, so boolean('notify') gives true. off, 0 and a missing key give false.

  6. Question 6 of 16 Why does the lesson write date('from', '!Y-m-d', 'Asia/Kolkata') with an exclamation mark?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: So every field the text does not give, such as the time, starts from zero and the result is midnight

    The leading ! makes a date-only text give midnight, which is what a filter by day needs.

  7. Question 7 of 16 A students page has ?status=refunded and enum('status', FeeStatus::class) returns null. What should the page do if the key was present?

    Choose one answer.

    Show the answer: Question 7 of 16

    Answer: Refuse the request, for example with 422, because the value is not a known status

    An unknown enum value arrives as null, so the page must tell it apart from an absent key and answer 422.

  8. Question 8 of 16 When is wantsJson() true?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: When the first type in the Accept header is JSON

    wantsJson reads the Accept header. isJson reads the Content-Type of the body. They are different questions.

  9. Question 9 of 16 A mobile app parses every response as JSON, and a call to a URL outside /api returns an HTML 404 page. What is the cleanest fix on the app side?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: Send Accept: application/json on every call

    The Accept header is how a client says what it can read, and Laravel honours it.

  10. Question 10 of 16 Behind a balancer at 10.0.0.5, with no trusted proxies configured, what does ip() return for a visitor?

    Choose one answer.

    Show the answer: Question 10 of 16

    Answer: 10.0.0.5, the balancer's address, for every visitor

    Without a trusted proxy, every request seems to come from the balancer, which is why limiters keyed by address lock out everybody together.

  11. Question 11 of 16 When is it acceptable to set trustProxies to the star?

    Choose one answer.

    Show the answer: Question 11 of 16

    Answer: When a firewall guarantees that every request reaches the application through the balancer

    The star is safe only when the network makes the balancer the single way in.

  12. Question 12 of 16 The balancer ends the https connection and forwards plain http. Which setting makes isSecure() true and links start with https?

    Choose one answer.

    Show the answer: Question 12 of 16

    Answer: Trusting the balancer, so that X-Forwarded-Proto is believed

    Trusted proxies let the application read the scheme and the address that the visitor really used.

  13. Question 13 of 16 What can a forged Host header do on a server with no trusted hosts?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: Make the application build absolute links, such as the one in a reset email, with the attacker's domain

    A link built from the Host header leads wherever the sender chose, so list the hosts you answer to.

  14. Question 14 of 16 A developer adds trustHosts and sees no 400 for a forged host on their laptop. Why?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: The middleware does nothing in the local environment

    Set APP_ENV to something other than local when you check it, as the lesson's sessions do.

  15. Question 15 of 16 Which of these comes from the sender and not from the file's content?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: getClientOriginalName()

    Methods that start with getClient return what the sender claimed.

  16. Question 16 of 16 A controller stores an upload with the file's getClientOriginalName() as the stored name. What is the risk?

    Choose one answer.

    Show the answer: Question 16 of 16

    Answer: The sender picks the name, so it can overwrite another file or end in an extension that the server runs

    Generate the stored name yourself and keep the client's name, if you need it at all, as plain data.

Interview questions

Warm-up: what do TrimStrings and ConvertEmptyStringsToNull do to input, and what does that mean for validation?

TrimStrings removes spaces from the start and end of every string in the query and the request body, and ConvertEmptyStringsToNull turns every empty string into null. Both are global middleware, so they run on every request before a controller or a form request sees the data. The practical effects are that a name typed with spaces arrives clean, a text box left empty arrives as null and not as an empty string, and a rule or a column that expects a string must allow null for an optional field, otherwise a blank box fails validation. A field where spaces matter can be listed as an exception with trimStrings(except: [...]) in the middleware configuration.

Three longer questions come from these readers:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A staff member opens /students?status=refunded&joined_from=05/01/2026 and sees either the full student list or a server error, depending on the day. Explain what each typed reader does with these values and design the filter so that bad input gets a clear answer.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    After the move to a load balancer, the access log shows one address for all visitors and the login limiter locks out whole classrooms. A colleague proposes trusting every proxy to fix it. Evaluate that and propose what you would set.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A receipt upload checks that getClientMimeType() starts with image/ and stores the file under its original name. A colleague rejects it. Say what is wrong, what you would read instead, and how you would store the file.

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How do input(), query(), post() and json() differ on a Laravel request?

    input() reads the query string and the body together, query() only the query string, post() only a form body, and json() only a JSON body. Choose the reader by where the value is allowed to come from.

  2. What do the TrimStrings and ConvertEmptyStringsToNull middleware do to input?

    Before any controller runs, the first trims spaces from both ends of every string and the second turns every empty string into null. So an empty optional field arrives as null, and its rule and column must allow null.

  3. Why is $request->integer('seats') no substitute for validation?

    Typed readers return a value of their type instead of reporting bad input: integer() keeps the leading digits of 12abc, enum() gives null for an unknown case. Only a date in the wrong format throws. Rejecting a value is your code's decision.

  4. When does a Laravel 13 app send an error, such as a 404, as JSON rather than an HTML page?

    When the request accepts JSON or its path starts with api/, the skeleton's rule. A mobile client should therefore send Accept: application/json on every call.

  5. When should the app believe the client address in X-Forwarded-For?

    Only when the request came through a proxy you trust, such as your own load balancer. Name it rather than trusting every address, because a caller who reaches the app directly can write the header themselves.

  6. Why list the hosts an app answers to?

    The app builds links and redirects from the Host header, so a forged one plants its domain in them. With trusted hosts set, a request for any other host gets a 400.

  7. What does an uploaded file's original name and type tell you?

    Only what the sender claims; both can be anything. The type read from the file's own content is the better evidence.

Key takeaways

  • input reads the query and the body together, query only the query string, post only a form body, and json the JSON body, so choose the reader by where the value is allowed to come from.
  • TrimStrings and ConvertEmptyStringsToNull change every string before your code starts, so an empty field arrives as null and an optional rule has to allow it.
  • Typed readers are lenient: integer keeps the digits of 12abc, enum returns null for an unknown case, and a date in the wrong format throws, so the decision to reject a value stays in your code.
  • A request is answered in JSON when it accepts JSON or its path starts with api/, which is the skeleton’s rule, and a mobile client should send Accept: application/json on every call.
  • The application believes X-Forwarded-For only from trusted proxies, so name your balancer and not the star where a caller can reach the application directly, and list the hosts you answer to so a forged Host header gets a 400.
  • The name and the type that come with an uploaded file are claims by the sender, and the type read from the file’s content is the better evidence.

Terms in this lesson

trusted host
A host name the application agrees to answer to. A request with any other Host header is refused with a 400, so links are never built from a forged name.
trusted proxy
A load balancer or reverse proxy whose forwarded headers, such as X-Forwarded-For, the application is told to believe. Headers from any other address are ignored.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.