Menu
Reference
Request
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: falseSee 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
GETorPROPFIND. $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.%2Fis a separator here; read$targetwhen 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`. */string1.1or1.0.$headersHeaders- 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. $remoteAddrstring- 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.
remoteAddrmay 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-Portfrom 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 byqueryArray()andpostArray(). $maxFileUploadsint = 20- php's
max_file_uploads: file parts beyond this many are dropped. $uploadMaxFilesizeint = 2097152- php's
upload_max_filesizein bytes: a larger file part is kept withERR_INI_SIZEand no temporary file. $reader/** Present only for a streamed body. */?\Buffer\Reader = null- The body reader, present only for a streamed body.
header()#source
function header(string $n, string $d = ''): string
A request header value. The name is case-insensitive.
A field sent more than once comes back comma-joined.
Parameters
$nstring- Header name.
$dstring = ''- Returned when the header is absent.
Returns
string — The value, or $d.
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
$kstring- Parameter name.
$dstring = ''- 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
cookie()#source
function cookie(string $k, string $d = ''): string
One cookie from the Cookie header, urldecoded.
Last-wins for a repeated name.
Parameters
$kstring- Cookie name.
$dstring = ''- Returned when the cookie is absent.
Returns
string — The value, or $d.
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/jsonstream()#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
$mMethod- 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.