NAME
PAGI::Server::ConnectionState - Connection state tracking for HTTP requests
SYNOPSIS
my $conn = $scope->{'pagi.connection'};
# Synchronous, non-destructive check
if ($conn->is_connected) {
# Client still connected
}
# Get disconnect reason (undef while connected)
my $reason = $conn->disconnect_reason;
# Register a callback for an abnormal end (client gone, timeout, error)
$conn->on_disconnect(sub {
my ($reason) = @_;
rollback();
});
# ...and its counterpart for a clean finish. Exactly one of the two fires.
$conn->on_complete(sub {
commit();
});
# Await abnormal disconnect (if Future provided)
if (my $future = $conn->disconnect_future) {
my $reason = await $future;
}
DESCRIPTION
PAGI::Server::ConnectionState provides a mechanism for applications to detect client disconnection without consuming messages from the receive queue.
This addresses a fundamental limitation in the PAGI (and ASGI) model where checking for disconnect via receive() may inadvertently consume request body data.
The disconnect_future() method lazily creates a Future only when called, avoiding allocation overhead for simple request/response handlers that don't need async disconnect detection.
See the "Connection State" section in PAGI::Spec::Www for the full specification.
METHODS
new
my $conn = PAGI::Server::ConnectionState->new(connection => $connection);
Creates a new connection state object. The connection argument provides a reference to the parent Connection object for lazy Future creation.
is_connected
my $connected = $conn->is_connected; # Boolean
Returns true if the connection is still open, false if disconnected.
This is a synchronous, non-destructive check that does not consume messages from the receive queue.
response_started
my $started = $conn->response_started; # 0 or 1
True once the server has started this request's response (http.response.start emitted -- by the application, a framework, a middleware, or a server-synthesized error/backstop response). Server-owned; read-only to applications.
disconnect_reason
my $reason = $conn->disconnect_reason; # String or undef
Returns the disconnect reason string, or undef if still connected or if the request completed normally -- every reason below describes an abnormal end.
Standard reason strings:
client_closed- Client initiated clean close (TCP FIN) mid-requestclient_timeout- Client stopped responding (read timeout)idle_timeout- Connection idle too long before the request arrivedkeepalive_timeout- Keep-alive connection idled out between requestswrite_timeout- Response write timed outwrite_error- Socket write failed (EPIPE, ECONNRESET)read_error- Socket read failedprotocol_error- HTTP parse error, invalid requestserver_shutdown- Server shutting down gracefullyserver_error- Unhandled server-side error aborted the requestbody_too_large- Request body exceeded limitqueue_overflow- A bounded server queue overflowed; connection dropped
See "Standard Disconnect Reasons" in PAGI::Spec::Www for the authoritative list.
disconnect_future
my $future = $conn->disconnect_future; # Future or undef
my $reason = await $future;
Returns a Future that resolves when the connection closes abnormally (client disconnect, transport error). On a clean completion the connection closes but this Future is deliberately left pending — use on_complete to observe normal completion.
The Future is created lazily on first call, avoiding allocation overhead for handlers that don't need async disconnect detection.
The Future resolves with the disconnect reason string.
This is useful for racing against other async operations:
await Future->wait_any($disconnect_future, $event_future);
on_disconnect
$conn->on_disconnect(sub {
my ($reason) = @_;
# cleanup code
});
Registers a callback to be invoked when disconnect occurs.
May be called multiple times to register multiple callbacks
Callbacks are invoked in registration order
Callbacks receive the disconnect reason as the first argument
If registered after disconnect already occurred, callback is invoked immediately with the reason
One callback's failure does not prevent other callbacks from being invoked
on_complete
$conn->on_complete(sub {
# request finished cleanly
});
Registers a callback invoked only when the request completes successfully (the response was fully delivered without the client disconnecting). It is the counterpart to "on_disconnect": exactly one of the two fires for a given request.
May be called multiple times to register multiple callbacks
Callbacks are invoked in registration order, with no arguments
If registered after the request already completed, the callback is invoked immediately
If the request ended in an abnormal disconnect, the callback never fires
One callback's failure does not prevent other callbacks from being invoked
_mark_disconnected
$conn->_mark_disconnected($reason);
Internal method - Called by the server when disconnect is detected.
Updates the connection state and invokes all registered callbacks. Applications should not call this method directly.
State transitions occur in this order:
- 1.
is_connected()returns false - 2.
disconnect_reason()returns the reason string - 3.
disconnect_future()resolves with the reason (if it was created) - 4.
on_disconnectcallbacks are invoked in registration order
_mark_complete
$conn->_mark_complete;
Internal method - Called by the server when the request completes successfully (the response was fully delivered). Applications should not call this method directly.
Transitions to the completed terminal state and invokes on_complete callbacks in registration order. Unlike "_mark_disconnected", it leaves disconnect_reason() as undef and does not resolve disconnect_future() or fire on_disconnect callbacks -- a clean completion is not a disconnect.
Idempotent, and a no-op once the connection has already reached a terminal state (so a stray completion after an abnormal disconnect is ignored).
USAGE WITH PAGI::Request
The PAGI::Request class provides convenience methods that delegate to the connection object:
my $req = PAGI::Request->new($scope, $receive);
# Access connection object directly
my $conn = $req->connection;
# Convenience delegates
$req->is_connected; # $conn->is_connected
$req->is_disconnected; # !$conn->is_connected
$req->disconnect_reason; # $conn->disconnect_reason
$req->on_disconnect(sub { ... }); # $conn->on_disconnect(...)
$req->disconnect_future; # $conn->disconnect_future
EXAMPLE: Basic Connection Check
async sub handler {
my ($scope, $receive, $send) = @_;
my $conn = $scope->{'pagi.connection'};
# Check before expensive work
return unless $conn->is_connected;
my $result = await expensive_operation();
# Check again before responding
return unless $conn->is_connected;
await $send->({ type => 'http.response.start', status => 200, headers => [] });
await $send->({ type => 'http.response.body', body => $result, more => 0 });
}
EXAMPLE: Cleanup on Disconnect vs. Completion
async sub handler {
my ($scope, $receive, $send) = @_;
my $conn = $scope->{'pagi.connection'};
my $temp_file = create_temp_file();
my $lock = acquire_lock();
my $cleanup = sub { $temp_file->unlink; $lock->release };
# Abnormal end: client vanished, or a timeout/error fired mid-request.
$conn->on_disconnect(sub {
my ($reason) = @_;
$cleanup->();
log_info("Client disconnected: $reason");
});
# Clean end: the response was fully delivered.
$conn->on_complete(sub {
$cleanup->();
log_info("Delivered OK");
});
# Exactly one of the two callbacks runs, so cleanup happens once.
my $result = await process_data($temp_file);
await send_response($send, $result);
}
SEE ALSO
PAGI::Request - High-level request API with connection convenience methods
PAGI::Server - Reference server implementation
PAGI::Server::Connection - Per-connection state machine (internal)