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.