Docs / Requests

Requests

On this page

Introduction

Most actions never touch the request: they declare the fields they take and receive them checked; see Actions. Take the request itself when you need what a signature cannot say, such as a header, a cookie, an uploaded file or the raw body.

The request extends the PSR-7 server request. It is immutable: a method starting with with returns a changed copy.

Getting the request

Add a parameter typed as the request, beside any others:

use Crab\Contracts\Http\Request;

Route::post('/webhooks/stripe', static function (Request $request): array {
    $signature = $request->getHeaderLine('Stripe-Signature');
    $payload = (string) $request->getBody();

    return ['received' => true];
});

There is no request() helper. A worker serves overlapping requests, so the request is passed, never looked up; see Request lifecycle.

Input

$page = $request->query('page', 1);            // from the query string
$name = $request->input('name');               // a form field or a JSON key
$all = $request->getParsedBody();              // the whole body, parsed
$raw = (string) $request->getBody();           // the body as it was sent

input() and getParsedBody() read JSON objects and arrays, urlencoded forms and multipart/form-data on first use. Malformed JSON and multipart bodies are answered 400; urlencoded forms use PHP's permissive parse_str().

Each JSON object may have at most 1,000 members, including repeated names. This bound applies inside nested objects too; exceeding it is answered 413 before the object is decoded. Lists of smaller objects may contain more than 1,000 items, within server.max_body. Declared Length bounds on typed lists are checked before their items are validated.

PHP's max_input_vars can silently truncate query strings and urlencoded forms; multipart forms exceeding their field or part limits are answered 413.

Prefer declared fields to input(): a declared field is validated, an input() value is whatever the client sent.

Headers and cookies

$request->getHeaderLine('Accept');             // "" when absent
$request->hasHeader('Authorization');
$request->cookie('theme', 'light');            // a cookie, or the default when it was not sent
$request->getMethod();                         // "POST"
$request->getUri()->getPath();                 // "/users/7"

Route parameters and attributes

Route parameters reach an action by name. Middleware reads them, and anything other middleware added, as attributes:

$id = $request->getAttribute('id');                     // the route parameter
$user = $request->getAttribute('user');                 // set by authentication middleware
$requestId = $request->getAttribute('request_id');      // set by RequestId

Attributes are how middleware hands something to what runs after it; see Middleware.

The client's address

$request->ip();          // "203.0.113.7"

It is the address at the other end of the connection. An IPv4 client of a server listening on [::] is given as its IPv4 address, not as ::ffff:203.0.113.7. Behind a load balancer or a reverse proxy that is the proxy's, for every request. Say which proxies stand in front of the application, and the address becomes the client's:

// app.php
use Crab\Http\TrustedProxies;

->withMiddleware(prepend: [new TrustedProxies(['10.0.0.0/8', '127.0.0.1'])])

A proxy passes the client's address on in X-Forwarded-For, a header anyone can also write themselves. So it is believed only on a request that comes from a proxy you named, and read from the right: the first address that is not one of your proxies. Without the middleware the header is never read.

[!WARNING] Name only proxies you run. Trusting an address a client can connect from lets that client say it is anyone, which defeats a limit or a block by address.

Uploaded files

A file the client uploads is a field of the body, like any other, typed as PSR-7's UploadedFileInterface in the action's input class. It is checked before the action runs:

// app/Http/NewAvatar.php
namespace App\Http;

use Crab\Validation\FileSize;
use Crab\Validation\MediaType;
use Psr\Http\Message\UploadedFileInterface;

final readonly class NewAvatar
{
    public function __construct(
        #[FileSize(max: 2 * 1024 * 1024)]
        #[MediaType('image/png')]
        public UploadedFileInterface $avatar,
        public int $rotate = 0,
    ) {
    }
}
// app/Actions/ReplaceAvatar.php
namespace App\Actions;

use App\Http\NewAvatar;
use Crab\Contracts\Storage\Disk;

final readonly class ReplaceAvatar
{
    public function __construct(private Disk $disk)
    {
    }

    public function handle(User $user, NewAvatar $input): array
    {
        $this->disk->write("avatars/{$user->id}.png", $input->avatar->getStream(), 'image/png');

        return ['avatar' => "avatars/{$user->id}.png"];
    }
}
// routes/web.php
Route::post('/me/avatar', ReplaceAvatar::class)->middleware(TokenAuth::class);

The client sends a multipart/form-data form with a file field named avatar. The form's other fields are fields of the class too, read from text as a query string's are, so int $rotate takes rotate=90. The file is named after the logged-in user, not an id in the URL, so no one can replace someone else's picture; file storage keeps it on a disk, local or on S3.

$request->file('avatar') returns the same file, or null, and getUploadedFiles() the whole tree, for an action that reads the request itself. A form may have at most 1,000 parts, and its fields together 1 MiB.

[!WARNING] getClientFilename() and getClientMediaType() are what the client claims. Never use the name as a path, and check the kind with #[MediaType] before trusting the type.

Large uploads

A request body may be 1 MiB unless you raise the limit, and a client has 30 seconds to send it:

// config/server.php
return [
    'max_body' => 50 * 1024 * 1024,
    'body_timeout' => 120,
];

These are the server.max_body and server.body_timeout settings, and they bound every request; #[FileSize] is checked once a file has arrived. The server keeps small bodies in memory and spills large ones to disk; moveTo() copies an upload without loading the whole file, while getBody() materializes the whole request body. Copying a large file does take the worker's time while it lasts.

A client that has gone

$request->cancellation() fires when the client disconnects before the answer is ready. Pass it to slow work that is pointless without a reader, such as a call to another API:

$report = $this->http->request('GET', $url, cancellation: $request->cancellation());

See HTTP client.