Menu

Reference / Http

Request

finalclass
final class Request

One parsed HTTP request, as the handler receives it. Read-only.

The public properties are readonly. Query and cookie parsing is lazy and cached: reading five query parameters scans the query string once. The path is percent-decoded and ..-collapsed (so it can be used to look files up); target keeps the raw request-target for code that needs the wire form.

$server->serve(function (Request $req): Response {
    $name = $req->query('name', 'world');
    return (new Response())->text("hello $name\n");
});

The server refuses malformed requests before the handler runs (see Server), so a handler only ever sees a request that is well-formed HTTP/1.0 or 1.1.

Example

// Everything a handler can read from the request.

use Http\{Request, Response, Server};
use function Async\{async, spawn};

// Bind a loopback listener on a free port (no fixed port in the example).
$listener = stream_socket_server('tcp://127.0.0.1:0');
stream_set_blocking($listener, false);
$name = stream_socket_get_name($listener, false);
$port = (int) substr($name, strrpos($name, ':') + 1);

/** One request on a fresh connection; returns the raw response. */
function fetch(int $port, string $request, string $body = ''): string
{
    $c = fsockopen('127.0.0.1', $port);
    fwrite($c, $request . "Connection: close\r\n\r\n" . $body);
    $raw = '';
    while (($chunk = fread($c, 8192)) !== '' && $chunk !== false) {
        $raw .= $chunk;
    }
    fclose($c);
    return $raw;
}

$server = Server::onListener($listener)->acceptWait(0.02);

async(function () use ($server, $port) {
    spawn(fn () => $server->serve(function (Request $req): Response {
        $out = [
            'method' => $req->method,
            'path' => $req->path,
            'target' => $req->target,
            'query string' => $req->queryString,
            'version' => $req->version,
            'name' => $req->query('name', '-'),
            'page' => $req->query('page', '1'),
            'queryArray' => json_encode($req->queryArray()),
            'user-agent' => $req->header('User-Agent', '-'),
            'content type' => $req->contentType(),
            'content length' => $req->contentLength(),
            'cookie sid' => $req->cookie('sid', '-'),
            'post title' => $req->postArray()['title'] ?? '-',
            'body' => $req->body(),
            'keep-alive' => var_export($req->isKeepAlive(), true),
            'is POST' => var_export($req->is(Http\Method::Post), true),
            'secure' => var_export($req->secure, true),
        ];
        $text = '';
        foreach ($out as $k => $v) {
            $text .= "$k: $v\n";
        }
        return (new Response())->text($text);
    }));

    $form = 'title=Hello+World&x=1';
    $raw = fetch(
        $port,
        "POST /a%2Fb/../c?name=Ada&tags[]=x&tags[]=y HTTP/1.1\r\n"
        . "Host: localhost\r\nUser-Agent: demo/1.0\r\nCookie: sid=abc123; theme=dark\r\n"
        . "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n"
        . 'Content-Length: ' . strlen($form) . "\r\n",
        $form,
    );
    [$head, $body] = explode("\r\n\r\n", $raw, 2);
    echo strtok($head, "\r\n"), "\n", $body;
    $server->stop();
});

Output

HTTP/1.1 200 OK
method: POST
path: /a/c
target: /a%2Fb/../c?name=Ada&tags[]=x&tags[]=y
query string: name=Ada&tags[]=x&tags[]=y
version: 1.1
name: Ada
page: 1
queryArray: {"name":"Ada","tags":["x","y"]}
user-agent: demo/1.0
content type: application/x-www-form-urlencoded
content length: 21
cookie sid: abc123
post title: Hello World
body: title=Hello+World&x=1
keep-alive: false
is POST: true
secure: false

See also: Response, Server, Headers

Methods

__construct()Creates a request value.
header()A request header value.
query()One query-string parameter, decoded (+ is a space).
queries()All query-string parameters as a flat, last-wins map of decoded names to decoded values.
queryArray()The query string in php's $_GET shape.
postArray()The form fields of the body in php's $_POST shape.
multipartFailed()Whether a buffered multipart/form-data body could not be parsed.
multipart()The parts of a streamed multipart/form-data body, one at a time.
allFiles()Every uploaded file of a buffered multipart/form-data body, in wire order, including f[] names.
files()The first uploaded file for each field name, keyed by field.
filesArray()The uploads in php's $_FILES shape: six columns (name, full_path, type, tmp_name, error, size), with nested names such as f[] or u[avatar] transposed per column.
cookie()One cookie from the Cookie header, urldecoded.
cookies()All cookies of the Cookie header as a map of names to urldecoded values, last-wins.
body()The request body as a string.
stream()The body reader for a streamed body, otherwise null.
hasBody()Whether the request carries a body (buffered and non-empty, or streamed).
contentLength()The declared Content-Length, or -1 when the request did not declare one (a chunked body never does — its size is only known once it is read).
contentType()The media type without its parameters, lowercased.
methodEnum()The method as a {@see Method}, or null for one outside RFC 9110.
is()Whether the request method is this one.
isKeepAlive()Whether the client allows the connection to be reused after this message.

__construct()#source

function __construct(
    public /** The raw method token — `GET`, but also `PROPFIND`. */string $method,
    public /** The request-target exactly as it arrived, still percent-encoded. */string $target,
    public /** Decoded and `..`-collapsed path. `%2F` IS a separator here — read
         *  {@see $target} when that distinction matters. */string $path,
    public /** Raw query string, no leading `?`, still percent-encoded. */string $queryString,
    public /** `1.1` or `1.0`. */string $version,
    public Headers $headers,
    public /** The body, when it was small enough to buffer; '' when streamed. */string $bodyRaw,
    public /** True when the body was left on the wire for {@see stream}. */bool $streamed,
    public string $remoteAddr,
    public /** True when the connection is TLS. */bool $secure,
    public /** The socket's own peer address, always — {@see $remoteAddr} may come
         *  from a trusted proxy's header. */string $peerAddr = '',
    public /** `X-Forwarded-Port` from a trusted proxy, else '' — RFC 7239 has no
         *  port field of its own; the port inside a `Forwarded: for=` value
         *  is part of the client address and is discarded. */string $forwardedPort = '',
    public /** php's `max_input_vars`: pairs past this many are dropped by
         *  {@see queryArray} and {@see postArray}. */int $maxInputVars = 1000,
    int $maxFileUploads = 20,
    int $uploadMaxFilesize = 2097152,
    private /** Present only for a streamed body. */?\Buffer\Reader $reader = null
)

Creates a request value. The server builds these while parsing; construct one yourself only in tests.

Parameters

$method /** The raw method token — `GET`, but also `PROPFIND`. */string
The raw method token, for example GET or PROPFIND.
$target /** The request-target exactly as it arrived, still percent-encoded. */string
The request-target exactly as it arrived, still percent-encoded.
$path /** Decoded and `..`-collapsed path. `%2F` IS a separator here — read * {@see $target} when that distinction matters. */string
The decoded, ..-collapsed path. %2F is a separator here; read $target when the difference matters.
$queryString /** Raw query string, no leading `?`, still percent-encoded. */string
The raw query string, without the leading ?.
$version /** `1.1` or `1.0`. */string
1.1 or 1.0.
$headers Headers
The request header fields.
$bodyRaw /** The body, when it was small enough to buffer; '' when streamed. */string
The buffered body; empty when the body is streamed.
$streamed /** True when the body was left on the wire for {@see stream}. */bool
Whether the body was left on the wire for stream() instead of buffered.
$remoteAddr string
The client address as ip:port. Behind a trusted proxy this is the address the proxy reported (no port).
$secure /** True when the connection is TLS. */bool
Whether the connection is TLS (or a trusted proxy reported https).
$peerAddr /** The socket's own peer address, always — {@see $remoteAddr} may come * from a trusted proxy's header. */string = ''
The socket's own peer address, always. remoteAddr may come from a proxy header, this never does.
$forwardedPort /** `X-Forwarded-Port` from a trusted proxy, else '' — RFC 7239 has no * port field of its own; the port inside a `Forwarded: for=` value * is part of the client address and is discarded. */string = ''
X-Forwarded-Port from a trusted proxy, otherwise an empty string.
$maxInputVars /** php's `max_input_vars`: pairs past this many are dropped by * {@see queryArray} and {@see postArray}. */int = 1000
php's max_input_vars: pairs beyond this many are dropped by queryArray() and postArray().
$maxFileUploads int = 20
php's max_file_uploads: file parts beyond this many are dropped.
$uploadMaxFilesize int = 2097152
php's upload_max_filesize in bytes: a larger file part is kept with ERR_INI_SIZE and no temporary file.
$reader /** Present only for a streamed body. */?\Buffer\Reader = null
The body reader, present only for a streamed body.

query()#source

function query(string $k, string $d = ''): string

One query-string parameter, decoded (+ is a space).

The query is flat and last-wins: ?a[]=1 is the key a[], and for a repeated key the last value is returned. Use queryArray() for php's nested $_GET shape.

Parameters

$k string
Parameter name.
$d string = ''
Returned when the parameter is absent.

Returns

string — The value, or $d.

queries()#source

function queries(): array<string,string>

All query-string parameters as a flat, last-wins map of decoded names to decoded values.

Returns

array<string,string> — flat, last-wins

queryArray()#source

function queryArray(): array<int|string,mixed>

The query string in php's $_GET shape.

Names nest exactly like parse_str(): a[]=1&b[x]=2 gives ['a' => ['1'], 'b' => ['x' => '2']], . and spaces in a name become _, and a canonical decimal key becomes an int. At most max_input_vars pairs (default 1000, Server::maxInputVars()) are read; the rest are dropped silently.

Returns

array<int|string, mixed> — The nested array.

postArray()#source

function postArray(): array<int|string,mixed>

The form fields of the body in php's $_POST shape.

Filled from an application/x-www-form-urlencoded body, or from the fields of a buffered multipart/form-data body (a multipart body is parsed on the first call). Any other content type gives an empty array. Names nest like queryArray() and the same max_input_vars cap applies.

Returns

array<int|string, mixed> — The nested array.

Throws

\LogicException
when the body is a streamed multipart body; read it with multipart()

multipartFailed()#source

function multipartFailed(): bool

Whether a buffered multipart/form-data body could not be parsed.

The server answers such a request with 400 before the handler runs, so a handler normally sees false. Always false for a streamed body and for other content types.

Returns

bool — true if the body is multipart and malformed.

multipart()#source

function multipart(): \Generator

The parts of a streamed multipart/form-data body, one at a time.

Only available when the server streams bodies (Server::streamBodies()) and the body is larger than maxBodySize. Each Part is read from the connection as the handler consumes it: no temporary file, nothing buffered beyond one read. The generator owns the body, so call this once and do not mix it with allFiles() or postArray().

The generator is lazy: a malformed body throws RuntimeException('malformed multipart') while you iterate, not when you call multipart(). If the handler lets it escape, the server answers 500 (or calls onError).

Returns

\Generator<int, Part> — The parts in wire order.

Throws

\LogicException
on first iteration, when the body is buffered (use allFiles()) or was already consumed

See also: Part

allFiles()#source

function allFiles(): array<int,UploadedFile>

Every uploaded file of a buffered multipart/form-data body, in wire order, including f[] names.

The body is parsed on the first call. Each file was written to a temporary file that is deleted when the request ends unless you call UploadedFile::moveTo(). At most max_file_uploads files (default 20, Server::maxFileUploads()) are kept. Without a multipart body the result is empty.

Returns

array<int, UploadedFile> — The files in wire order.

Throws

\LogicException
when the body is a streamed multipart body; read it with multipart()

See also: UploadedFile

files()#source

function files(): array<string,UploadedFile>

The first uploaded file for each field name, keyed by field. Use allFiles() when a field can carry several files.

Returns

array<string, UploadedFile> — Field name to file.

Throws

\LogicException
when the body is a streamed multipart body

filesArray()#source

function filesArray(): array<int|string,mixed>

The uploads in php's $_FILES shape: six columns (name, full_path, type, tmp_name, error, size), with nested names such as f[] or u[avatar] transposed per column. A part with a filename= but no name= is filed under a running integer key, as php does.

Returns

array<int|string, mixed> — The nested array.

Throws

\LogicException
when the body is a streamed multipart body

cookies()#source

function cookies(): array<string,string>

All cookies of the Cookie header as a map of names to urldecoded values, last-wins.

Returns

array<string,string> — Cookie name to value.

body()#source

function body(): string

The request body as a string.

Bodies up to Server::maxBodySize() (8 MiB by default) are buffered here. A chunked body is always buffered. For a streamed body this is an empty string; read stream() instead.

Returns

string — The body bytes.

Example

// JSON in, JSON out: there is no Response::json(), json_encode() and json_decode() do the work.

use Http\{Request, Response, Server};
use function Async\{async, spawn};

// Bind a loopback listener on a free port (no fixed port in the example).
$listener = stream_socket_server('tcp://127.0.0.1:0');
stream_set_blocking($listener, false);
$name = stream_socket_get_name($listener, false);
$port = (int) substr($name, strrpos($name, ':') + 1);

/** One request on a fresh connection; returns the raw response. */
function fetch(int $port, string $request, string $body = ''): string
{
    $c = fsockopen('127.0.0.1', $port);
    fwrite($c, $request . "Connection: close\r\n\r\n" . $body);
    $raw = '';
    while (($chunk = fread($c, 8192)) !== '' && $chunk !== false) {
        $raw .= $chunk;
    }
    fclose($c);
    return $raw;
}

$server = Server::onListener($listener)->acceptWait(0.02);

async(function () use ($server, $port) {
    spawn(fn () => $server->serve(function (Request $req): Response {
        if ($req->contentType() !== 'application/json') {
            return (new Response(415))->text("send application/json\n");
        }
        $data = json_decode($req->body(), true);
        if (!is_array($data) || !isset($data['name'])) {
            return (new Response(400))->type('application/json')->body(json_encode(['error' => 'name required']));
        }
        return (new Response(201))
            ->type('application/json')
            ->body(json_encode(['greeting' => 'hello ' . $data['name'], 'method' => $req->method]));
    }));

    $send = function (string $type, string $json) use ($port): void {
        $raw = fetch(
            $port,
            "POST /users HTTP/1.1\r\nHost: localhost\r\nContent-Type: $type\r\nContent-Length: " . strlen($json) . "\r\n",
            $json,
        );
        [$head, $body] = explode("\r\n\r\n", $raw, 2);
        echo strtok($head, "\r\n"), ' ', trim($body), "\n";
    };
    $send('application/json', '{"name":"Ada"}');
    $send('application/json', '{"nope":1}');
    $send('text/plain', 'Ada');
    $server->stop();
});

Output

HTTP/1.1 201 Created {"greeting":"hello Ada","method":"POST"}
HTTP/1.1 400 Bad Request {"error":"name required"}
HTTP/1.1 415 Unsupported Media Type send application/json

stream()#source

function stream(): ?\Buffer\Reader

The body reader for a streamed body, otherwise null.

The server streams a body only when Server::streamBodies(true) is on and the declared Content-Length exceeds maxBodySize; the reader's budget is that length. A body the handler does not read is drained before the connection is reused.

Returns

?\Buffer\Reader — A reader, or null when the body was buffered.

hasBody()#source

function hasBody(): bool

Whether the request carries a body (buffered and non-empty, or streamed).

Returns

bool — true if there is a body to read.

contentLength()#source

function contentLength(): int

The declared Content-Length, or -1 when the request did not declare one (a chunked body never does — its size is only known once it is read).

Returns

int — The declared length in bytes, or -1.

contentType()#source

function contentType(): string

The media type without its parameters, lowercased.

Returns

string — For example application/json; empty when there is no Content-Type.

methodEnum()#source

function methodEnum(): ?Method

The method as a {@see Method}, or null for one outside RFC 9110.

Returns

?Method — The matching Method case, or null for a token such as PROPFIND. Matching is case-sensitive.

is()#source

function is(Method $m): bool

Whether the request method is this one. Compared to the raw token, so it is case-sensitive.

Parameters

$m Method
The method to test for.

Returns

bool — true on a match.

isKeepAlive()#source

function isKeepAlive(): bool

Whether the client allows the connection to be reused after this message.

HTTP/1.1 is persistent unless it sends Connection: close; HTTP/1.0 is not unless it sends Connection: keep-alive. The server also closes for other reasons (Response::close(), Server::keepAliveMax(), shutdown), so this is the client's wish only.

Returns

bool — true if the client wants to keep the connection open.

View source