NAME
PAGI::Request - Convenience wrapper for PAGI request scope
SYNOPSIS
use PAGI::Request;
use Future::AsyncAwait;
async sub app {
my ($scope, $receive, $send) = @_;
my $req = PAGI::Request->new($scope, $receive);
# Basic properties
my $method = $req->method; # GET, POST, etc.
my $path = $req->path; # /users/42
my $host = $req->host; # example.com
# Query parameters (Hash::MultiValue)
my $page = $req->query_param('page');
my @tags = $req->query_params->get_all('tags');
# Headers
my $ct = $req->content_type;
my $auth = $req->header('authorization');
# Cookies
my $session = $req->cookie('session');
# Body parsing (async)
my $json = await $req->json; # Parse JSON body
my $form = await $req->form_params; # Parse form data (Hash::MultiValue)
my $name = await $req->form_param('name'); # Single form value
# File uploads
my $avatar = await $req->upload('avatar');
if ($avatar && !$avatar->is_empty) {
$avatar->move_to('/uploads/avatar.jpg'); # blocking I/O
}
# Streaming large bodies
my $stream = $req->body_stream(max_bytes => 100 * 1024 * 1024);
await $stream->stream_to_file('/uploads/large.bin');
# Auth helpers
my $token = $req->bearer_token;
my ($user, $pass) = $req->basic_auth;
# Per-request shared state
use PAGI::Stash;
my $stash = PAGI::Stash->new($req);
$stash->set(user => $current_user);
}
DESCRIPTION
PAGI::Request provides a friendly interface to PAGI request data. It wraps the raw $scope hashref and $receive callback with convenient methods for accessing headers, query parameters, cookies, request body, and file uploads.
This is an optional convenience layer. Raw PAGI applications continue to work with $scope and $receive directly.
CONSTRUCTOR
new
my $req = PAGI::Request->new($scope, $receive);
Creates a new request object. $scope is required. $receive is optional but required for body/upload methods.
PROPERTIES
method
HTTP method (GET, POST, PUT, etc.)
path
Request path, UTF-8 decoded.
raw_path
Request path as raw bytes (percent-encoded).
query_string
Raw query string (without leading ?).
scheme
http or https.
host
Host from the Host header.
http_version
HTTP version (1.0 or 1.1).
client
Arrayref of [host, port] or undef.
content_type
Content-Type header value (without parameters).
content_length
Content-Length header value.
raw
Returns the raw scope hashref.
HEADER METHODS
header
my $value = $req->header('Content-Type');
Get a single header value (case-insensitive). Returns the last value if the header appears multiple times.
header_all
my @values = $req->header_all('Accept');
Get all values for a header.
headers
my $headers = $req->headers; # PAGI::Headers
Returns a PAGI::Headers clone of the inbound headers snapshot. The returned object is independent: mutating it (clear, set, etc.) does not affect subsequent calls to header, header_all, content_type, or cookie -- those always read the private snapshot, not the clone.
QUERY PARAMETERS
query_params
my $params = $req->query_params; # Hash::MultiValue
my $params = $req->query_params(strict => 1); # Die on invalid UTF-8
my $params = $req->query_params(raw => 1); # Skip UTF-8 decoding
Get query parameters as Hash::MultiValue.
Options:
strict- If true, die on invalid UTF-8 sequences. Default: false (invalid bytes replaced with U+FFFD).raw- If true, skip UTF-8 decoding entirely and return raw bytes. Default: false.
query_param
my $value = $req->query_param('page');
my $value = $req->query_param('page', strict => 1);
my $value = $req->query_param('page', raw => 1);
Shortcut for $req->query_params(%opts)->get($name). Accepts the same strict and raw options as query_params.
raw_query_param
my $value = $req->raw_query_param('page');
Shortcut for $req->query_param($name, raw => 1). Returns the raw bytes without UTF-8 decoding.
PATH PARAMETERS
Path parameters are captured from the URL path by a router (e.g., PAGI::App::Router) and stored in $scope->{path_params}. This is a router-agnostic interface - any router can populate this field.
path_params
my $params = $req->path_params; # hashref
Get all path parameters as a hashref. Returns an empty hashref if no router has set path parameters.
# Route: /users/:id/posts/:post_id
# URL: /users/42/posts/100
my $params = $req->path_params;
# { id => '42', post_id => '100' }
Note: This method can be overridden in subclasses for custom parameter handling (e.g., lazy conversion from positional to named parameters). The path_param method delegates to this method.
Options:
strict- If true, die when no router has populated$scope->{path_params}instead of returning an empty hashref. Default: false. Mirrors thestrictoption on "path_param".
path_param
my $id = $req->path_param('id');
my $id = $req->path_param('id', strict => 0); # Don't die if missing
Get a single path parameter by name.
# Route: /users/:id
# URL: /users/42
my $id = $req->path_param('id'); # '42'
Strict by default: Unlike query_param(), this method dies if the requested parameter does not exist. This catches typos early since path parameters are defined by the route - if the route matched, the expected parameters must exist.
# Route defines :userId but you typed :user_id
my $id = $req->path_param('user_id');
# Dies: "path_param 'user_id' not found. Available: userId, postId"
Options:
strict- If false, returnundeffor missing parameters instead of dying. Default: true.
Strict Mode
By default, path_params returns an empty hashref if no router has set $scope->{path_params}. This is the safest behavior for middleware and handlers that may run with or without a router.
To catch configuration errors early, pass strict => 1:
# Dies if no router populated the scope:
my $params = $req->path_params(strict => 1);
# "path_params not set in scope (no router configured?)"
path_param (singular) is strict by default for the requested key, so asking for a parameter when no router ran also dies, naming the missing key:
my $id = $req->path_param('id');
# "path_param 'id' not found. ... No path params set (no router?)"
This matches Starlette's behavior of returning an empty dict by default, while letting you opt into a loud failure per call.
COOKIES
cookies
my $cookies = $req->cookies; # hashref
Get all cookies.
cookie
my $session = $req->cookie('session');
Get a single cookie value.
BODY METHODS (ASYNC)
body_stream
my $stream = $req->body_stream;
my $stream = $req->body_stream(
max_bytes => 10 * 1024 * 1024, # 10MB limit
decode => 'UTF-8', # Decode to UTF-8
strict => 1, # Strict UTF-8 decoding
);
Returns a PAGI::Request::BodyStream for streaming body consumption. This is useful for processing large request bodies incrementally without loading them entirely into memory.
Options:
max_bytes- Maximum body size. Defaults to Content-Length header if present.decode- Encoding to decode chunks to (typically 'UTF-8').strict- If true, throw on invalid UTF-8. Default: false (use replacement chars).
Important: Body streaming is mutually exclusive with buffered body methods (body, text, json, form_params). Once you start streaming, you cannot use those methods, and vice versa.
Example:
# Stream large upload to file
my $stream = $req->body_stream(max_bytes => 100 * 1024 * 1024);
await $stream->stream_to_file('/uploads/data.bin');
See PAGI::Request::BodyStream for full documentation.
multipart_stream
my $stream = $req->multipart_stream;
my $stream = $req->multipart_stream(
max_files => 1000,
max_fields => 1000,
max_field_size => 1024 * 1024,
max_file_size => 100 * 1024 * 1024,
max_request_body => 1024 * 1024 * 1024,
);
Returns a PAGI::Request::MultipartStream for pull-based streaming of a multipart/form-data request body. You pull one part at a time and choose where each one goes:
while (defined(my $part = await $stream->next)) {
if ($part->is_file) {
await $part->stream_to_file($path);
}
else {
my $value = await $part->value; # raw bytes; you decode
}
}
Each part is a PAGI::Request::Part exposing its metadata (name, filename, content_type, headers, is_file) and methods to consume its body: next_chunk (pull raw bytes), value (buffer the whole part as raw bytes), stream_to($cb) (drain to a possibly-async sink), and stream_to_file($path) (write to a new file, path-safe).
Unlike the buffered multipart path (form_params/uploads), this does not spool each upload to a temp file: the application owns the sink, so a part can stream straight to an object store or a transform, and that sink can be fully asynchronous (stream_to awaits a Future-returning sink for backpressure) -- whereas the buffered spool is blocking.
Options:
max_files- Maximum number of file parts. Default: 1000.max_fields- Maximum number of field parts. Default: 1000.max_field_size- Maximum bytes per field part. Default: 1 MiB.max_file_size- Maximum bytes per file part. Default: 100 MiB.max_request_body- Maximum total body bytes (per-stream defence-in-depth; the server'smax_body_sizeis the primary cap). Default: 1 GiB.
Important: Streaming the multipart body is mutually exclusive with the buffered body methods. multipart_stream croaks if the body was already read or a stream was already created, and conversely body/text/json/ form_params/uploads croak once a stream exists -- a body can only be consumed once.
See PAGI::Request::MultipartStream for full documentation.
body
my $bytes = await $req->body;
Read raw body bytes. Cached after first read.
Important: Cannot be used after body_stream() has been called.
text
my $text = await $req->text;
Read body as UTF-8 decoded text.
json
my $data = await $req->json;
Parse body as JSON. Dies on parse error.
form_params
my $form = await $req->form_params; # Hash::MultiValue
my $form = await $req->form_params(strict => 1); # Die on invalid UTF-8
my $form = await $req->form_params(raw => 1); # Skip UTF-8 decoding
Parse URL-encoded or multipart form data, returning a Hash::MultiValue.
Options:
strict- If true, die on invalid UTF-8 sequences. Default: false.raw- If true, skip UTF-8 decoding entirely. Default: false.max_field_size,max_file_size,spool_threshold,max_files,max_fields,temp_dir- Per-request limits for multipart parsing, passed through to PAGI::Request::MultiPartHandler. Each defaults to the matching package variable in that module (e.g.$PAGI::Request::MultiPartHandler::MAX_FILE_SIZE);local-ize those to change a default process-wide.
form_param
my $value = await $req->form_param('name');
my $value = await $req->form_param('name', strict => 1);
Shortcut for (await $req->form_params(%opts))->get($name). Accepts the same strict and raw options as form_params.
raw_form_params
my $form = await $req->raw_form_params;
Shortcut for $req->form_params(raw => 1). Returns form data without UTF-8 decoding.
raw_form_param
my $value = await $req->raw_form_param('name');
Shortcut for $req->form_param($name, raw => 1). Returns a single form value without UTF-8 decoding.
UPLOAD METHODS (ASYNC)
uploads
my $uploads = await $req->uploads; # Hash::MultiValue
Get all uploads as Hash::MultiValue of PAGI::Request::Upload objects.
upload
my $file = await $req->upload('avatar');
Get a single upload by field name.
upload_all
my @files = await $req->upload_all('photos');
Get all uploads for a field name.
PREDICATES
is_get, is_post, is_put, is_patch, is_delete, is_head, is_options
if ($req->is_post) { ... }
Check HTTP method.
is_json
True if Content-Type is application/json.
is_form
True if Content-Type is form-urlencoded or multipart.
is_multipart
True if Content-Type is multipart/form-data.
accepts
if ($req->accepts('text/html')) { ... }
if ($req->accepts('json')) { ... }
Check Accept header (supports wildcards and shortcuts). Returns true if the client accepts the given MIME type.
preferred_type
my $type = $req->preferred_type('json', 'html', 'xml');
Returns the best matching content type from the provided list based on the client's Accept header and quality values. Returns undef if none are acceptable. Supports shortcuts (json, html, xml, etc).
CONNECTION STATE METHODS
These methods provide non-destructive disconnect detection. Unlike reading from the receive queue, these methods do not consume any messages.
See PAGI::Server::ConnectionState for the underlying implementation.
connection
my $conn = $req->connection;
Returns the PAGI::Server::ConnectionState object for this request, or undef if not provided by the server.
is_connected
if ($req->is_connected) {
# Client still connected
}
Returns true if the client connection is still alive. This is a synchronous, non-destructive check that does not consume messages from the receive queue.
is_disconnected
if ($req->is_disconnected) {
# Client has disconnected
}
Returns true if the client has disconnected. Equivalent to !$req->is_connected.
This is a synchronous, non-destructive check.
disconnect_reason
my $reason = $req->disconnect_reason;
Returns the disconnect reason string, or undef if still connected.
Standard reasons include: client_closed, client_timeout, idle_timeout, write_error, read_error, protocol_error, server_shutdown, body_too_large.
See "disconnect_reason" in PAGI::Server::ConnectionState for the full list.
on_disconnect
$req->on_disconnect(sub {
my ($reason) = @_;
rollback();
log_info("Client disconnected: $reason");
});
Registers a callback invoked only on an abnormal disconnect (the client goes away, a timeout fires, an error occurs) -- not on a clean finish. The callback receives the disconnect reason. Multiple callbacks may be registered; if the client has already disconnected, the callback is invoked immediately. Returns the request for chaining. The counterpart to "on_complete": exactly one of the two fires per request.
on_complete
$req->on_complete(sub {
commit();
});
Registers a callback invoked only when the request completes successfully (the response was fully delivered without the client disconnecting). Multiple callbacks may be registered; if the request has already completed, the callback is invoked immediately. Returns the request for chaining. The counterpart to "on_disconnect".
disconnect_future
my $future = $req->disconnect_future;
if ($future) {
# Race against other operations
await Future->wait_any($disconnect_future, $event_future);
}
Returns a Future that resolves when the client disconnects, or undef if not supported. The Future resolves with the disconnect reason string.
This is useful for racing against other async operations.
buffered_amount, high_water_mark, low_water_mark
my $pending = $req->buffered_amount; # bytes queued, not yet on the wire
my $ceiling = $req->high_water_mark; # backpressure ceiling (or undef)
my $floor = $req->low_water_mark; # backpressure floor (or undef)
Outbound flow-control introspection, delegated to the server-provided pagi.transport handle (see "Transport Flow Control" in PAGI::Spec::Www). For a streaming response, use buffered_amount to conflate or shed load instead of only blocking on drain; when the server does not provide the handle, buffered_amount returns 0 and the watermarks return undef.
on_high_water, on_drain, is_writable
$req->on_high_water(sub { $source->pause }); # backpressure engaged
$req->on_drain(sub { $source->resume }); # backpressure cleared
last unless $req->is_writable; # below the high mark?
Backpressure controls delegated to the pagi.transport handle. on_high_water and on_drain register edge-triggered callbacks (the Node/Mojo drain model) for producers that cannot self-pace with a blocking send; each returns the object for chaining. is_writable is true when the outbound buffer is below the high mark. When the server provides no transport handle (or only the read methods), the callbacks are quiet no-ops and is_writable is true.
AUTH HELPERS
bearer_token
my $token = $req->bearer_token;
Extract Bearer token from Authorization header.
basic_auth
my ($user, $pass) = $req->basic_auth;
Decode Basic auth credentials.
scope
my $scope = $req->scope;
Returns the raw PAGI scope hashref. Useful for constructing helper objects like PAGI::Stash and PAGI::Session:
my $stash = PAGI::Stash->new($req);
response
my $res = $req->response;
Vends a detached PAGI::Response bound to this request's scope: the raw-application analog of $ctx->response. The response is a value, not a connection; build it up and send it with $res->respond($send):
await $req->response->status(201)->json($data)->respond($send);
Per-Request Shared State
See PAGI::Stash for per-request shared state between middleware and handlers. Construct from a Request object or scope:
use PAGI::Stash;
my $stash = PAGI::Stash->new($req);
$stash->set(user => $current_user);
state
my $db = $req->state->{db};
my $config = $req->state->{config};
Returns the application state hashref injected by PAGI::Lifespan. This contains worker-level shared state like database connections and configuration. Returns empty hashref if no state was injected.
Key differences from PAGI::Stash:
stateis read-only, set during lifespan startupstateis shared across all requests in a workerPAGI::Stash is per-request, writable by middleware/handlers
SEE ALSO
PAGI::Stash, PAGI::Request::Upload, PAGI::Request::BodyStream, Hash::MultiValue