NAME
PAGI::Context - Per-request context with protocol-specific subclasses
SYNOPSIS
use PAGI::Context;
use Future::AsyncAwait;
# Factory returns the right subclass based on scope type
my $ctx = PAGI::Context->new($scope, $receive, $send);
# Shared methods (all protocol types)
my $type = $ctx->type; # 'http', 'websocket', 'sse'
my $path = $ctx->path;
my $stash = $ctx->stash; # PAGI::Stash
my $session = $ctx->session; # PAGI::Session
# WebSocket context - protocol ops directly on $ctx
await $ctx->accept;
await $ctx->send_json({ msg => 'hello' });
my $text = await $ctx->receive_text;
await $ctx->close;
# SSE context - same idea
await $ctx->send_event(event => 'update', data => $payload);
await $ctx->keepalive(25);
# Event dispatcher - works on any protocol type
my $reason = await $ctx
->on('websocket.receive', async sub { ... })
->on('chat.message', async sub { ... })
->on_error(sub { ... })
->run; # returns 'disconnect', 'stop', or 'error'
# Underlying protocol objects still available
my $ws = $ctx->websocket; # PAGI::WebSocket (WS only)
my $sse = $ctx->sse; # PAGI::SSE (SSE only)
my $req = $ctx->request; # PAGI::Request (HTTP only)
my $res = $ctx->response; # PAGI::Response (HTTP only)
DESCRIPTION
PAGI::Context is a factory and base class that provides a unified entry point for per-request context. Calling PAGI::Context->new(...) inspects $scope->{type} and returns the appropriate subclass: PAGI::Context::HTTP, PAGI::Context::WebSocket, or PAGI::Context::SSE.
Shared methods (scope accessors, stash, session, event dispatcher) live on the base class. Protocol-specific methods are delegated from subclasses so you can use $ctx as your single object:
# Instead of:
my $ws = $ctx->websocket;
await $ws->send_json($data); # closes over $ws in every handler
# Just do:
await $ctx->send_json($data); # $ctx is already in scope
Protocol Shape
Each context type has a different set of available methods. Calling a method that belongs to a different protocol type raises a standard Perl Can't locate object method error.
Method HTTP WebSocket SSE
────────────────── ────── ────────── ──────
request, response yes - -
text, html, json yes - -
redirect yes - -
method yes - -
accept - yes -
send_text - yes -
send_bytes - yes -
send_json - yes yes
send - - yes
send_event - - yes
send_comment - - yes
start - - yes
close - yes yes
query / query_param - yes(query) yes(query_param)
is_connected base* WS override -
is_closed - yes yes
is_started - - yes
keepalive - yes yes
each_text, etc. - yes -
each, every - - yes
*is_connected on WebSocket contexts checks WS handshake state,
not the TCP-level pagi.connection that the base class uses.
See PAGI::Context::WebSocket and PAGI::Context::SSE for the full method reference on each subclass.
EXTENSIBILITY
Override _type_map to add or replace protocol types:
package MyApp::Context;
our @ISA = ('PAGI::Context');
sub _type_map {
my ($class) = @_;
return {
%{ $class->SUPER::_type_map },
grpc => 'MyApp::Context::GRPC',
};
}
Override _resolve_class for custom resolution logic beyond the type map.
CONSTRUCTOR
new
my $ctx = PAGI::Context->new($scope, $receive, $send);
Factory constructor. Returns a subclass instance based on $scope->{type}. Defaults to HTTP if type is missing or unknown.
CLASS METHODS
_type_map
my $map = PAGI::Context->_type_map;
Returns a hashref mapping scope type strings to subclass package names. Override in a subclass to add or replace protocol types.
_resolve_class
my $class = PAGI::Context->_resolve_class($scope);
Resolves the scope to a subclass package name. Looks up $scope->{type} in _type_map; defaults to the http mapping if the type is missing or unknown. Override for custom resolution logic.
METHODS
Scope Accessors
$ctx->scope; # raw $scope hashref
$ctx->type; # $scope->{type}
$ctx->path; # $scope->{path}
$ctx->raw_path; # $scope->{raw_path} // $scope->{path}
$ctx->query_string; # $scope->{query_string} // ''
$ctx->scheme; # $scope->{scheme} // 'http'
$ctx->client; # $scope->{client}
$ctx->server; # $scope->{server}
$ctx->headers; # $scope->{headers} arrayref of [name, value]
assert_http, assert_websocket, assert_sse
my $ctx = PAGI::Context->new($scope, $receive, $send)->assert_http;
Type guards for handlers that support only one protocol. Each returns the context unchanged when $ctx->type matches, and croaks with a clear message otherwise — turning a forgotten scope-type check into a loud, early failure instead of a confusing Can't locate object method deeper in the handler. Because they chain off the polymorphic "new", the whole gate is one line:
my $ctx = PAGI::Context->new(@_)->assert_http; # croaks unless the scope is http
They are named assert_* rather than PAGI::Context->http / ->websocket because websocket and sse are already instance methods that return the underlying channel objects.
Path Parameters
my $params = $ctx->path_params; # hashref
my $id = $ctx->path_param('id'); # strict: dies if missing
my $id = $ctx->path_param('id', strict => 0); # returns undef
path_params returns the $scope->{path_params} hashref (set by the router), defaulting to {} if not present.
path_param returns a single parameter by name. By default it dies if the key is not found (strict mode). Pass strict => 0 to return undef for missing keys instead.
Protocol Introspection
$ctx->is_http; # true if type eq 'http'
$ctx->is_websocket; # true if type eq 'websocket'
$ctx->is_sse; # true if type eq 'sse'
header
my $value = $ctx->header('Content-Type');
Returns the last value for the named header (case-insensitive), or undef if not found.
receive
my $receive = $ctx->receive;
Returns the raw $receive coderef. Calling it returns a Future that resolves to the next protocol event hashref from the client.
# Read an HTTP request body event
my $event = await $ctx->receive->();
# $event = { type => 'http.request', body => '...' }
# Read a WebSocket message
my $msg = await $ctx->receive->();
# $msg = { type => 'websocket.receive', text => 'hello' }
Most users should prefer the protocol helpers ($ctx->request, $ctx->websocket, $ctx->sse) which handle the event protocol internally. Use receive only for raw protocol access.
send
my $send = $ctx->send;
Returns the raw $send coderef. Calling it with an event hashref returns a Future that resolves when the event has been sent.
# Send an HTTP response (two events: start + body)
await $ctx->send->({ type => 'http.response.start', status => 200,
headers => [['content-type', 'text/plain']] });
await $ctx->send->({ type => 'http.response.body', body => 'Hello' });
# Accept a WebSocket connection
await $ctx->send->({ type => 'websocket.accept' });
Most users should prefer the protocol helpers ($ctx->response, $ctx->websocket, $ctx->sse) which build and send events for you. Use send only for raw protocol access.
raw_send
my $send = $ctx->raw_send;
Returns the raw $send coderef on any context type. On the base context this is identical to "send", but subclasses never override raw_send, whereas some override send: PAGI::Context::SSE replaces send with a sse.send convenience ($ctx->send($data) sends an SSE data event). So when you are holding an SSE (or any) context and need the actual underlying channel rather than the protocol convenience, reach for raw_send.
The reason you reach for it: emitting your own event types that something downstream (a middleware) translates into the wire protocol — the same shape PAGI's SSE/WebSocket layers themselves use (sse.send / websocket.send are custom send events a layer renders).
# Inside an SSE handler: $ctx->send would emit an sse.send for you, but we
# want to emit our own typed event for a middleware to render.
my $emit = $ctx->raw_send;
await $emit->({ type => 'app.event', name => 'tick', data => 1 });
# A middleware wrapping $send catches it and renders to the wire:
# my $wrapped = async sub ($ev) {
# if (($ev->{type} // '') eq 'app.event') {
# await $send->({ type => 'sse.send', event => $ev->{name},
# data => encode_json({ value => $ev->{data} }) });
# return;
# }
# await $send->($ev); # real protocol events pass through
# };
Note the trap raw_send avoids: on an SSE context $ctx->send is a method that sends (it calls $ctx->sse->send), not an accessor — so $ctx->send there is not the raw coderef. raw_send always is.
stash
my $stash = $ctx->stash; # PAGI::Stash instance
Returns a PAGI::Stash wrapping $scope->{'pagi.stash'}. Lazy-constructed and cached.
session
my $session = $ctx->session; # PAGI::Session instance
Returns a PAGI::Session wrapping $scope->{'pagi.session'}. Lazy-constructed and cached. Dies if session middleware has not run. Use has_session to check availability first.
has_session
if ($ctx->has_session) {
my $user_id = $ctx->session->get('user_id');
}
Returns true if session middleware has populated $scope->{'pagi.session'}.
state
my $state = $ctx->state; # hashref
Returns $scope->{state} - the app/endpoint-level shared state.
Connection State
$ctx->connection; # PAGI::Server::ConnectionState object
$ctx->is_connected; # boolean
$ctx->is_disconnected; # boolean
$ctx->disconnect_reason; # string or undef (abnormal disconnect only)
$ctx->on_disconnect($cb); # callback on abnormal disconnect
$ctx->on_complete($cb); # callback on clean completion
Delegates to $scope->{'pagi.connection'}. on_disconnect fires only on an abnormal end and on_complete only on a clean finish -- exactly one per request.
$ctx->buffered_amount, $ctx->high_water_mark, and $ctx->low_water_mark expose outbound flow control via the server's pagi.transport handle (see "Transport Flow Control" in PAGI::Spec::Www): bytes queued for the client but not yet on the wire, and the backpressure band. buffered_amount returns 0 (and the watermarks undef) when the server does not provide the handle.
$ctx->on_high_water($cb) and $ctx->on_drain($cb) register edge-triggered backpressure callbacks (pause/resume a producer), and $ctx->is_writable is true while the buffer is below the high mark. Both delegate to pagi.transport and degrade quietly -- the callbacks become no-ops and is_writable stays true -- when the handle (or its callback support) is absent.
EVENT DISPATCHER
The event dispatcher provides a generic, protocol-agnostic way to handle PAGI events. It is most useful when the receive stream carries a mix of protocol events and application-level events injected by middleware such as PAGI::Middleware::Channels.
my $ctx = PAGI::Context->new($scope, $receive, $send);
$ctx->on('websocket.receive', async sub {
my ($ctx, $event) = @_;
my $text = $event->{text} // '';
await $ctx->send->({ type => 'websocket.send', text => "echo: $text" });
});
$ctx->on('chat.message', async sub {
my ($ctx, $event) = @_;
# handle a channel-injected event
});
$ctx->on_error(sub {
my ($ctx, $error, $source) = @_;
warn "[$source] $error";
});
my $reason = await $ctx->run; # 'disconnect', 'stop', or 'error'
on
$ctx->on($event_type, $callback); # returns $ctx
Register a handler for a raw PAGI event type string. Multiple handlers may be registered for the same type; they are called in registration order. Handlers receive ($ctx, $event). Handlers may be plain coderefs or async subs; if a handler returns a Future, run() awaits it before continuing.
Returns $ctx for chaining.
on_default
$ctx->on_default($callback); # returns $ctx
Register a single fallback handler, called with ($ctx, $event) for any event that has no type-specific handler. The last registration wins. The callback may be a plain coderef or an async sub; if it returns a Future, run() awaits it. Exceptions are routed to on_error with source = 'handler'.
The terminal disconnect event is excluded. on_default does not fire for the protocol's disconnect event (websocket.disconnect, sse.disconnect, http.disconnect): that event ends the loop normally and is not treated as an "unhandled" surprise. To run cleanup or logging on disconnect, register a handler for it explicitly instead:
$ctx->on('websocket.disconnect', sub { ... });
Returns $ctx for chaining.
on_error
$ctx->on_error($callback); # returns $ctx
Register an error callback. It is called when $receive->() fails ($source = 'receive') or when a registered handler throws ($source = 'handler'). Callbacks receive ($ctx, $error, $source).
Multiple callbacks may be registered and are called in order. Callbacks may be async subs; if a callback returns a Future, it is awaited. If no callbacks are registered, errors are emitted via warn.
Returns $ctx for chaining.
Handlers and error callbacks are cleared automatically when run() resolves, so closures that capture $ctx do not leak -- you do not need to weaken them. (Weakening only matters if you register callbacks and never call run().)
stop
$ctx->stop; # returns $ctx
Signal the run() loop to exit cleanly after the current event's handlers finish, before the next event is read. run() will resolve with reason 'stop'.
Returns $ctx for chaining.
run
my $reason = await $ctx->run;
Start the event dispatch loop. Reads events from the receive stream and dispatches each to registered handlers. The loop runs until one of:
The protocol's terminal disconnect event arrives (
websocket.disconnect,sse.disconnect,http.disconnect) - resolves with'disconnect'stop()was called - resolves with'stop'$receive->()fails - fireson_errorcallbacks and resolves with'error'
run() always resolves successfully (never rejects). The caller does not need to catch it.
Calling run() a second time while already running throws synchronously.
When run() resolves, all registered handlers and error callbacks are cleared to break closure-based reference cycles.
SEE ALSO
Protocol subclasses (full method reference for each protocol):
PAGI::Context::HTTP, PAGI::Context::WebSocket, PAGI::Context::SSE
Underlying protocol objects (standalone use, or direct access via $ctx->websocket, $ctx->sse, $ctx->request, $ctx->response):
PAGI::WebSocket, PAGI::SSE, PAGI::Request, PAGI::Response
Shared services: