Requests, Input and Responses
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05. Every example runs in
the Bookshelf application used across this guide. Illuminate\Http\Request extends Symfony HttpFoundation’s
Request, and the responses extend Symfony’s Response: the PHP superglobals and header() calls of
HTTP Requests and Responses are wrapped in objects, which
also makes them testable. Validation of that input is on
Validation and Form Requests.
Getting the request
Type-hint Illuminate\Http\Request in a controller action or route closure; route parameters come after it, in any
order. The request() helper returns the same object anywhere else. Information about the request itself:
<?php
// app/Http/Controllers/InputDemoController.php (excerpt)
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class InputDemoController extends Controller
{
public function about(Request $request): array // GET /about/{section}, named about.show
{
return [
'path' => $request->path(),
'is' => $request->is('about/*'),
'routeIs' => $request->routeIs('about.*'),
'url' => $request->url(),
'fullUrl' => $request->fullUrl(),
'method' => $request->method(),
'ip' => $request->ip(),
'bearer' => $request->bearerToken() !== null,
'wantsJson' => $request->wantsJson(),
'prefers' => $request->prefers(['text/html', 'application/json']),
'accepts_csv' => $request->accepts(['text/csv']),
];
}
}
$ curl -s -H 'Accept: application/json' -H 'Authorization: Bearer abc' 'localhost:8000/about/history?x=1'
{"path":"about\/history","is":true,"routeIs":true,"url":"http:\/\/localhost:8000\/about\/history",
"fullUrl":"http:\/\/localhost:8000\/about\/history?x=1","method":"GET","ip":"127.0.0.1","bearer":true,
"wantsJson":true,"prefers":"application\/json","accepts_csv":false}
$ curl -s -H 'Accept: text/html,application/json;q=0.9' localhost:8000/about/history
{... "bearer":false,"wantsJson":false,"prefers":"text\/html","accepts_csv":false}
header('X-Name', 'default'), hasHeader(), host(), schemeAndHttpHost(), isMethod('post') and ajax()
complete the set. ip() and the scheme are only correct behind a load balancer when trusted proxies are configured
(Middleware). wantsJson() and expectsJson() decide whether exceptions and
validation errors render as JSON or as a redirect.
Retrieving input
input() reads the query string and the body (form fields or JSON) together, with dot notation for nested values.
Typed accessors convert as they read, which removes most (int) casts and isset() checks:
<?php
// app/Http/Controllers/InputDemoController.php (excerpt), GET /find
use App\Enums\Format; // enum Format: string { case Ebook = 'ebook'; ... }
use Illuminate\Http\Request;
function search(Request $request): array
{
return [
'q' => $request->string('q')->trim()->lower()->value(), // Stringable
'page' => $request->integer('page', 1), // "abc" -> 0, missing -> 1
'in_stock' => $request->boolean('in_stock'), // "1", "true", "on", "yes"
'since' => $request->date('since', 'Y-m-d')?->toDateString(),
'loan' => $request->interval('loan', 'day')?->forHumans(), // Laravel 13
'format' => $request->enum('format', Format::class)?->name,
'formats' => array_map(fn (Format $f) => $f->value, $request->enums('formats', Format::class)),
'filters' => $request->only(['author', 'year']),
'has_q' => $request->has('q'),
'filled_q' => $request->filled('q'),
'missing_year' => $request->missing('year'),
];
}
$ curl -s 'localhost:8000/find?q=%20%20Kindred%20&page=abc&in_stock=yes&since=2024-02-30&loan=14&format=ebook&formats[]=ebook&formats[]=vinyl&formats[]=paperback&author=Butler&year='
{"q":"kindred","page":0,"in_stock":true,"since":"2024-03-01","loan":"2 weeks","format":"Ebook",
"formats":{"0":"ebook","2":"paperback"},"filters":{"author":"Butler","year":null},
"has_q":true,"filled_q":true,"missing_year":false}
$ curl -s localhost:8000/find
{"q":"","page":1,"in_stock":false,"since":null,"loan":null,"format":null,"formats":[],"filters":[],
"has_q":false,"filled_q":false,"missing_year":true}
Source: Laravel — Retrieving Input and Input Presence
The run shows why the typed accessors are conveniences, not validation:
-
integer('page', 1)returns the default only when the key is missing;page=abcgives0. -
date('since', 'Y-m-d')accepted the impossible2024-02-30and rolled it over to 1 March, PHP’sDateTime::createFromFormat()behaviour. Validate withdate_format:Y-m-dfirst. -
enum()returnsnullfor an unknown value andenums()drops unknown values but keeps the original keys, so the list["ebook","vinyl","paperback"]became the JSON object{"0":"ebook","2":"paperback"}. Wrap it inarray_values(). -
year=becamenull(see input normalisation below), sohas('year')is true butfilled('year')false. -
interval()is new in Laravel 13 and throwsInvalidArgumentExceptionon a malformed duration.
query('page') reads the query string only, post() the body only, all() everything, only()/except() a
subset, has(['a', 'b']) and hasAny() test presence, and whenHas('q', fn ($q) => ...) / whenFilled() run a
callback only when the value is there. merge() and mergeIfMissing() add values, typically in
prepareForValidation().
JSON input and normalisation
With Content-Type: application/json, the decoded body is available through the same input() calls, and *
wildcards collect array values. Two global middleware, TrimStrings and ConvertEmptyStringsToNull, rewrite every
string of the input, JSON included:
<?php
// app/Http/Controllers/InputDemoController.php (excerpt), POST /inbox
use Illuminate\Http\Request;
function store(Request $request): array
{
return [
'all' => $request->all(),
'json_title' => $request->input('book.title'),
'tags' => $request->input('book.tags.*'),
'isJson' => $request->isJson(),
'except' => $request->except(['book.tags']),
];
}
$ curl -s -H 'Content-Type: application/json' -d '{"book":{"title":" Kindred ","tags":["sf","classic"],"note":""}}' localhost:8000/inbox
{"all":{"book":{"title":"Kindred","tags":["sf","classic"],"note":null}},"json_title":"Kindred",
"tags":["sf","classic"],"isJson":true,"except":{"book":{"title":"Kindred","note":null}}}
$ curl -s -d 'book[title]=Dawn&book[tags][]=sf' localhost:8000/inbox
{"all":{"book":{"title":"Dawn","tags":["sf"]}},"json_title":"Dawn","tags":["sf"],"isJson":false,
"except":{"book":{"title":"Dawn"}}}
The demo route uses withoutMiddleware(PreventRequestForgery::class) so that curl can post without a CSRF token;
real forms send the token (Sessions, Cookies and Flash
Data). Normalisation is configured in bootstrap/app.php with $middleware->trimStrings(except: [...]) and
convertEmptyStringsToNull(except: [fn (Request $r) => $r->is('webhooks/*')]). Excluding fields matters for
passwords and for signed webhook payloads, whose signature covers the raw body; read the raw body with
$request->getContent() in that case.
Old input, cookies, files and PSR-7
back()->withInput() flashes the submitted input to the session, so the form can be refilled after a failed
submission with old('title') (in Blade, value="{{ old('title') }}"). Validation does it automatically. Flashed
data lives for exactly one more request, as the second call below shows:
$ curl -s -c jar -b jar -o /dev/null -w '%{http_code} %{redirect_url}\n' -H 'Referer: http://localhost:8000/r/flashed' -d 'title=Dawn' localhost:8000/r/back
302 http://localhost:8000/r/flashed
$ curl -s -c jar -b jar localhost:8000/r/flashed
{"status":"Saved","old_title":"Dawn"}
$ curl -s -c jar -b jar localhost:8000/r/flashed
{"status":null,"old_title":null}
Source: Laravel — Old Input
$request->cookie('name') returns a decrypted cookie value: the web group’s EncryptCookies encrypts every cookie
the application sets and ignores (treats as absent) cookies it cannot decrypt, so a value set by JavaScript must be
listed in $middleware->encryptCookies(except: [...]). $request->file('cover') returns an UploadedFile
(isValid(), extension() from the MIME type, store('covers', 's3')); uploads are covered on
File Uploads and Images and
File Storage and Images.
Code written against PSR-7 can receive a PSR-7 request instead, once the bridge and an implementation are installed:
$ composer require symfony/psr-http-message-bridge nyholm/psr7
<?php
// routes/catalogue.php
use Illuminate\Support\Facades\Route;
use Psr\Http\Message\ServerRequestInterface;
Route::get('/psr7', function (ServerRequestInterface $request) {
return ['class' => $request::class, 'query' => $request->getQueryParams(), 'ua' => $request->getHeaderLine('User-Agent')];
});
$ curl -s -A 'curl-demo' 'localhost:8000/psr7?shelf=A3'
{"class":"Nyholm\\Psr7\\ServerRequest","query":{"shelf":"A3"},"ua":"curl-demo"}
Source: Laravel — PSR-7 Requests
Responses: strings, arrays, JSON, headers and cookies
A route returns a string (sent as text/html), an array, an Eloquent model or collection (sent as JSON), a view, or a
response object built with response(). The object API sets status, headers and cookies:
<?php
// app/Http/Controllers/ResponseDemoController.php (excerpt)
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class ResponseDemoController extends Controller
{
public function headers(): Response // GET /r/headers
{
return response('Shelved', 201)
->header('Content-Type', 'text/plain; charset=UTF-8')
->withHeaders(['X-Shelf' => 'A3', 'Cache-Control' => 'no-store'])
->cookie('last_shelf', 'A3', 60); // minutes; encrypted by EncryptCookies
}
public function json(): JsonResponse // GET /r/json
{
return response()->json(['title' => 'Kindred', 'price' => 12.0], 200, [], JSON_PRESERVE_ZERO_FRACTION);
}
}
$ curl -s -D - localhost:8000/r/headers
HTTP/1.1 201 Created
Cache-Control: no-store, private
Content-Type: text/plain; charset=UTF-8
X-Shelf: A3
Set-Cookie: last_shelf=eyJ…; expires=Mon, 05 Oct 2026 21:03:59 GMT; Max-Age=3600; path=/; httponly; samesite=lax
Shelved
$ curl -s localhost:8000/r/json
{"title":"Kindred","price":12.0}
Three details from the run: Cache-Control gains private (Symfony adds it unless public is set); the cookie
value is encrypted, not A3; and ->cookie('last_shelf', 'A3', minutes: 60) with a named argument fails with a
500 ("Unknown named parameter $minutes"), because the response’s cookie() method forwards func_get_args(). Pass
the arguments positionally, or build the cookie with the global cookie() helper, which does accept named arguments,
and pass that object. Without JSON_PRESERVE_ZERO_FRACTION, 12.0 is sent as 12. The
cache.headers middleware alias sets Cache-Control and ETag on a route group
(->middleware('cache.headers:public;max_age=3600;etag')).
Redirects
redirect('/books'), redirect()->route('books.show', $book), to_route('books.show', $book),
redirect()->action([BookController::class, 'show'], $book), back() and redirect()->away('https://…') all
return a 302 by default (pass 301 for a permanent one). with('status', 'Saved') flashes data and
withInput() flashes the input, as in the old-input example above, where the POST answered:
<?php
// app/Http/Controllers/ResponseDemoController.php (excerpt), POST /r/back
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
function back(Request $request): RedirectResponse
{
return back()->withInput()->with('status', 'Saved'); // flashed for the next request only
}
back() uses the Referer header (falling back to the previous URL stored in the session), which is why the curl
call above sent one. A redirect target taken from user input (?next=) is an open redirect: allow only relative
paths or a list of hosts.
Views, files and downloads
view('books.show', ['book' => $book]) or response()->view(...) renders Blade
(Blade Templates). For files:
| Call | Sends |
|---|---|
|
The file inline ( |
|
The file as an attachment ( |
|
The same from a filesystem disk, including S3. |
|
Generated content as an attachment, written as it is produced, never held in memory. |
Source: Laravel — File Downloads and Streamed Downloads
response()->file(public_path('favicon.ico')) in a new application returned Content-Type: application/x-empty:
the skeleton’s favicon.ico is a zero-byte placeholder, and the type is detected from the content, not the
extension. Files served this way pass through PHP; static and large files are better served by the web server or a
CDN (X-Sendfile/X-Accel-Redirect, or a signed S3 URL).
Responsable objects and response macros
A class implementing Illuminate\Contracts\Support\Responsable can be returned from a controller, and Laravel calls
its toResponse(). It packages a response format for reuse, here a streamed CSV export:
<?php
// app/Http/Responses/BookCsv.php
namespace App\Http\Responses;
use Illuminate\Contracts\Support\Responsable;
use Symfony\Component\HttpFoundation\StreamedResponse;
/** A Responsable: a controller returns it, Laravel calls toResponse(). */
class BookCsv implements Responsable
{
public function __construct(private iterable $books) {}
public function toResponse($request): StreamedResponse
{
return response()->streamDownload(function () {
$out = fopen('php://output', 'w');
fputcsv($out, ['isbn', 'title'], escape: '');
foreach ($this->books as $book) {
fputcsv($out, [$book->isbn, $book->title], escape: '');
}
}, 'books.csv', ['Content-Type' => 'text/csv']);
}
}
// in ResponseDemoController: public function export(): BookCsv
// { return new BookCsv(Book::orderBy('id')->limit(3)->cursor()); }
$ curl -s -D - localhost:8000/r/export
HTTP/1.1 200 OK
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename=books.csv
isbn,title
978-0-8070-8305-4,Kindred
978-1-5387-3218-7,"Parable of the Sower"
978-0-06-051275-6,"The Dispossessed"
Source: Laravel — Streamed Downloads
cursor() reads one row at a time, so the export stays flat in memory for any table size; escape: '' avoids the
PHP 8.4 deprecation of fputcsv()'s default escape character
(Files, Streams and I/O). A response macro adds a method to the
response() factory, registered in a service provider’s boot():
<?php
// app/Providers/AppServiceProvider.php, in boot()
use Illuminate\Support\Facades\Response;
Response::macro('caps', fn (string $value) => Response::make(strtoupper($value)));
// routes: Route::get('/r/caps', fn () => response()->caps('shelved')); -> SHELVED
Source: Laravel — Response Macros
Streamed JSON and server-sent events
response()->streamJson() encodes a JSON document while iterating over its lazy parts, and
response()->eventStream() turns a generator into a text/event-stream response for the browser’s EventSource
(both documented since Laravel 11):
<?php
// app/Http/Controllers/ResponseDemoController.php (excerpt)
use App\Models\Book;
use Illuminate\Http\StreamedEvent;
function streamJson() // GET /r/stream-json
{
return response()->streamJson(['books' => Book::orderBy('id')->limit(3)->cursor()->map->only('id', 'title')]);
}
function events() // GET /r/events
{
return response()->eventStream(function () {
foreach (['queued', 'importing', 'done'] as $step) {
yield new StreamedEvent(event: 'import', data: ['step' => $step]);
usleep(200_000);
}
});
}
$ curl -sN localhost:8000/r/stream-json
{"books":[{"id":1,"title":"Kindred"},{"id":2,"title":"Parable of the Sower"},{"id":3,"title":"The Dispossessed"}]}
$ curl -sN -D - localhost:8000/r/events
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, private
X-Accel-Buffering: no
event: import
data: {"step":"queued"}
event: import
data: {"step":"importing"}
event: import
data: {"step":"done"}
event: update
data: </stream>
Source: Laravel — Streamed JSON Responses and Event Streams (SSE)
The stream ends with an update event whose data is </stream>, which the @laravel/stream-react/-vue hooks
(useEventStream) use to know the stream is complete; a hand-written EventSource client should close on it rather
than let the browser reconnect. X-Accel-Buffering: no stops Nginx from buffering the stream. Each open stream holds
a PHP worker for its whole duration, so long-lived streams suit FrankenPHP or Octane better than a small PHP-FPM pool;
for many clients, broadcasting with Reverb (Broadcasting and Reverb)
scales further.
Testing requests and responses
<?php
// tests/Feature/RequestResponseTest.php
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('casts query input to types', function () {
$this->getJson('/find?q=%20Kindred%20&page=abc&in_stock=yes&format=ebook')
->assertJson(['q' => 'kindred', 'page' => 0, 'in_stock' => true, 'format' => 'Ebook']);
});
it('flashes status and old input for exactly one request', function () {
$this->from('/r/flashed')->post('/r/back', ['title' => 'Dawn'])
->assertRedirect('/r/flashed')
->assertSessionHas('status', 'Saved')
->assertSessionHasInput('title', 'Dawn');
});
it('streams a CSV download from a Responsable', function () {
$this->seed();
$response = $this->get('/r/export')->assertDownload('books.csv');
expect($response->streamedContent())->toStartWith("isbn,title\n");
});
$ php artisan test --filter=RequestResponse
PASS Tests\Feature\RequestResponseTest
✓ it casts query input to types 0.16s
✓ it flashes status and old input for exactly one request 0.01s
✓ it streams a CSV download from a Responsable 0.03s
Tests: 3 passed (7 assertions)
Duration: 0.26s
Source: Laravel — HTTP Tests
from() sets the previous URL that back() uses, and streamedContent() reads a streamed body that getContent()
would return as false.
Where older books and tutorials are outdated
-
The
Inputfacade (Input::get('title')) was removed in Laravel 6; use the injectedRequestorrequest(). -
Casting by hand (
(int) $request->input('page'),$request->input('flag') === 'on') (Stauffer ch. 7) is replaced bystring(),boolean(),date()andenum()(Laravel 9),integer()andenums()(11) andinterval()(13), though validation still has to come first. -
Hand-rolled SSE loops with
ob_flush()andflush()inresponse()->stream()are replaced byeventStream(), and large JSON exports bystreamJson()(both documented since Laravel 11). -
A service provider per concern (
RouteServiceProvider,EventServiceProvider, a provider for response macros) is no longer the starting point: a new application has onlyAppServiceProvider, where macros are registered inboot(). -
redirect()->route()everywhere can be shortened toto_route()(Laravel 9).