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

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 the strict option 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, return undef for 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.

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's max_body_size is 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:

  • state is read-only, set during lifespan startup

  • state is shared across all requests in a worker

  • PAGI::Stash is per-request, writable by middleware/handlers

SEE ALSO

PAGI::Stash, PAGI::Request::Upload, PAGI::Request::BodyStream, Hash::MultiValue