NAME
PAGI::Server::Connection - Per-connection state machine
SYNOPSIS
# Internal use by PAGI::Server
my $conn = PAGI::Server::Connection->new(
stream => $stream,
app => $app,
protocol => $protocol,
server => $server,
extensions => {},
);
$conn->start;
DESCRIPTION
PAGI::Server::Connection manages the state machine for a single client connection. It handles:
Request parsing via Protocol::HTTP1
Scope creation for the application
Event queue management for $receive and $send
Protocol upgrades (WebSocket, SSE)
SSE over HTTP/1.1 and HTTP/2
Connection lifecycle and cleanup
SSE OVER HTTP/2
SSE events (sse.start, sse.send, sse.comment, sse.keepalive) work transparently over both HTTP/1.1 and HTTP/2. Applications do not need to change their SSE handling code based on protocol version.
How It Works
A request is detected as SSE when its combined Accept header values contain the exact media range text/event-stream, case-insensitively, with an effective quality value greater than zero (see " "SSE Connection Detection"" in PAGI::Spec::Www); a q=0 refusal or a wildcard range such as */* never signals SSE. Detection works identically regardless of HTTP version. Over HTTP/1.1, SSE data is sent using chunked Transfer-Encoding. Over HTTP/2, SSE data is sent as DATA frames via the submit_response_streaming/data_callback mechanism. This difference is transparent to the application.
The http_version field in the scope hash will be '2' for HTTP/2 connections, allowing applications to distinguish if needed.
SSE Idle Timeout over HTTP/2
The sse_idle_timeout setting is enforced per stream on HTTP/2: each SSE stream owns its own idle timer, armed when that stream's sse.start is sent and reset by that stream's own send activity (sse.send, sse.comment, sse.keepalive, sse.close). When a stream's timer expires, only that stream ends -- the server marks the stream closing, lets it flush any already-queued data, and then emits the final HTTP/2 END_STREAM frame, the same path an application-initiated sse.close takes. Sibling SSE (and other) streams multiplexed on the same HTTP/2 connection are unaffected, and the connection itself stays open.
Over HTTP/1.1, each SSE stream already owns its own TCP connection, so sse_idle_timeout is enforced at the connection level there -- expiry closes that connection, which only ever carries the one SSE stream.
Connection Reuse after an SSE Stream (HTTP/1.1)
sse.start advertises Connection: keep-alive, and the server honors it. When an HTTP/1.1 SSE stream ends cleanly -- the application returns, or it sends sse.close -- the server writes the chunked terminator, resets the per-request state the stream accumulated, and hands the connection back to ordinary keep-alive request handling, including serving any request already pipelined in the read buffer. A pooled client (browser, Net::Async::HTTP, curl) can therefore reuse the same socket for its next request, which matters for the short POST-SSE-exchange pattern used by fetch-event-source and datastar.
Keep-alive yields to the usual overrides, each of which closes the connection the same way it does outside SSE: a client Connection: close, HTTP/1.0 semantics, server shutdown, an application exception, and any abnormal end (client disconnect, idle timeout, write error). An abnormal end is also the only thing that delivers sse.disconnect to the application; a clean end never does.
Ending the stream is decoupled from the application returning. After sse.close the application keeps running against a live transport, and any further send on that scope fails through the event sequence machine (after sse.close) rather than being silently swallowed by a closed transport.
SEE ALSO
PAGI::Server, PAGI::Server::Protocol::HTTP1
AUTHOR
John Napiorkowski <jjnapiork@cpan.org>
LICENSE
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.