Documentation
HTTP reference
Routing, messages, errors, body limits, and file responses.
Use Build your first HTTP application for a runnable
Composer application. This reference describes the Stage\Http classes.
Routes
Application accepts Route objects and HTTP ideas. Each route has an uppercase method, a
literal encoded path or a named path pattern, and a callable accepting Request
and returning Response.
| Entry point | Behavior |
|---|---|
new Route(string $method, string $path, callable $handler) |
Registers an uppercase method and encoded path or pattern |
Route::get(string $path, callable $handler) |
Constructs a GET route |
new Application(Route|Idea ...$entries) |
Expands ideas, builds the routing index, and rejects duplicate method and path shapes |
Duplicate methods for the same path shape fail at startup, including /{id}
and /{slug}. OPTIONS is provided by the application; registering an OPTIONS
route throws InvalidArgumentException.
Named parameters occupy a complete segment, as in /pages/{slug}. A parameter
name starts with a letter and contains letters, digits, or underscores. Names
must be unique within the route. A matched request exposes values through
$request->parameters['slug']. Values are decoded once. Empty values, decoded
slashes, backslashes, and control characters do not match.
Literal paths take priority regardless of registration order. Other overlapping patterns use registration order. The matched path determines allowed methods; a method mismatch does not fall through to a less specific route. Parameter values remain untrusted input and need application validation.
Dispatch
| Entry point | Behavior |
|---|---|
Application::handle(Request $request): Response |
Dispatches in memory without sending headers or output |
Application::run(int $maxBodyBytes = 1048576): void |
Reads PHP's request, dispatches, and sends the response |
An unknown path returns 404. An unsupported method returns 405 with Allow.
OPTIONS returns 204 with Allow for a known path. HEAD uses a registered HEAD
route or falls back to GET. handle returns the response without sending it;
run suppresses body output for HEAD. Ordinary responses retain a string body.
A FileResponse holds an opened file and sends its contents only from send.
handle translates a handler's HttpError into JSON such as {"error":422}
with the same HTTP status. run also translates errors from reading the request.
An unexpected exception propagates from handle. run catches the exception,
returns a generic 500, and logs its class without its message or request data.
This translation covers request parsing and dispatch. A failure while sending
a file can interrupt an already-started response; it cannot become a JSON error.
Stage\Security\Forbidden is not an HttpError. It needs translation to 403
in the application's HTTP handler. Compose features describes
operation permission checks.
Requests
Request holds method, path, body, headers, the raw query string, and
matched parameters. The constructor requires an uppercase method and an
encoded absolute path without spaces, control characters, a query, or a fragment.
Paths are not normalized or decoded. Invalid constructor input throws
InvalidArgumentException.
| Entry point | Behavior |
|---|---|
new Request(string $method, string $path, string $body = '', array $headers = [], string $query = '', array $parameters = []) |
Constructs a request for an adapter or an in-memory call |
Request::fromGlobals(int $maxBodyBytes = 1048576): Request |
Reads the request from PHP globals and limits the body in bytes |
Request::withParameters(array $parameters): Request |
Returns a new request with a replaced parameter map |
Request::json(): mixed |
Decodes JSON into PHP values, using associative arrays for objects |
Dispatch replaces any pre-existing parameter map with the matched route's values.
fromGlobals reads at most the body limit plus one byte. Oversized input throws
HttpError(413); a malformed request or unreadable body throws HttpError(400).
Headers read from PHP have lowercase names. The raw query is kept separately
from the path. Proxy headers never set identity or trust.
The raw body limit does not limit PHP-parsed POST multipart uploads. With PHP's
normal enable_post_data_reading setting, uploaded files and form fields are
available through $_FILES and $_POST; php://input is unavailable for these
requests. Configure PHP and server limits separately. See transfer files.
json() throws HttpError(400) for malformed JSON. A valid JSON value can be
an array, scalar, or null; decoding does not validate the shape or domain rules.
Responses
| Entry point | Behavior |
|---|---|
new Response(string $body = '', int $status = 200, array $headers = []) |
Constructs a buffered response with string-valued headers |
Response::text(string $body, int $status = 200): Response |
Sets text/plain; charset=utf-8 |
Response::html(string $body, int $status = 200): Response |
Sets text/html; charset=utf-8 |
Response::json(mixed $data, int $status = 200): Response |
Encodes JSON and sets application/json; charset=utf-8 |
Response::send(bool $head = false): void |
Writes status and headers, calculates content length, and sends the body unless $head is true |
The response constructor accepts statuses 200 through 599. It rejects header
injection, duplicate names regardless of case, server-owned framing headers,
and a body for status 204, 205, or 304. Header names are stored in lowercase.
JSON encoding failures throw. HTML is not escaped automatically; untrusted text
needs escaping at the rendering boundary with htmlspecialchars.
Ordinary response bodies are buffered. Repeated Set-Cookie headers and
arbitrary response streams are not supported.
Uploaded files
UploadedFile::fromPhp(array $upload, int $maxBytes): UploadedFile validates one
entry from $_FILES. Pass $_FILES['file'] ?? [] for a single file input.
Nested multiple-file arrays are rejected; adapt each entry separately if needed.
The returned readonly value exposes path, name, and size. The path must
be a native PHP upload. The name is a UTF-8 client basename without control
characters; it remains untrusted display text. The size is read from disk and
must match PHP's reported size and fit the caller's byte limit. Empty files are
allowed, including with a zero limit. A negative limit throws
InvalidArgumentException.
| Input failure | HTTP status |
|---|---|
| Missing, partial, malformed, nested, or non-upload input | 400 |
PHP upload size error or actual file exceeds maxBytes |
413 |
| PHP temporary-directory, write, extension, or file-stat failure | 500 |
Stage does not retain, move, delete, inspect, or trust the MIME type of the payload. PHP removes an unmoved upload when the request ends. Authentication, allowed content, generated storage names, quotas, and retention belong to the application.
File downloads
new FileResponse(string $path, string $downloadName, array $headers = [])
returns a Response with status 200. Construction opens a regular file and
records its byte length in the public size property. Its inherited body
is empty. Missing files and directories throw HttpError(404); open or stat
failures throw HttpError(500) before any response is sent.
send() reads at most 64 KiB at a time and emits the recorded Content-Length.
send(true) sends the same headers and length without reading or sending the
body. GET routes retain the ordinary HEAD fallback.
Every file response sets application/octet-stream, an attachment disposition
with an ASCII fallback and UTF-8 filename, and X-Content-Type-Options: nosniff.
The download name cannot contain paths, control characters, or invalid UTF-8.
The optional headers use normal response validation and cannot replace those
three download headers or framing headers. Pass application cache, indexing,
and CSP headers here instead of rebuilding the response from its empty body.
Keep the file unchanged until sending finishes. Concurrent truncation or disk failure can interrupt delivery. Application or server output buffering and compression can defeat streaming; configure them for file routes. Range, resume, conditional requests, and arbitrary streams are not implemented.
HTTP errors
new HttpError(int $status) accepts a status from 400 through 599. An invalid
status throws InvalidArgumentException. A translated error response contains
only the status, such as {"error":400}.
Routing performance
Literal routes use a direct lookup. Parameter routes are indexed at construction by their segment count and fixed prefix. Dispatch checks only those candidates, in registration order. Patterns sharing the same prefix and depth still require a linear scan. Reusing the application avoids rebuilding this index in a persistent runtime. Mutable handler dependencies still need their own lifecycle.
Run php bin/benchmark in the source checkout to measure dispatch with 10,
100, and 1,000 parameter routes. It reports microseconds per request after
warmup, using the median of five samples. The fixture uses distinct prefixes;
it measures routing in memory, not server throughput or application capacity.
Server configuration
Only the application's public directory belongs under the web root. Keep credentials, dependencies, source, and runtime data outside that directory. TLS, timeouts, request limits, and private-path denial are server responsibilities. PHP's built-in server is for local development.
Version and source
This guide targets Stage 0.1.4. Read the versioned source.