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('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;      # Parse form data

    # File uploads (async)
    my $avatar = await $req->upload('avatar');
    if ($avatar && !$avatar->is_empty) {
        await $avatar->save_to('/uploads/avatar.jpg');
    }

    # 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 storage
    $req->stash->{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.

CLASS METHODS

configure

PAGI::Request->configure(
    max_body_size     => 10 * 1024 * 1024,  # 10MB total body
    max_field_size    => 1 * 1024 * 1024,   # 1MB per form field
    max_file_size     => 10 * 1024 * 1024,  # 10MB per file upload
    spool_threshold   => 64 * 1024,         # 64KB
    path_param_strict => 0,                 # Die if path_params not in scope
);

Set class-level defaults for body/upload handling and path parameters.

max_body_size

Maximum total request body size. Enforced by the server.

max_field_size

Maximum size for non-file form fields in multipart requests. Default: 1MB. Protects against oversized text submissions.

max_file_size

Maximum size for file uploads in multipart requests. Default: 10MB. Applies to parts with a filename in Content-Disposition.

spool_threshold

Size at which multipart data is spooled to disk. Default: 64KB.

path_param_strict

When set to 1, path_params and path_param will die if $scope->{path_params} is not defined (i.e., no router has set it). Default: 0 (return empty hashref/undef silently).

This is useful for catching configuration errors where you expect a router but one isn't configured. See "Strict Mode" for details.

config

my $config = PAGI::Request->config;

Returns the current configuration hashref.

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;  # Hash::MultiValue

Get all headers as a Hash::MultiValue object.

QUERY PARAMETERS

query_params

my $params = $req->query_params;  # Hash::MultiValue

Get query parameters as Hash::MultiValue.

query

my $value = $req->query('page');

Shortcut for $req->query_params->get($name).

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' }

path_param

my $id = $req->path_param('id');

Get a single path parameter by name. Returns undef if not found.

# Route: /users/:id
# URL: /users/42
my $id = $req->path_param('id');  # '42'

Strict Mode

By default, path_params and path_param return empty values 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.

If you want to catch configuration errors early, enable strict mode:

PAGI::Request->configure(path_param_strict => 1);

With strict mode enabled, calling path_params or path_param when $scope->{path_params} is undefined will die with an error message. This helps catch bugs where you expect a router but one isn't configured.

# Strict mode: dies if no router set path_params
PAGI::Request->configure(path_param_strict => 1);

my $id = $req->path_param('id');
# Dies: "path_params not set in scope (no router configured?)"

The default is path_param_strict => 0 (non-strict), which matches Starlette's behavior of returning an empty dict when path_params is not set.

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). 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.

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

my $form = await $req->form;  # Hash::MultiValue

Parse URL-encoded or multipart form data.

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).

is_disconnected (async)

if (await $req->is_disconnected) { ... }

Check if client has disconnected.

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.

STASH

stash

$req->stash->{user} = $current_user;
my $user = $req->stash->{user};

Returns the per-request stash hashref for sharing data between middleware and handlers. The stash is also accessible via $res->stash, $ws->stash, and $sse->stash for consistency.

How Stash Works

The stash lives in $scope->{'pagi.stash'}, not in the Request object itself. This is an important design choice:

  • Scope-based, not object-based - Request/Response objects are ephemeral (each middleware/handler may create its own), but stash persists because it lives in scope.

  • Survives shallow copies - When middleware creates a modified scope ({ %$scope, key => val }), the stash hashref is preserved by reference. All objects in the chain see the same stash.

  • Shared across the chain - Middleware sets values, handlers read them, subrouters inherit them. The stash "flows through" via scope sharing.

Example

# In auth middleware
async sub require_auth {
    my ($self, $req, $res, $next) = @_;
    $req->stash->{user} = verify_token($req->bearer_token);
    await $next->();
}

# In handler - sees the user (even though it's a different $req object)
async sub get_profile {
    my ($self, $req, $res) = @_;
    my $user = $req->stash->{user};  # Set by middleware
    await $res->json($user);
}

# Can also read via Response
async sub another_handler {
    my ($self, $req, $res) = @_;
    my $user = $res->stash->{user};  # Same stash!
    await $res->json($user);
}

Note: For worker-level state (database connections, config), use $self->state in PAGI::Endpoint::Router subclasses.

SEE ALSO

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