=encoding UTF-8

=head1 NAME

PAGI::Spec::Www - PAGI message formats for HTTP, WebSocket, and SSE

=head1 HTTP, WebSocket & SSE PAGI Message Format

B<Version>: 0.3 (Draft)

The HTTP, WebSocket & SSE PAGI sub-specification defines how HTTP/1.1, HTTP/2, WebSocket, and Server-Sent Events (SSE) connections are transported within PAGI.

It is designed to be a superset of the PSGI specification and specifies how to translate between PAGI and PSGI for compatible requests.

=head2 Spec Versions

=over

=item -

C<0.5>: Constrained what a response body may contain and what an intermediary
may do to one. A response's body is B<either> a sequence of inline C<body>
events B<or> a single C<file>/C<fh> event, never both; an application MUST NOT
send C<file>/C<fh> after inline bytes and the server MUST fail that send. An
intermediary MUST NOT invalidate a commitment the response head made -- a
C<< trailers => 1 >> declaration, or a C<206>/C<Content-Range> body that is a
range rather than the representation -- and MUST examine the head before
transforming a body or attaching metadata derived from one. An intermediary
re-emitting a buffered event MUST preserve the event's other keys, including
C<more>, whose default of C<0> makes an omission an assertion of completeness.
Also states which of the two incomplete-response signals answers which
question: absence of the terminal event governs what may be forwarded or
synthesized, C<disconnect_reason> governs only whether to raise or log.

=item -

C<0.4>: Defined settlement of in-flight I/O at disconnect: a send Future
still pending when the connection (or the scope's stream) ends settles by
resolving successfully, a pending receive resolves with the disconnect
event, and both settlements are ordered after the connection-state
transition so that await-then-check is race-free. C<on_disconnect> callbacks
and C<disconnect_future> resolution are never delivered synchronously within
an application's call into C<$send> or C<$receive>. The HTTP/2
C<RST_STREAM> mapping now names the standard disconnect transition instead
of "cancel any outstanding Futures".

=item -

C<0.3>: Added C<pagi.transport> outbound flow-control introspection
(C<buffered_amount>, high/low watermarks, C<on_high_water>/C<on_drain> callbacks)
and the C<websocket.http.response> WebSocket denial-response extension. Reworked
disconnect handling into one abnormal-only reason vocabulary, adding
C<on_complete> (successful completion) alongside C<on_disconnect> (abnormal
drops) on C<pagi.connection>. Documented the server-supplied HTTP, SSE, and
WebSocket behaviors, HEAD response semantics (server-side body suppression),
incomplete-response handling (forced closure), and WebSocket over HTTP/2
(RFC 8441). Tightened SSE detection to an exact media-range match with
C<< q > 0 >> (wildcards and C<q=0> never signal SSE). B<Removed>
C<pagi.features> (superseded by C<extensions>) and the per-send C<timeout> field
(superseded by transport backpressure and keepalive timeouts).

=item -

C<0.2>: Added keepalive events (C<websocket.keepalive>, C<sse.keepalive>),
disconnect reasons, SSE support for all HTTP methods (not just GET) with the
C<sse.request> receive event, and the C<pagi.connection> scope key for
non-destructive disconnect detection.

=item -

C<0.1>: Initial draft, based on ASGI 2.5, including Server-Sent Events support.

=back

C<< $scope->{pagi}{spec_version} >> reports which version of this specification
governs an HTTP, WebSocket, or SSE connection. It is optional; if omitted,
applications MUST assume C<'0.1'>. Omitting it declares support for version 0.1
only, so the server MUST NOT use behaviour introduced by a later version on
that connection. The core PAGI C<version> does not affect this default.

=head2 Common Data Types

=head3 Headers Format

Headers are represented as C<ArrayRef[ArrayRef[Bytes]]> - an array of 2-element tuples where each tuple contains C<[name, value]>:

    headers => [
        ['content-type', 'text/html; charset=utf-8'],
        ['x-request-id', '12345'],
        ['set-cookie', 'session=abc'],
        ['set-cookie', 'tracking=xyz'],  # duplicate names allowed
    ]

B<Why tuples instead of PSGI's flat array?>

PSGI uses a flat array with implicit pairs: C<['Content-Type', 'text/html', 'X-Custom', 'value']>. PAGI uses explicit tuples for several reasons:

=over 4

=item 1.

B<Clearer iteration> - Each header is a discrete unit:

    # PAGI - straightforward
    for my $header (@$headers) {
        my ($name, $value) = @$header;
    }

    # PSGI - requires index math
    for (my $i = 0; $i < @$headers; $i += 2) {
        my ($name, $value) = @{$headers}[$i, $i+1];
    }

=item 2.

B<Explicit duplicates> - Duplicate header names (common for C<Set-Cookie>) are visually obvious

=item 3.

B<Easier manipulation> - Filtering, mapping, and transforming headers works naturally with array operations:

    # Remove all cookies
    my @filtered = grep { $_->[0] ne 'set-cookie' } @$headers;

    # Find a header
    my ($ct) = grep { $_->[0] eq 'content-type' } @$headers;

=item 4.

B<ASGI compatibility> - Matches the Python ASGI specification that PAGI is modeled on

=back

B<Rules:>

=over

=item -

Header names MUST be lowercase byte strings

=item -

Header values MUST be byte strings (opaque, not decoded)

=item -

Each inner arrayref MUST contain exactly 2 elements: C<[name, value]>

=item -

Duplicate header names are permitted (required for C<Set-Cookie>, etc.)

=back

B<HTTP::Headers Compatibility:>

PSGI's flat array format is more compatible with C<<< HTTP::Headers->flatten() >>>. If you need to interoperate:

    # PAGI tuples -> flat array (for HTTP::Headers)
    my @flat = map { @$_ } @$pagi_headers;
    my $hh = HTTP::Headers->new(@flat);

    # Flat array -> PAGI tuples
    my @flat = $http_headers->flatten;
    my @pagi = map { [$flat[$_*2], $flat[$_*2+1]] } 0 .. ($#flat/2);

=head3 Scope Extension Keys

The scope hashref may contain additional keys beyond those defined in the HTTP, WebSocket, and SSE sections below. This allows middleware and applications to pass data through the request lifecycle.

B<Reserved prefixes:>

=over

=item -

C<pagi.*> - Reserved for PAGI spec extensions (e.g., C<pagi.router>, C<pagi.session>)

=item -

Keys without a dot prefix (e.g., C<type>, C<method>, C<path>) - Reserved for core spec use

=back

B<Custom keys:>

Applications and third-party middleware SHOULD use a unique key or prefix to avoid collisions. Two common patterns:

    # Pattern 1: Single hashref (recommended for grouped data)
    $scope->{myauth} = {
        user  => $user_object,
        roles => ['admin', 'editor'],
    };

    # Pattern 2: Dotted keys (for flat/independent values)
    $scope->{'myapp.request_id'} = $uuid;
    $scope->{'myapp.started_at'} = time();

Either approach works - choose based on whether your data is naturally grouped or independent.

B<Allowed values:>

Scope values may be any Perl data type:

=over

=item -

Scalars, arrayrefs, hashrefs

=item -

Blessed objects

=item -

Code references

=back

B<Note:> Objects in scope are NOT serializable. Do not assume scope can be passed between processes, persisted to storage, or serialized to JSON. Scope exists only for the lifetime of a single request within a single process.

B<Example:>

    # Authentication middleware - hashref pattern
    $scope->{myauth} = {
        user  => $user_object,
        roles => ['admin', 'editor'],
        authenticated_at => time(),
    };

    # Router middleware (PAGI built-in)
    $scope->{'pagi.router'} = {
        params => { id => '42' },
        route  => '/users/:id',
    };

    # Application accessing middleware data
    my $user = $scope->{myauth}{user};
    my $id = $scope->{'pagi.router'}{params}{id};

B<Middleware guidelines:>

=over

=item -

Document the keys your middleware adds to scope

=item -

Use consistent naming within your namespace

=item -

Don't modify keys outside your namespace

=item -

Check for key existence before assuming middleware ran

=back

=head2 HTTP

PAGI covers HTTP/1.0, HTTP/1.1, and HTTP/2. Protocol servers assign separate scopes for requests within the same HTTP/2 connection and multiplex responses appropriately.

=head3 HTTP/2 Stream Mapping

PAGI servers must translate HTTP/2 frames into PAGI HTTP events per stream. Applications only see structured events, not raw frames:

=over

=item -

B<HEADERS>: start a new PAGI C<http> scope and emit an initial C<http.request> event with headers and C<<< more => 1 >>> if DATA will follow, or C<<< more => 0 >>> if C<END_STREAM> was signaled immediately.

=item -

B<DATA>: emit subsequent C<http.request> events with C<<< body => <chunk> >>> and C<<< more => 1 >>> or C<0> depending on C<END_STREAM>.

=item -

B<END_STREAM>: if no DATA frames, send an C<http.request> with C<<< body => '' >>> and C<<< more => 0 >>> to signal end of request.

=item -

B<RST_STREAM>: an abnormal disconnect of that stream's scope; the server performs the standard disconnect transition for it (see L</State Transition Order>).

=item -

B<WINDOW_UPDATE / PRIORITY>: ignored by default (advanced flow control is optional).

=item -

B<PUSH_PROMISE>: not supported; servers must reject push promises.

=back

Only HTTP/2 over TLS (C<h2>) is required for the initial implementation; cleartext HTTP/2 (C<h2c>) is optional.

The HTTP version is available in the scope. Pseudo headers (like C<:authority>) from HTTP/2 must be removed; if C<:authority> is present, its value must be used to populate or override the C<host> header.

Multiple C<Set-Cookie> headers must be preserved individually, and C<Cookie> headers should be combined or split according to the version-specific rules (as per RFC 7230, RFC 6265, and RFC 9113).

=head3 Cookie Header Normalization

PAGI servers B<must normalize C<Cookie> headers> before passing them to the application.

=over

=item -

If multiple C<Cookie:> headers are received from the client (which may happen in real-world deployments despite RFC guidance), the server must:

=item -

Concatenate them using C<"; "> (semicolon followed by space)

=item -

Ensure only B<one C<cookie> header> appears in the PAGI C<headers> list

=back

B<Example:>

If the client sends:

    Cookie: a=1 Cookie: b=2; c=3

The PAGI scope must include:

    headers => [
      [ 'cookie', 'a=1; b=2; c=3' ]
    ]

The server does not parse the cookie string into key-value pairs -- parsing is left to middleware or application code. The server only guarantees RFC-compliant normalization.

=head3 HTTP Connection Scope

Each HTTP request has a single-request connection scope. Scope keys:

=over

=item -

C<type> (String) -- C<"http">

=item -

C<< $scope->{pagi}{version} >> (String) -- The PAGI core spec version (e.g. C<'0.5'>)

=item -

C<< $scope->{pagi}{spec_version} >> (String) -- The version of this HTTP sub-specification (e.g. C<'0.5'>). Optional; if omitted, assume C<'0.1'>.

=item -

C<http_version> (String) -- C<"1.0">, C<"1.1">, or C<"2">

=item -

C<method> (String) -- Uppercase HTTP method

=item -

C<scheme> (String, default C<"http">) -- URL scheme (C<"http"> or C<"https">)

=item -

C<path> (String) -- Decoded HTTP path

=item -

C<raw_path> (Bytes, optional) -- Original HTTP path bytes

=item -

C<query_string> (Bytes) -- Percent-encoded query string

=item -

C<root_path> (String, default C<"">) -- Application mount path, equivalent to C<SCRIPT_NAME> in PSGI

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]]) -- Original HTTP headers. Header names B<must> be lower-cased byte strings and header values must be opaque byte strings.

=item -

C<client> (ArrayRef[String, Int], optional) -- C<[host, port]> of client

=item -

C<server> (ArrayRef[String, Optional[Int]], optional) -- C<[host, port]> or C<[path, undef]> for Unix sockets

=item -

C<state> (HashRef, optional) -- A shallow copy of the lifespan C<state> namespace. Top-level keys are private to this request; values (object references) are shared. See L<PAGI::Spec::Lifespan/Lifespan State> for how to share mutable data safely.

=item -

C<pagi.connection> (Object) -- Per-request connection-state object for disconnect detection and response progress (C<response_started> / C<response_complete>). See L</Connection State> below.

=item -

C<pagi.transport> (Object, optional) -- Outbound flow-control introspection handle (C<buffered_amount>, watermarks, drain callbacks), when the server provides one. See L</Transport Flow Control>.

=item -

C<extensions> (HashRef, default C<{}>) -- Optional server capabilities advertised for this connection (e.g. C<tls>). Each present key names an extension mapping to a hashref of extension data; applications check for a key before relying on it. See L<PAGI::Spec::Extensions>.

=back

=head3 Request - C<receive> event

Note: Chunked transfer encoding must be de-chunked by the server. Each C<http.request> represents a de-chunked body fragment.

Keys:

=over

=item -

C<type> -- C<"http.request">

=item -

C<body> (Bytes, default C<"">) -- Request body chunk

=item -

C<more> (Int, default C<0>) -- C<1> if more body data is forthcoming, otherwise C<0>

=back

=head3 Response Start - C<send> event

Note: Protocol servers are NOT required to flush on C<http.response.start>, giving flexibility to emit an error response in case of internal application errors before data is sent.

Transfer-Encoding headers sent by the application must be ignored. Content-Encoding (e.g. gzip) is under application control.

Keys:

=over

=item -

C<type> -- C<"http.response.start">

=item -

C<status> (Int) -- HTTP status code

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]], default C<[]>) -- Response headers

=item -

C<trailers> (Int, default C<0>) -- C<1> if trailers will be sent after body via C<http.response.trailers>, otherwise C<0>

=back

B<Header byte safety.> Because header names and values cross into HTTP message
framing, a server B<MUST NOT> emit a header whose name or value contains C<CR>
(C<\x0D>), C<LF> (C<\x0A>), or C<NUL> (C<\x00>), nor a name containing any other
control character. The server B<MUST> reject such a header by failing the Future
returned by C<$send> rather than forwarding it or silently rewriting the
offending bytes: letting C<CR>/C<LF> through permits HTTP response splitting
(CRLF injection), while silently stripping them mutates the application's data
without signalling the bug. This is the same guarantee C<sse.send> gives for
its C<event> and C<id> fields (see L</Send SSE - C<send> event>): the server's
emission path is the single point that guards the wire, so applications and
frameworks MAY hold header values as opaque bytes and need not pre-validate
them. (RFC 9110 S5.5; RFC 9112 S11.1.)

=head3 Response Body - C<send> event

Keys:

=over

=item -

C<type> -- C<"http.response.body">

=item -

C<body> (Bytes, default C<"">) -- Response body chunk

=item -

C<file> (String) -- Absolute path to file for server to open and stream

=item -

C<fh> (Filehandle) -- Already-open filehandle for server to stream

=item -

C<offset> (Int, default C<0>) -- Byte offset to start reading from (for range requests)

=item -

C<length> (Int, optional) -- Number of bytes to send (omit to read until EOF)

=item -

C<more> (Int, default C<0>) -- Indicates more body content to follow (C<1> if true, otherwise C<0>). B<Ignored for C<file> and C<fh> responses> which are implicitly complete.

=back

The C<body>, C<file>, and C<fh> keys are B<mutually exclusive> - at most one may be provided per event. If none is provided, the event is treated as if C<< body => '' >> had been given (per the C<body> default above); an event with no payload keys and C<< more => 0 >> is therefore a valid way to end a response. Applications B<MUST> provide C<body> as encoded bytes. For text content, this typically means UTF-8 encoding before sending. The C<Content-Length> header (if present) MUST reflect byte length, not character length.

B<Note:> When using C<file> or C<fh>, the response is implicitly complete after the file/handle contents are sent. The C<more> key is ignored for these response types - there is no need to specify C<<< more => 0 >>>.

B<Payload kinds do not mix within a response.> The exclusivity above holds
across the response as well as within an event: a response's body is B<either>
a sequence of inline C<body> events B<or> a single C<file>/C<fh> event, never
both. An application B<MUST NOT> send a C<file> or C<fh> event once inline body
bytes have been delivered on that response, and the server B<MUST> fail such a
C<<< $send->() >>> Future. Because a C<file>/C<fh> event is implicitly
complete, the only sequence this rules out is one or more C<<< more => 1 >>>
chunks followed by a C<file>/C<fh> event.

A C<file>/C<fh> send that B<fails> delivers no bytes and leaves the response
sequence state untouched (see B<Error Handling> below), so an application may
still recover by sending an inline body instead.

The restriction exists for intermediaries. A middleware that transforms body
bytes must commit to an encoding before it can know whether a delegated
payload will follow, and it cannot apply that encoding to bytes the server
streams on the application's behalf; permitting the mix would oblige every such
middleware either to read the delegated file itself, defeating the delegation,
or to emit a response whose declared encoding does not describe its bytes.
ASGI's Path Send extension draws the same line ("cannot be mixed with
C<http.response.body>"); PSGI and Rack make the combination unrepresentable.
An application that wants to prepend generated bytes to a file's contents
sends the whole body inline, or arranges for the file to contain them.

When C<file> or C<fh> is provided, servers MUST stream the file contents efficiently:

=over

=item -

Servers SHOULD stream large files in chunks to avoid memory bloat

=item -

Servers MAY use zero-copy mechanisms (sendfile, splice) when appropriate

=item -

The C<offset> and C<length> keys enable range request support (e.g., HTTP 206 Partial Content)

=item -

For production file serving, consider using XSendfile middleware to delegate to a reverse proxy

=item -

When using C<file>, the server opens the file, streams it, and closes it

=item -

When using C<fh>, the application retains ownership and B<MUST> close the handle after the C<<< $send->() >>> Future completes

=back

B<Error Handling:>

=over

=item -

If C<file> cannot be opened (not found, permission denied), the C<<< $send->() >>> Future MUST fail with an appropriate exception

=item -

If C<fh> is invalid or closed, or a read or seek on it fails, the C<<< $send->() >>> Future MUST fail

=item -

B<Delivery failure is fail-don't-mutate.> When a C<file>/C<fh> send fails,
the server MUST leave the response sequence state as it was before the
event -- the same rule as header byte safety -- so the application may
recover, for example by sending an alternative body. Once body bytes from
the failed event have already reached the wire, recovery is no longer
possible and L</Application Left a Response Incomplete> governs the
outcome.

=item -

The failed Future is the application's notification of the failure; an
application that does not await its send Futures forfeits per-send failure
notification, and delivery problems then surface only through the
connection-state callbacks and disconnect events.

=item -

Applications SHOULD validate file existence before sending C<http.response.start> to avoid incomplete responses

=back

B<Validation:>

=over

=item -

C<offset> MUST be a non-negative integer

=item -

C<length> MUST be a non-negative integer if provided

=item -

If C<offset> exceeds file size, servers SHOULD send zero bytes

=back

B<Examples:>

    # Full file streaming
    await $send->({
        type => 'http.response.body',
        file => '/var/www/static/large-video.mp4',
    });

    # Range request (bytes 1000-1999)
    await $send->({
        type => 'http.response.body',
        file => '/var/www/static/document.pdf',
        offset => 1000,
        length => 1000,
    });

    # Streaming from already-open filehandle
    open my $fh, '<:raw', '/tmp/generated-report.csv' or die $!;
    await $send->({
        type => 'http.response.body',
        fh => $fh,
    });
    close $fh;  # Application MUST close after send Future completes

=head3 Response Trailers - C<send> event

Only valid when C<http.response.start> was sent with C<<< trailers => 1 >>>. After trailers are transmitted the server MUST consider the response body complete.

B<Framing requirement (HTTP/1.1).> Over HTTP/1.1, trailers exist only in
chunked transfer coding (RFC 9112): when the application supplied
C<Content-Length> — so the server framed the response without chunking — a
trailer section cannot be transmitted. The server MUST reject a
C<http.response.trailers> event on such a response by failing the C<$send>
Future, and MUST NOT silently discard it or treat the response as complete
with trailers unsent — the same fail-don't-mutate rule the spec applies to
header byte safety. HTTP/2 carries trailers as trailing HEADERS frames and
has no such framing restriction. (On a C<HEAD> response the trailers event
is accepted and discarded per L</HEAD Requests>, which takes precedence.)

Keys:

=over

=item -

C<type> -- C<"http.response.trailers">

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]], default C<[]>) -- Trailer headers encoded the same way as response headers (lower-case names, byte values)

=back

=head3 HEAD Requests

A response to a C<HEAD> request carries no content on the wire, on B<every>
HTTP version (RFC 9110 S9.3.2). The server's send path is the single point
that guards the wire, so B<suppression is the server's job>: the application
handles a C<HEAD> request exactly as it would the equivalent C<GET> --
same C<http.response.start>, same headers, same body events -- and the server
B<MUST NOT> transmit the body bytes. Suppression is transparent to the
application: each C<$send> Future resolves as usual, the terminal body event
(C<< more => 0 >>) completes the request, and C<on_complete> /
C<response_complete> behave exactly as for any other method.

Server requirements:

=over

=item -

The server B<MUST NOT> transmit response body content for a C<HEAD> request,
regardless of HTTP version.

=item -

An application-supplied C<Content-Length> header B<MUST> be passed through
untouched: per RFC 9110 it advertises the length the C<GET> response would
have had. The server B<MUST NOT> apply C<Transfer-Encoding: chunked> framing
to a C<HEAD> response; when no C<Content-Length> was supplied, the response
simply ends after the header section, with no body-framing headers at all.

=item -

For C<file> and C<fh> body events the server skips streaming entirely. It is
B<NOT> required to open or C<stat> the file to synthesize a C<Content-Length>;
an application that wants the length advertised sets the header itself.

=item -

A C<http.response.trailers> event on a C<HEAD> response is accepted and
discarded.

=back

Application guidance:

=over

=item -

C<HEAD> requests are delivered to the application like any other method --
the server does B<not> answer them itself and does B<not> rewrite them to
C<GET>. An application whose routing only matches C<GET> will return its
not-found response to C<HEAD> clients.

=item -

An application B<MAY> short-circuit on C<< $scope->{method} eq 'HEAD' >> and
skip generating an expensive body: send C<http.response.start> with the same
headers the C<GET> would carry, then a single empty terminal body event. This
is purely an optimization -- an unmodified C<GET> code path is equally
correct, the server discards the bytes.

=item -

Set C<Content-Length> explicitly when the size matters to C<HEAD> clients. A
response that relies on the server's chunked framing gives a C<GET> client
the size implicitly through the body, but a C<HEAD> client receives B<no>
length information at all.

=item -

Middleware that measures response size by observing body events will
over-report for C<HEAD>: the bytes it counts are discarded by the server.
Wire-accurate sizes come from the server's own logging.

=back

=head3 Application Produced No Response

If an HTTP application's Future resolves without any C<http.response.start> having
been sent, and the client is B<still connected>, the client is left waiting for a
response that will never come. As a last-resort B<backstop> the server produces a
C<500> response, logs it, and closes the connection so the client is not left
hanging.

This is a backstop, B<not> a completion policy. Which terminal response to produce
-- a C<404>, a C<204>, an error page, or anything else -- is the application's or
framework's responsibility, and a well-behaved application always produces one
before its Future resolves. PAGI exposes only the observer-independent fact that
no response was started -- read C<< $conn->response_started >> on the
C<pagi.connection> object; it makes B<no> judgment about whether a response is
"complete".
An application that does not handle the C<http> scope may decline by raising an
exception, which the server treats identically -- the same C<500> backstop.

If the client has B<already disconnected> (see L</Disconnect - C<receive> event>),
this is not an application error: the server MUST NOT synthesize a C<500> and MUST
NOT log an error.

=head3 Application Left a Response Incomplete

A response is B<incomplete> when the application's Future becomes ready --
resolves B<or fails with an exception> -- after C<http.response.start> was
sent but before the response reached its terminal
event: the final C<http.response.body> with C<< more => 0 >>, a C<file>/C<fh>
body event, or -- when C<http.response.start> promised trailers with
C<< trailers => 1 >> -- the C<http.response.trailers> event.

B<What the terminal event asserts.> The terminal event is the application's
claim that the response is B<complete> in the sense of RFC 9110 Section 6.1 --
that all the octets its framing indicates are available. The server renders
that claim on the wire as the zero-length chunk of the chunked coding
(RFC 9112 Section 7.1) or as C<END_STREAM> (RFC 9113 Section 8.1). Everything
below follows from that: the event is a statement about the representation,
not a formality that ends a code path.

The server MUST treat this as an B<abnormal end>, never a clean one. The
governing principle: a truncated response must be B<observable as truncated>
by the client, and a connection with unfinished framing must never carry
another response. Concretely, the server:

=over

=item -

MUST NOT synthesize the terminal framing the application failed to send.
Writing the chunked terminator or signaling C<END_STREAM> on the
application's behalf would present a truncated response to the client as
complete -- a lie that downstream caches would then store (RFC 9111
Section 3.3 permits storing an incomplete response only when it is recorded
as incomplete, and forbids answering a request with one).

=item -

MUST terminate the transport so the client observes truncation. Over
HTTP/1.1 this means closing the connection without the chunk terminator (or,
for a C<Content-Length>-framed response, before the declared length was
delivered); the connection MUST NOT be kept alive or serve a pipelined
request. Over HTTP/2 the server resets the stream (C<RST_STREAM>, e.g.
C<INTERNAL_ERROR>); other streams on the connection are unaffected.

=item -

MUST report the abnormal end on the C<pagi.connection> object: fire
C<on_disconnect> with reason C<server_error>. C<on_complete> MUST NOT fire
-- the response was not fully delivered (see L</Connection State>).

=item -

SHOULD log the event at error level.

=back

As with L</Application Produced No Response>, if the client had B<already
disconnected> before the application's Future resolved, this is not an
application error: the request already ended abnormally with its own
disconnect reason, and the server MUST NOT log an incomplete-response error.

B<The application side of the same rule.> An application that stops early
because its client disconnected B<MUST NOT> send the terminal event to
"tidy up": it did not produce the whole representation, so it may not assert
that it did. Its C<$send> would be a successful no-op in that state
(see L</Disconnected Client - sending after disconnect>), but the assertion
would still be false to every intermediary in the C<$send> chain, and an
application that aborts for some other reason -- a failed backend, say --
may not yet be inside the post-disconnect no-op guarantee at all, in which
case a live client receives a terminal marker over a truncated body. The
absence of the terminal event B<is> the signal that the response stopped
short; that is what makes it distinguishable from one that completed.

B<Every producer is bound, not only the server.> The prohibition on
synthesizing terminal framing applies to B<anything that emits response
events> -- middleware that buffers a body and re-emits it, a framework
component that adapts one response into another, or any other intermediary
wrapping C<$send> -- not only to the server writing bytes. An intermediary
that observes an incomplete event stream MUST NOT complete it on the
application's behalf, and MUST NOT compute or attach a representation
validator (an C<ETag> or digest), a C<Content-Length>, or any other
completeness-dependent metadata from the bytes it happened to observe.
Intermediaries that need to distinguish an abnormal end from an application
fault read the request's C<disconnect_reason> (see L</Connection State>);
a defined reason means the request ended abnormally, while C<undef>
distinguishes a clean completion, which C<is_connected> alone does not.

B<Which signal answers which question.> The two signals above are not
alternatives, and an intermediary needs both. The absence of the terminal
event answers B<whether the response completed>; C<disconnect_reason>
answers B<why the request ended>. An intermediary decides what to
B<forward> -- and whether it may attach completeness-dependent metadata --
from the event stream alone: it may synthesize a terminal event, an C<ETag>,
or a C<Content-Length> only if it observed a terminal event itself. It
consults C<disconnect_reason> only to decide whether to B<raise or log>,
never to decide whether to forward what the application already produced.

Using the disconnect signal to make the forwarding decision is a fault in
both directions. A buffering intermediary that discards its buffered
C<http.response.start> because the client disconnected destroys a response
the application really did start: every outer observer then sees a request
that never started a response, which is a different state with different
consequences -- servers report it differently, and a client that read no
bytes at all may retry an idempotent request under RFC 9110 Section 9.2.2.
Conversely, an intermediary that keys on the disconnect signal alone still
fabricates whenever an application stops early while its client is B<still
connected>, which the disconnect signal cannot see. Forward the events you
withheld; withhold only the metadata you did not earn.

B<An intermediary must not invalidate what the head promised.> A response head
can commit the application to things a body-transforming intermediary will
break unless it looks first. Before transforming a body, or attaching metadata
derived from one, an intermediary B<MUST> examine the response head for such
commitments and decline to act where it cannot honour them. Two that exist
today, and the list is not closed:

=over

=item -

C<<< trailers => 1 >>> commits the server to chunked framing over HTTP/1.1,
because that is the only HTTP/1.1 framing that can carry a trailer section
(see L</Response Trailers - C<send> event>). An intermediary that adds a
C<Content-Length> to such a response makes the server reject the
application's own C<http.response.trailers> event — the application did
nothing wrong, and its framing choice was overruled downstream.

=item -

A C<206> with C<Content-Range> means the body is a B<range>, not the
representation. An intermediary that computes an C<ETag> or digest from it
labels the resource with a validator that identifies only the fragment, so a
cache may later serve the fragment as the whole. The same applies to any
C<Content-Encoding> the intermediary would add: it describes the transfer of
a range whose bounds were computed against the unencoded representation.

=back

An intermediary that re-emits an event it buffered B<MUST> also preserve the
event's other keys rather than reconstructing it from the fields it happens to
care about. Rebuilding an C<http.response.start> from C<status> and C<headers>
alone silently drops C<<< trailers => 1 >>>, which is the first failure above
reached by a different route.

B<Re-emitting a buffered body event.> An intermediary that buffers body
events and later re-emits them B<MUST> preserve each event's C<more> value.
Because C<more> defaults to C<0> (see L</Response Body - C<send> event>),
omitting it on a re-emitted non-terminal event asserts a completeness the
application never claimed -- the same prohibition as above, reached by
omission rather than intent.

=head3 Server-Supplied Headers and Behaviors

Beyond the headers an application sets on C<http.response.start>, a conforming
server supplies a few headers and framing details on its own, and may originate
certain responses without invoking the application. Applications and frameworks
SHOULD be aware of these so they neither duplicate nor fight them.

=over

=item -

B<C<Date>> -- The server SHOULD add a C<Date> response header in HTTP-date format
(RFC 7231) when the application did not supply one. (The reference server adds it
to both HTTP/1.1 and HTTP/2 responses.)

=item -

B<Body framing> -- When the application provides no C<Content-Length> on an
HTTP/1.1 response, the server frames the body with C<Transfer-Encoding: chunked>.
Applications SHOULD set C<Content-Length> (byte length) when the size is known.
HTTP/2 uses its own DATA-frame framing, where neither header applies.

=item -

B<C<Connection> (HTTP/1.0)> -- For HTTP/1.0 clients the server manages the
C<Connection> header: C<close> when the response carries no C<Content-Length>
(the close delimits the body), or C<keep-alive> when the client requested
keep-alive and a C<Content-Length> is present. Applications SHOULD NOT set
C<Connection> themselves.

=item -

B<C<100 Continue>> -- When an HTTP/1.1 client sends C<Expect: 100-continue>, the
server sends an interim C<100 Continue> before the application first reads the
request body, then delivers the body through C<receive> as usual. This is
transparent to the application. HTTP/2 has no C<Expect: 100-continue> mechanism.

=item -

B<Connection-level and framing headers belong to the server.> An application
MAY include C<Connection>, C<Transfer-Encoding>, C<Keep-Alive>, C<Upgrade>,
C<Proxy-Connection>, or C<TE> among its response headers, but the server
controls connection disposition and message framing and B<must not> let an
application-supplied value contradict them: it strips or ignores
application-supplied values in this family (the full set over HTTP/2, per
RFC 9113; C<Transfer-Encoding> and C<Connection> over HTTP/1.1), supplies
its own, and SHOULD log when it strips. PAGI defines no exceptions of its
own to this rule -- where the HTTP RFCs themselves carve one out, the
server implements the RFC rather than the blanket strip. The two the RFCs
define today: RFC 9113 permits C<te> when its value is the C<trailers>
token (a server forwarding it normalizes to the canonical token, since the
RFC forbids surrounding whitespace in field values), and RFC 9110 obliges
any C<Upgrade> sender to pair it with C<Connection: upgrade>, so over
HTTP/1.1 a server B<must> supply that token itself when an application
response carries C<Upgrade> (e.g. a C<426 Upgrade Required>) -- the
application never sends C<Connection: upgrade> directly. Applications
express content encoding through C<Content-Encoding>, and connection
lifetime through completing or not completing responses, never through
these headers. (This continues the WSGI and ASGI rule that hop-by-hop
headers are the gateway's to manage.)

=back

B<Server-generated responses.> A server MAY originate a response on its own --
before the application runs, or in place of it -- for protocol and resource
errors it detects, each carrying a short C<text/plain> body. Applications cannot
intercept these (the request never reaches the application, or the application
has already failed). A conforming server is expected to generate at least:

=over

=item -

C<400 Bad Request> -- a malformed request line, headers, or message framing.

=item -

C<413 Payload Too Large> -- the request body, or its declared C<Content-Length>,
exceeds the server's configured C<max_body_size>.

=item -

C<414 URI Too Long> / C<431 Request Header Fields Too Large> -- the request line
or header section exceeds the server's limits.

=item -

C<500 Internal Server Error> -- the application threw before sending
C<http.response.start>. (Once the response has started the server can only
truncate the connection -- see L</Disconnected Client - sending after disconnect>.)

=item -

C<501 Not Implemented> -- an unsupported request, e.g. an unrecognized
C<Transfer-Encoding>, or an Extended C<CONNECT> without a supported C<:protocol>.

=item -

C<503 Service Unavailable> -- the server is at an admission-control limit
(for example, a configured maximum connection count) and declines the
connection before invoking any application; a C<Retry-After> header SHOULD
accompany it (RFC 9110 Section 15.6.4).

=back

The exact reason phrases and body text are server-defined; applications MUST NOT
depend on them.

=head3 Sends Are Sequential

An application B<must not> issue a new C<$send> on a connection before the
previous send's Future has resolved; the effect of overlapping sends is
unspecified. Applications with concurrent producers serialize them (for
example through a queue) before sending. (This matches the one-outstanding-
write rule of comparable streaming interfaces; a normally structured
application that awaits each send satisfies it automatically.)

=head3 Disconnected Client - sending after disconnect

If the client disconnects or cancels the connection, servers MUST send an explicit C<http.disconnect> event to the application and update the connection state (see L</Connection State>).

A subsequent C<$send> is a B<no-op>: it neither delivers data nor raises. Applications therefore detect disconnection through the C<http.disconnect> event or the C<pagi.connection> connection-state object (see L</Connection State>) -- PAGI's non-destructive, race-friendly mechanism for exactly this -- rather than by inspecting the result of C<$send>.

A send already B<in flight> when the disconnect is detected is not an error
either: it settles by resolving successfully, per the Send Completion
Contract in L<PAGI::Spec>. The application resumes from its await, observes
the disconnect through C<pagi.connection> (already updated -- see L</State
Transition Order>), and stops or cleans up as it chooses.

Applications MUST gracefully handle disconnect events by:

=over

=item -

Immediately halting unnecessary processing upon disconnect

=item -

Optionally sending minimal final acknowledgment messages

=item -

Executing asynchronous cleanup of resources as necessary.

=back

=head3 Disconnect - C<receive> event

Sent to the application if receive is called after a response has been sent or after the HTTP connection has been closed.

Keys:

=over

=item -

C<type> -- C<"http.disconnect">

=back

=head3 Connection State

The C<pagi.connection> scope key provides a mechanism for applications to detect client disconnection B<without consuming messages from the receive queue>. This addresses a fundamental limitation where checking for disconnect via C<receive()> may inadvertently consume request body data.

=head4 The Problem

In PAGI's pull-based receive model, the only way to know if a client has disconnected is to consume the next message:

    my $message = await $receive->();
    if ($message->{type} eq 'http.disconnect') {
        # Client is gone - but if it wasn't a disconnect, that data is lost!
    }

The connection state object solves this with synchronous, non-destructive checks.

=head4 Connection Object Interface

The object reports two distinct ways a request can end, through B<separate>
callbacks, so an application never has to disambiguate them:

=over 4

=item *

An B<abnormal disconnect> -- the client goes away, a timeout fires, or an error
occurs -- B<while the application is still working>.

=item *

The request B<completing successfully> -- the response was fully delivered.

=back

B<When a server provides C<on_complete>>, exactly one of C<on_disconnect> and C<on_complete> fires for a given request.

Servers MUST provide:

B<C<is_connected()>> - Returns true while the connection is open, false once it has closed (for any reason).

    my $connected = $conn->is_connected;  # Boolean, synchronous

B<C<disconnect_reason()>> - Returns the abnormal-disconnect reason string (see L</Standard Disconnect Reasons>), or C<undef> if the connection is still open or completed normally.

    my $reason = $conn->disconnect_reason;  # String or undef

B<C<on_disconnect($callback)>> - Registers a callback invoked B<only on abnormal disconnect> (client gone, timeout, error) -- not on normal completion. The callback receives the disconnect reason.

    $conn->on_disconnect(sub {
        my ($reason) = @_;
        cancel_work();          # the client is gone; stop and clean up
    });

=over

=item -

May be called multiple times to register multiple callbacks

=item -

Callbacks are invoked in registration order

=item -

An exception raised by a registered callback (here, in C<on_complete>, or in
the C<pagi.transport> callbacks) B<must not> prevent the remaining callbacks
from running and B<must not> disturb the server's own processing; servers
SHOULD log such exceptions. Two independently registered cleanup callbacks
must not be able to sabotage each other.

=item -

If registered after disconnect already occurred, the callback is invoked immediately

=back

B<Callback invocation context.> Servers MUST NOT deliver abnormal-disconnect
notifications -- invoking C<on_disconnect> callbacks, or resolving
C<disconnect_future> -- synchronously within an application's call into
C<$send> or C<$receive>. A server that detects a disconnect while such a call
is on the stack updates the synchronous facts immediately (C<is_connected>,
C<disconnect_reason>) and defers only the delivery to the event loop.
Deferred notifications MUST still be delivered even if the connection is torn
down before the deferred delivery runs -- deferral changes I<when> a
notification arrives, never I<whether>. The registration-time rule above is
unchanged: registering a callback after the transition has already happened
invokes it immediately, within the registering call; that re-entrancy is
under the application's control.

No equivalent guarantee exists for C<on_complete> or for C<pagi.transport>
callbacks: in particular, a completion callback may be invoked within the
application's terminal send call.

Rationale: a callback that can run inside an application's own half-finished
call into C<$send> would force every framework to build re-entrancy defenses
around every send. Deferring delivery to the event loop is cheap for servers
and eliminates the hazard class, while the synchronous facts stay immediate
for polling code.

B<C<response_started()>> - Returns true once B<this request's> response has started
-- that is, once C<http.response.start> has been emitted for this scope -- and false
before. The server sets it when it processes the response-start event, so it is true no
matter who produced the response: the application, a framework, a middleware that
short-circuited the request, or a B<server-synthesized error/backstop response>
(e.g. the C<500> the server emits when the application produces nothing -- see
L</Application Produced No Response>). Read-only to the application.

C<response_started> does not identify which producer started the response; an
application response and a server-synthesized backstop set it alike. The
clean-versus-abnormal outcome is reported separately by the request's terminal
signal -- C<on_complete> on a clean finish, C<on_disconnect> with
C<disconnect_reason> C<'server_error'> on a server backstop.

    my $started = $conn->response_started;  # Boolean, synchronous

A streaming response counts as started for its whole lifetime: C<response_started>
becomes true at C<http.response.start> and stays true while body chunks flow. Use
C<response_complete> to learn when the body has actually finished.

Servers SHOULD also provide:

B<C<on_complete($callback)>> - Registers a callback invoked B<only when the request completes successfully> (the response was fully delivered without the client disconnecting). The counterpart to C<on_disconnect>.

    $conn->on_complete(sub {
        commit_transaction();   # finished cleanly
    });

B<C<response_complete()>> - Returns true once B<this request's> response body has
been fully sent -- the terminal C<http.response.body> with C<<< more => 0 >>>, a
C<file>/C<fh> response, or the final trailers -- false while a response is still
streaming or has not started, and B<C<undef> if the server does not track
completion>. The synchronous counterpart to C<on_complete>, and read-only to the
application. As with C<disconnect_future>, test C<defined> to learn whether the
feature is available before relying on it.

    my $done = $conn->response_complete;   # undef = unsupported, else 0 or 1

This accessor is B<SHOULD>, not MUST, on purpose. "The body has been fully sent"
is not always a crisp instant: buffering, HTTP/2 flow-control windows, and
intermediaries can blur exactly when the last byte reaches the client, so some
servers cannot report completion precisely. The load-bearing fact -- whether a
response B<started> -- is the MUST (C<response_started>); completion is a
convenience layered on top, which is also why it shares the SHOULD level of its
callback form C<on_complete>. A future revision may promote both to MUST if
implementation experience shows the timing can be pinned down reliably across
transports.

B<C<disconnect_future()>> - Returns a Future that resolves, with the reason, on abnormal disconnect -- useful for racing against long-running work, e.g. C<< Future->wait_any($work, $conn->disconnect_future) >>. Returns C<undef> if not supported.

    my $future = $conn->disconnect_future;  # Future or undef
    my $reason = await $future;

Each returned Future MUST be B<cancellation-isolated>: cancelling it --
directly, or implicitly as the losing component of a combinator such as
C<< Future->wait_any >> -- MUST NOT affect the server's disconnect
processing, the delivery of other notifications, or a Future returned by
another call to this method. Successive calls MAY return distinct
Futures; obtain a fresh one for each race, since a Future that lost one
race cannot win a later one. This makes explicit what the definition
above already promises: I<every> returned Future "resolves, with the
reason, on abnormal disconnect" -- a Future one consumer can cancel out
from under the others cannot keep that promise, and step 3 of
L</State Transition Order> cannot be performed on it.

=head4 What "connection" means here

The object is named for the connection, but it is scoped to a B<single request>:
each request gets its own C<pagi.connection>, and the C<response_started> /
C<response_complete> flags describe B<that request's> response, never the transport
as a whole. This matters the moment a transport carries more than one request:

=over 4

=item *

B<HTTP/1.1> reuses a connection for requests B<sequentially> (keep-alive); each
request is a fresh scope with its own connection object.

=item *

B<HTTP/2> B<multiplexes> many requests over one TCP connection as independent
streams. PAGI maps each stream to its own C<http> scope (see L</HTTP/2 Stream
Mapping>), so ten concurrent streams have ten connection objects and ten
independent C<response_started> flags -- one request starting its response is never
visible as another's.

=item *

B<HTTP/3> (when supported) multiplexes streams over QUIC the same way; the
per-request scoping is identical, so nothing here changes.

=back

The C<response_started> / C<response_complete> flags are defined for the C<http>
scope only. B<SSE> is treated as its own scope type (C<< type => 'sse' >>) even
though it is really a long-lived response over HTTP: it begins with C<sse.start>
rather than C<http.response.start>, so these HTTP flags do not apply to it. An
SSE-specific equivalent may be defined later if a need arises.

=head4 Standard Disconnect Reasons

Every reason below describes an B<abnormal> end to the request -- the connection
went away, a timeout fired, or the server aborted the exchange before the
response was fully delivered. These are the values reported by
C<disconnect_reason()>, passed to C<on_disconnect> callbacks, and resolved from
C<disconnect_future()>. A request that completes successfully has B<no> reason
(C<disconnect_reason()> returns C<undef> and C<on_complete> fires instead).

Servers MUST use these standard reason strings where the condition applies:

=over 4

=item C<client_closed>

Client initiated clean close (TCP FIN) mid-request

=item C<client_timeout>

Client stopped responding (read timeout)

=item C<idle_timeout>

Connection or stream idle too long -- before a request arrived, or
mid-stream on a quiet WebSocket or SSE session

=item C<keepalive_timeout>

Keep-alive connection idled out between requests, or a WebSocket
keepalive ping received no pong within its timeout (see
C<websocket.keepalive>)

=item C<write_timeout>

Response write timed out

=item C<write_error>

Socket write failed (EPIPE, ECONNRESET)

=item C<read_error>

Socket read failed

=item C<protocol_error>

HTTP parse error, invalid request

=item C<server_shutdown>

Server shutting down gracefully

=item C<server_error>

Unhandled server-side error aborted the request

=item C<body_too_large>

Request body exceeded limit

=item C<queue_overflow>

A bounded server queue (e.g. outgoing frames or buffered events) overflowed and the connection was dropped to relieve backpressure

=back

Servers MAY define additional reasons prefixed with C<x-> (e.g., C<x-rate-limited>).

=head4 Server Requirements

=over

=item 1.

MUST provide C<pagi.connection> in scope for C<http> type requests

=item 2.

MUST implement C<is_connected()>, C<disconnect_reason()>, and C<on_disconnect()> methods

=item 3.

SHOULD implement C<on_complete()> and C<disconnect_future()>

=item 4.

MUST fire C<on_disconnect> callbacks B<only> on abnormal disconnect, and C<on_complete> callbacks B<only> on successful completion -- never both for the same request

=item 5.

MUST update connection state as soon as disconnect is detected

=item 6.

MUST use standard reason strings where applicable

=item 7.

MUST NOT transition C<is_connected()> back to true once false (one-way transition)

=back

=head4 State Transition Order

When an B<abnormal disconnect> is detected, servers MUST update state in this order:

=over

=item 1.

Set C<is_connected()> to return false

=item 2.

Set C<disconnect_reason()> to return the reason string

=item 3.

Resolve C<disconnect_future()> with the reason (if provided)

=item 4.

Invoke C<on_disconnect> callbacks in registration order

=item 5.

Deliver the disconnect event: append C<http.disconnect> to the receive queue,
and if a receive Future is already pending, resolve it with that event

=item 6.

Settle any send Future still pending for this scope, per the Send Completion
Contract in L<PAGI::Spec> (it resolves successfully)

=back

Steps 5 and 6 MAY be performed in either order, but both MUST happen after
step 4.

An application coroutine resumed by step 5 or step 6 therefore always
observes the transition already complete: C<is_connected()> returns false,
C<disconnect_reason()> returns the reason, and any registered
C<on_disconnect> callbacks have been invoked or scheduled. Awaiting C<$send>
or C<$receive> and then consulting the connection state is race-free by
construction.

When a request B<completes successfully>, servers MUST invoke C<on_complete>
callbacks in registration order and MUST leave C<disconnect_reason()> as
C<undef>. C<on_disconnect> callbacks and C<disconnect_future()> MUST NOT fire on
this path.

B<Exceptions after the terminal event.> Delivery defines completion: an
exception raised by the application B<after> its response reached the
terminal event (the final body, a C<file>/C<fh> body, or the final
trailers) does not un-complete the request. The server MUST still treat
the request as completed — C<on_complete> fires, C<disconnect_reason()>
stays C<undef> — and SHOULD log the exception at error level as an
operational event. The server MAY close the connection afterwards rather
than reuse it; the delivered response is unaffected either way. (This
matches ASGI-ecosystem practice, where a post-completion exception is
logged and the already-granted keep-alive stands.)

=head4 Applicability

=over 4

=item C<http>

MUST provide C<pagi.connection>

=item C<websocket>

NOT APPLICABLE (use C<websocket.disconnect> events)

=item C<sse>

NOT APPLICABLE (use C<sse.disconnect> events)

=back

WebSocket and SSE have dedicated disconnect events and typically use handler objects (like C<PAGI::WebSocket>, C<PAGI::SSE>) that already manage connection state.

=head4 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 });
    }

=head4 Example: Cleanup on Disconnect vs. Completion

    async sub handler {
        my ($scope, $receive, $send) = @_;
        my $conn = $scope->{'pagi.connection'};

        my $temp_file = create_temp_file();

        # Abnormal end: client vanished, or a timeout/error fired mid-request.
        $conn->on_disconnect(sub {
            my ($reason) = @_;
            $temp_file->unlink;
            log_info("Aborted, client gone: $reason");
        });

        # Clean end: the response was fully delivered.
        $conn->on_complete(sub {
            $temp_file->unlink;
            log_info("Delivered OK");
        });

        my $result = await process_data($temp_file);
        await send_response($send, $result);
    }

Exactly one of the two callbacks runs, so the temp file is always removed
exactly once -- no manual cleanup in the request body, and no risk of a
double-unlink.

=head4 Example: Racing Against Disconnect

    async sub long_poll_handler {
        my ($scope, $receive, $send) = @_;
        my $conn = $scope->{'pagi.connection'};

        my $disconnect_future = $conn->disconnect_future;
        my $event_future = wait_for_event();

        if ($disconnect_future) {
            # Race: wait for event OR disconnect
            await Future->wait_any($disconnect_future, $event_future);

            return unless $conn->is_connected;
        }

        my $event = $event_future->get;
        await send_response($send, $event);
    }

=head2 WebSocket

WebSocket servers handle fragmentation and PING/PONG messages. Servers MUST wait for a reply to C<websocket.connect> before completing the handshake. If C<websocket.close> is sent instead of C<websocket.accept>, the server MUST reject the connection with HTTP 403.

=head3 WebSocket Connection Scope

=over

=item -

C<type> (String) -- C<"websocket">

=item -

C<< $scope->{pagi}{version} >> (String) -- The PAGI core spec version (e.g. C<'0.5'>)

=item -

C<< $scope->{pagi}{spec_version} >> (String) -- The version of this WebSocket sub-specification (e.g. C<'0.5'>). Optional; if omitted, assume C<'0.1'>.

=item -

C<http_version> (String, default C<"1.1">) -- HTTP version used for handshake

=item -

C<scheme> (String, default C<"ws">) -- URL scheme (C<"ws"> or C<"wss">)

=item -

C<path> (String) -- Decoded path string

=item -

C<raw_path> (Bytes, optional) -- Original path bytes from request

=item -

C<query_string> (Bytes) -- Percent-encoded query string

=item -

C<root_path> (String, default C<"">) -- Mount point for application

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]]) -- Original headers

=item -

C<client> (ArrayRef[String, Int], optional)

=item -

C<server> (ArrayRef[String, Optional[Int]], optional)

=item -

C<subprotocols> (ArrayRef[String], default C<[]>)

=item -

C<state> (HashRef, optional)

=item -

C<pagi.transport> (Object, optional) -- Outbound flow-control introspection handle, when the server provides one. See L</Transport Flow Control>.

=item -

C<max_frame_size> (Int, optional) -- The server's inbound WebSocket frame
size ceiling in bytes, when the server enforces one. Frames from the peer
exceeding it terminate the connection, so an application that directs its
client's framing (for example, a chunked-upload protocol advertising a
chunk size at handshake) SHOULD size against this value rather than
hard-coding a guess.

=item -

C<max_receive_queue> (Int, optional) -- The server's inbound event-queue
ceiling for this connection, when the server enforces one. Overflow closes
the connection with C<1008>/C<queue_overflow>, so an application doing
expensive per-message work can use this value to apply its own flow
control (acknowledgement throttling, pause messages) before the server
sheds the session.

=item -

C<extensions> (HashRef, default C<{}>) -- Optional server capabilities advertised for this connection (e.g. C<tls>, C<websocket.http.response>). See L<PAGI::Spec::Extensions>.

=back

=head4 Handshake Headers and Subprotocols

The C<headers> arrayref B<must> include all WebSocket handshake headers as raw byte strings, lower-cased, for example:

=over

=item -

C<upgrade>, C<connection>, C<sec-websocket-key>, C<sec-websocket-version>, C<host>, etc.

=item -

C<sec-websocket-protocol> (if present)

=back

The C<subprotocols> key is an arrayref of strings parsed from the C<Sec-WebSocket-Protocol> header by splitting on commas and trimming whitespace. If the header is absent, C<subprotocols> MUST be an empty arrayref.

=head3 WebSocket Events

=head4 Connect - C<receive> event

=over

=item -

C<type> -- C<"websocket.connect">

=back

=head4 Accept - C<send> event

=over

=item -

C<type> -- C<"websocket.accept">

=item -

C<subprotocol> (String, optional)

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]], optional)

=back

=head4 Receive - C<receive> event

=over

=item -

C<type> -- C<"websocket.receive">

=item -

C<bytes> (Bytes, optional)

=item -

C<text> (String, optional)

=back

Exactly one must be non-null.

The server B<must> UTF-8 decode incoming text frames into Unicode characters
for C<text>, and UTF-8 encode outgoing C<text> values to wire format. Binary
frames pass through as raw C<bytes> without encoding transformation.

If a text frame contains invalid UTF-8, the server B<must> fail the WebSocket
connection with close code 1007 (Invalid frame payload data) per RFC 6455.

=head4 Send - C<send> event

=over

=item -

C<type> -- C<"websocket.send">

=item -

C<bytes> (Bytes, optional)

=item -

C<text> (String, optional)

=back

Exactly one of C<bytes> or C<text> must be non-null.

=head4 Keepalive - C<send> event

Enables WebSocket protocol-level ping/pong keepalive. The server sends ping frames (opcode 0x9) at the specified interval. Clients automatically respond with pong frames per RFC 6455 - no application code required.

=over

=item -

C<type> -- C<"websocket.keepalive">

=item -

C<interval> (Number) -- Seconds between ping frames. C<0> disables keepalive.

=item -

C<timeout> (Number, optional) -- Seconds to wait for pong response. If no pong is received within this time, the connection is closed with code 1006 and the application receives a C<websocket.disconnect> event with C<<< reason => 'keepalive_timeout' >>>.

=back

B<Behavior:>

=over

=item -

Multiple C<websocket.keepalive> events update settings (last wins)

=item -

Omitting C<timeout> enables keepalive without dead connection detection (useful for high-latency connections)

=item -

Setting C<<< interval => 0 >>> stops the keepalive timer

=back

B<Example:>

    # Enable keepalive with 30s ping interval, 20s pong timeout
    await $send->({
        type     => 'websocket.keepalive',
        interval => 30,
        timeout  => 20,
    });

=head4 Disconnect - C<receive> event

Sent when the WebSocket connection is closed, either by the client, server, or due to error conditions.

=over

=item -

C<type> -- C<"websocket.disconnect">

=item -

C<code> (Int) -- WebSocket close code per RFC 6455. When the peer sent a close frame, this is its code (or C<1005>, "no status received", if the frame carried none). When the connection dropped or was aborted with no close handshake -- timeout, write error, server shutdown -- servers MUST report C<1006> ("abnormal closure"). When the server itself closes after a protocol violation, C<code> is the close code it sent (e.g. C<1002> for a framing error or C<1007> for an invalid UTF-8 payload).

=item -

C<reason> (String, default empty) -- For a server-detected abnormal close, the matching standard token from L</Standard Disconnect Reasons>. When the peer sent the Close frame, this is the peer's own reason text, which MAY be empty.

=back

The C<code> carries the RFC 6455 close code and C<reason> supplements it with a
standard token (see L</Standard Disconnect Reasons>), so an application can branch
on one reason vocabulary across HTTP, WebSocket, and SSE. How the two pair up
depends on how the connection ended:

=over 4

=item *

B<The peer sent a Close frame.> C<code> is the peer's own close code (C<1005> if
the frame carried none) and C<reason> is the peer's reason text, commonly empty.
The server does not substitute a PAGI token in this case.

=item *

B<Abnormal drop with no close handshake> -- a bare TCP FIN, a timeout, or a write
failure. C<code> is C<1006> ("abnormal closure") and C<reason> is the token for
the condition: C<client_closed>, C<keepalive_timeout>, C<write_timeout>,
C<write_error>, or C<server_shutdown>.

=item *

B<Server-initiated protocol close> -- the server detected a framing or protocol
violation and sent the Close frame itself. C<code> is the RFC 6455 code for the
fault (C<1002> for a framing/protocol error, C<1007> for an invalid UTF-8
payload) with C<reason> C<protocol_error>; a bounded-queue overflow closes with
C<1008> and C<reason> C<queue_overflow>.

=back

These pairings describe the reference server. The binding contract is only that
C<code> is a valid RFC 6455 close code and C<reason>, when the server can name
one, is a token from L</Standard Disconnect Reasons>; applications SHOULD branch
on C<reason> rather than C<code>.

=head4 Disconnected Client - sending after disconnect

How a C<$send> behaves after closure depends on who closed. After the
B<transport> closed -- the peer disconnected, a timeout fired, the server is
shutting down -- a C<$send> is a B<no-op>: it neither delivers data nor
raises, because the application may race the disconnect through no fault of
its own; applications detect a closed WebSocket through the
C<websocket.disconnect> event delivered to C<$receive>. After the
B<application itself> sent C<websocket.close>, further sends are a
programming error and the server MUST fail the C<$send> Future -- the
application cannot race itself.

A send already B<in flight> when the transport closes settles per the Send
Completion Contract in L<PAGI::Spec>: it resolves successfully; the
application learns of the closure from the C<websocket.disconnect> event,
not from the send.

=head4 Close - C<send> event

=over

=item -

C<type> -- C<"websocket.close">

=item -

C<code> (Int, default C<1000>)

=item -

C<reason> (String, default empty)

=back

=head3 WebSocket Protocol Enforcement

A conforming server enforces the framing rules of RFC 6455 and fails the
connection with the appropriate close code when a client violates them. The
reference server sends:

=over

=item -

B<1002 (Protocol Error)> -- a reserved RSV bit is set, a reserved or unknown
opcode is used, a control frame exceeds 125 bytes, or a C<Close> frame carries a
malformed length or an out-of-range close code.

=item -

B<1007 (Invalid Frame Payload Data)> -- a text frame, or a C<Close> frame's
reason, is not valid UTF-8.

=item -

B<1008 (Policy Violation)> -- the application is not draining messages and the
server's inbound queue limit is reached (backpressure shedding).

=back

A frame whose payload exceeds the server's configured C<max_ws_frame_size> fails
the connection; the application observes a C<websocket.disconnect> with code
C<1006> (a server MAY instead send C<1009 Message Too Big>).

B<Transport note:> over HTTP/2 (below) some of these checks are delegated to the
HTTP/2 and frame-parser layers, so the exact code for a given malformation MAY
differ; the UTF-8 (1007) and C<Close>-frame validations apply on both transports.

=head3 WebSocket over HTTP/2 (RFC 8441)

A server MAY accept WebSocket connections over HTTP/2 using the bootstrapping
mechanism of RFC 8441. This is B<transparent to the application>: the C<websocket>
scope and the C<websocket.*> events are identical to HTTP/1.1 and C<http_version>
is C<"2">. The differences are confined to the handshake and live entirely in the
server:

=over

=item -

B<Detection> -- instead of an HTTP/1.1 C<Upgrade>, the client opens an Extended
C<CONNECT> stream (C<:method> = C<CONNECT>, C<:protocol> = C<websocket>), which the
server enables by advertising C<SETTINGS_ENABLE_CONNECT_PROTOCOL>.

=item -

B<Accept> -- C<websocket.accept> is answered with HTTP status B<200>, not the
HTTP/1.1 C<101 Switching Protocols>; C<subprotocol> still maps to
C<Sec-WebSocket-Protocol>, and there is no C<Upgrade> or C<Sec-WebSocket-Accept>
header.

=item -

B<Data and close> -- WebSocket frames travel as HTTP/2 C<DATA> frames on the
stream. Closing the stream (a client C<RST_STREAM>, or END_STREAM) surfaces as a
C<websocket.disconnect>: an abnormal teardown reports code C<1006>, while a clean
WebSocket C<Close> frame carries its own code and reason as on HTTP/1.1.

=back

Servers that do not implement RFC 8441 simply never produce a C<websocket> scope
over HTTP/2; applications need not distinguish the two cases.

=head3 WebSocket Denial Response (extension)

By default an application rejects a handshake by sending C<websocket.close>
before C<websocket.accept>, and the server replies with a bare
C<403 Forbidden>. The B<denial response> extension lets the application reject
with a B<custom> HTTP response instead -- a C<401> with a C<WWW-Authenticate>
header, a C<429> with C<Retry-After>, a redirect, a JSON error body, and so on.
It mirrors ASGI's C<websocket.http.response> extension.

A server that supports it advertises C<<< extensions->{'websocket.http.response'} >>>
as an empty hashref on the C<websocket> scope. To deny, the application sends two
events B<in place of> C<websocket.accept>, only valid before the connection is
accepted:

=over

=item -

B<C<websocket.http.response.start>> -- C<{ status (Int), headers (ArrayRef[ArrayRef[Bytes]]) }>,
structured exactly like C<http.response.start>.

=item -

B<C<websocket.http.response.body>> -- C<{ body (Bytes, default ""), more (Int, default 0) }>,
structured exactly like C<http.response.body> (only the C<body> form applies;
C<file>/C<fh> are not used for denial responses); set C<<< more => 1 >>> to send
the body in multiple events.

=back

The server sends the resulting HTTP response and then closes the connection; the
WebSocket is never established, so no C<websocket.disconnect> is delivered. After
C<websocket.accept> these events are invalid and the server MUST fail the
C<$send> Future without transmitting anything or disturbing the established
WebSocket -- the same fail-don't-mutate rule as every other out-of-sequence
send.

    # Reject the handshake with a custom 401 instead of a bare 403:
    my $connect = await $receive->();   # websocket.connect
    if (!authorized($scope)) {
        await $send->({
            type    => 'websocket.http.response.start',
            status  => 401,
            headers => [
                [ 'content-type',     'application/json' ],
                [ 'www-authenticate', 'Bearer' ],
            ],
        });
        await $send->({
            type => 'websocket.http.response.body',
            body => '{"error":"unauthorized"}',
        });
        return;
    }
    await $send->({ type => 'websocket.accept' });

Applications SHOULD check for the extension and fall back to C<websocket.close>
when it is absent:

    if ($scope->{extensions}{'websocket.http.response'}) { ... deny with a response ... }
    else                                                 { await $send->({ type => 'websocket.close' }) }

The C<PAGI::WebSocket> helper in C<PAGI-Tools> wraps this as
C<< $ws->deny(status => 401, headers => [...], body => ...) >>, falling back to a
plain close when the server does not advertise the extension.

=head2 Server-Sent Events (SSE)

SSE connections stream C<text/event-stream> data to clients.

=head3 SSE Connection Detection

PAGI servers MUST detect SSE requests and assign a scope of type C<sse> when all of the following are true:

=over

=item -

The request carries the B<SSE client signal>: parsing the C<Accept> header
value (values from repeated C<Accept> headers are combined) as a
comma-separated list of media ranges per RFC 9110 S12.5.1, the B<exact>
range C<text/event-stream> appears with an effective quality value greater
than zero. A range with C<q=0> is an explicit refusal and does not signal
SSE. Media-type parameters other than C<q> are ignored for this test, as is
case (media types are case-insensitive).

=item -

Wildcard ranges B<never> satisfy the test: neither C<*/*> nor C<text/*>
signals SSE. Browsers send C<*/*> on ordinary navigation; a rule that
counted wildcards would classify every plain page load as an SSE request.

=item -

The request has not been upgraded to WebSocket.

=back

Otherwise the connection uses a normal C<http> scope.

This is a B<boolean client-signal check, not content negotiation>. Real SSE
clients are unambiguous -- WHATWG requires C<EventSource> to send exactly
C<Accept: text/event-stream>, and C<fetch-event-source> does the same -- so
the server only asks "did the client ask for an event stream?", never "which
representation does the client prefer?". Preference ranking among acceptable
types (a client that accepts both C<text/event-stream> and C<text/html>,
say) is the application's business, and fuller RFC 9110 content negotiation
MAY be layered on as middleware. When the boolean signal classifies a
request as C<sse> that the application would rather answer with an ordinary
HTTP response, the decline mechanism (see L</Decline SSE - C<send> event>)
is the correction path.

B<Middleware note:> because C<http> and C<sse> are sibling scope types for
what is on the wire an ordinary HTTP request, middleware that filters on
C<< $scope->{type} eq 'http' >> silently skips SSE requests to the same
URL. Middleware guarding HTTP endpoints (authentication, rate limiting,
logging) SHOULD match both types.

B<Note on HTTP methods:> SSE works with any HTTP method, not just GET. While the browser's native C<EventSource> API only supports GET, libraries like Microsoft's C<fetch-event-source> (used by htmx 4, datastar, and others) enable SSE over POST, PUT, and other methods via the Fetch API. PAGI servers MUST support SSE for all HTTP methods to enable these modern patterns.

Routing based on URL or application logic is not used to infer SSE.

=head3 SSE Connection Scope

SSE scopes reuse the HTTP scope structure. Servers MUST populate the same keys (C<http_version>, C<method>, C<scheme>, C<path>, C<headers>, C<client>, C<server>, C<state>, the C<pagi> dict, C<pagi.transport> where provided, and C<extensions>) but set C<<< type => 'sse' >>>. Header casing rules follow the HTTP section.

=head3 Request Body - C<receive> event

For SSE requests with a body (POST, PUT, etc.), the application receives the body via C<sse.request> events, similar to HTTP:

=over

=item -

C<type> -- C<"sse.request">

=item -

C<body> (Bytes, default C<"">) -- Request body chunk

=item -

C<more> (Int, default C<0>) -- C<1> if more body data is forthcoming, otherwise C<0>

=back

For GET requests (no body), a single C<sse.request> event with empty body and C<<< more => 0 >>> is returned.

B<Example (POST SSE with htmx/datastar):>

    my $event = await $receive->();
    if ($event->{type} eq 'sse.request') {
        my $body = $event->{body};
        # Parse JSON body, extract query parameters, etc.
    }

    await $send->({ type => 'sse.start', status => 200 });
    # ... send SSE events based on POST body ...

=head3 Start SSE - C<send> event

C<sse.start> replaces C<http.response.start> for SSE connections and MUST be sent before any C<sse.send> events.

=over

=item -

C<type> -- C<"sse.start">

=item -

C<status> (Int, default C<200>)

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]]) -- Must include C<<< content-type => 'text/event-stream' >>> unless already supplied by middleware.

=back

On C<sse.start> the server supplies the headers a well-behaved event stream needs
when the application did not: C<Content-Type: text/event-stream> (if absent),
C<Cache-Control: no-cache>, and a C<Date> header. Over HTTP/1.1 it additionally
sends C<Connection: keep-alive> and frames the stream with chunked
C<Transfer-Encoding>; HTTP/2 multiplexes and DATA-frames the stream, so those
connection-specific headers do not apply. Applications need not set any of these.

=head3 Decline SSE - C<send> event

An C<sse> request is an ordinary HTTP C<GET> with C<Accept: text/event-stream>;
the server classifies it as an C<sse> scope before the application runs. Instead
of starting an event stream, an application B<may> decline and return a normal
HTTP response -- a C<404> for an unknown endpoint, a C<401>/C<403> for an
unauthenticated request, a C<204> to tell an C<EventSource> client to stop
reconnecting, a redirect, and so on. It sends two events B<in place of>
C<sse.start>, valid only before the stream has started:

=over

=item -

B<C<sse.http.response.start>> -- C<{ status (Int), headers (ArrayRef[ArrayRef[Bytes]]) }>,
structured exactly like C<http.response.start>.

=item -

B<C<sse.http.response.body>> -- C<{ body (Bytes, default ""), more (Int, default 0) }>,
structured exactly like C<http.response.body>; set C<<< more => 1 >>> to send the
body in multiple events.

=back

These events are namespaced under C<sse.> -- mirroring the
C<websocket.http.response> denial events -- so that an C<sse> scope only ever
carries C<sse.*> events, keeping the server's per-scope dispatch and any
event-type-aware middleware scope-separated.

The server sends the resulting HTTP response and closes the connection; no event
stream is started and no C<sse.disconnect> is delivered. This is a
B<first-send-wins> choice: after C<sse.start>, C<sse.http.response.*> events are
invalid and MUST raise; after C<sse.http.response.start>, the stream events
(C<sse.send> / C<sse.comment> / C<sse.keepalive> / C<sse.close>) are invalid and
MUST raise.

Unlike the WebSocket denial response -- an optional extension with a
C<websocket.close> fallback -- declining an SSE request is a B<core> capability:
there is no event-stream fallback, so every conforming server MUST support it.

    # Decline an unauthenticated SSE request with a 401 instead of streaming:
    if (!authorized($scope)) {
        await $send->({
            type    => 'sse.http.response.start',
            status  => 401,
            headers => [
                [ 'content-type',     'text/plain' ],
                [ 'www-authenticate', 'Bearer' ],
            ],
        });
        await $send->({ type => 'sse.http.response.body', body => 'Unauthorized' });
        return;
    }
    await $send->({ type => 'sse.start', status => 200 });

A C<204 No Content> (or any non-C<200>) response is the supported way to stop an
C<EventSource> client's reconnection loop: the client treats it as a terminal
failure and does not reconnect.

=head3 Send SSE - C<send> event

C<sse.send> emits a single SSE dispatch. Fields marked "String" are Unicode strings per the core data-type rules and MUST be UTF-8 encoded by the server before transmission.

=over

=item -

C<type> -- C<"sse.send">

=item -

C<event> (String, optional)

=item -

C<data> (String) -- Required text payload

=item -

C<id> (String, optional)

=item -

C<retry> (Int, optional) -- Milliseconds for the C<retry:> directive

=back

B<Field validation.> The C<event> and C<id> fields MUST NOT contain newline
(C<\n> or C<\r>) characters: a newline there would forge an SSE frame boundary and
let one dispatch inject another. A server MUST reject such a value -- the reference
server raises, failing the C<send>. This is the SSE-framing case of the same byte
safety the server enforces on response headers (see L</Response Start - C<send>
event>). C<retry> MUST be a non-negative integer.
C<data> is the one field that legitimately contains newlines: the server splits it
on newlines and emits one C<data:> line per segment, preserving multi-line
payloads.

To end the SSE stream the application either returns after its final C<sse.send>,
or sends C<sse.close> explicitly (see L</Close SSE - C<send> event>). On return the
server flushes buffered events and closes the HTTP connection; C<sse.close> does
the same immediately, decoupled from the application returning.

=head3 SSE Comment - C<send> event

C<sse.comment> sends an SSE comment line. Comments start with a colon (C<:>) and are used for keepalive pings or protocol-level messages. Comments do NOT trigger the client's C<onmessage> handler in browsers, making them ideal for connection maintenance.

=over

=item -

C<type> -- C<"sse.comment">

=item -

C<comment> (String) -- Comment text. If the text does not start with C<:>, the server MUST prepend one.

=back

B<Example:>

    # Keepalive ping (no browser callback triggered)
    await $send->({
        type    => 'sse.comment',
        comment => ':keepalive',
    });

The server emits the comment followed by two newlines (C<:keepalive\n\n>). This keeps the connection alive through proxies without triggering application-level event handlers on the client.

=head3 SSE Keepalive - C<send> event

Enables automatic SSE keepalive comments. The server sends comment lines at the specified interval to prevent proxy/load balancer timeouts on idle connections.

=over

=item -

C<type> -- C<"sse.keepalive">

=item -

C<interval> (Number) -- Seconds between keepalive comments. C<0> disables keepalive.

=item -

C<comment> (String, default C<''>) -- Comment text to send. Empty string sends just C<:> followed by newlines.

=back

B<Behavior:>

=over

=item -

Multiple C<sse.keepalive> events update settings (last wins)

=item -

Setting C<<< interval => 0 >>> stops the keepalive timer

=item -

Comments do not trigger client's C<onmessage> handler

=back

B<Example:>

    # Enable keepalive with 30s interval
    await $send->({
        type     => 'sse.keepalive',
        interval => 30,
        comment  => 'ping',
    });

=head3 Close SSE - C<send> event

C<sse.close> ends the SSE stream explicitly. It is the SSE analog of
C<http.response.body> with C<<< more => 0 >>>: the server flushes any buffered
events, writes the end-of-stream marker, and closes the stream. An application
B<may> end a stream either by returning after its final C<sse.send> or by sending
C<sse.close>; the latter is the explicit form and ends the stream B<immediately>,
decoupled from the application returning -- which lets a helper deep in the call
stack end the stream without unwinding to the top.

=over

=item -

C<type> -- C<"sse.close">

=item -

C<reason> (String, optional) -- A server-side reason for logging, metrics, or
tracing (for example C<"job_complete">). The SSE wire protocol has no close frame,
so C<reason> is B<never transmitted to the client>; it is server-side metadata
only, surfaced through the same channels as the C<sse.disconnect> reason. To convey
anything to the client -- including "stop reconnecting" -- send an ordinary
C<sse.send> event before closing and use an HTTP C<204> (or C<retry:>) on the
client's next connection.

=back

After C<sse.close>, any further C<sse.send>, C<sse.comment>, or C<sse.keepalive>
on the same connection B<MUST> raise -- the stream is closed. C<sse.close> is
B<idempotent>: a second C<sse.close> is ignored. C<sse.close> does not by itself
stop the client reconnecting; that is governed by the response to the client's
next connection, not by the close of the current stream.

Unlike C<websocket.close>, C<sse.close> carries B<no> code or client-visible
reason: WebSocket has an RFC 6455 close frame to carry them, while SSE has no such
frame on the wire.

B<Return vs C<sse.close>.> Both terminate the stream and are B<identical on the
wire>: the server writes the end-of-stream marker (the chunked terminator over
HTTP/1.1, C<END_STREAM> over HTTP/2) and closes the stream. A client cannot tell
which path was taken -- C<sse.close> adds nothing to the wire, since its C<reason>
never leaves the server. They differ only in who signals the end, when, and with
what server-side metadata:

=over

=item -

B<Return> sends no end-event: the server infers completion from the application's
returned Future resolving and tears the stream down then. The end is tied to the
application returning.

=item -

B<C<sse.close>> is an explicit C<send> event: the server tears the stream down
B<immediately> on receipt, B<before> the application returns. This lets a helper
deep in the call stack end the stream in place (without unwinding to a return),
frees the client's connection ahead of any slow post-close cleanup the application
still has to perform, and records the C<reason> for the server's access log. After
C<sse.close>, further sends raise (see above) and a subsequent return is a no-op.

=back

When the application's final act is its last C<sse.send> immediately followed by a
return, the two paths are equivalent apart from the server-side C<reason>.

=head3 SSE Disconnect - C<receive> event

Sent to the application if the client disconnects or if the server shuts down the SSE stream after C<sse.start>.

=over

=item -

C<type> -- C<"sse.disconnect">

=item -

C<reason> (String) -- Standard reason string from L</Standard Disconnect Reasons>

=back

B<Common reasons:>

=over 4

=item C<client_closed>

Client closed the connection

=item C<write_error>

Failed to write to the socket (keepalive or event)

=item C<write_timeout>

Send timeout exceeded

=item C<server_shutdown>

Server shut down the stream

=back

=head2 Transport Flow Control

The C<pagi.transport> scope key provides a synchronous, read-only handle for
inspecting B<outbound flow control> -- how many bytes the server has queued for
the client but not yet written to the network. It lets an application observe
backpressure and make its own delivery decisions (conflate, coalesce, shed
load, or disconnect a slow client) instead of only blocking until the buffer
drains. It is the server-side analogue of the browser WebSocket API's
C<bufferedAmount>, generalized across the C<http>, C<websocket>, and C<sse>
scope types.

Servers SHOULD provide C<pagi.transport> for C<http>, C<websocket>, and C<sse>
scopes. A server that cannot determine its outbound buffer state omits the key;
applications MUST treat its absence as "flow-control introspection unavailable"
(and the high-level helpers report a buffered amount of C<0>).

The presence of C<pagi.transport> affects introspection, not the fundamental
send contract. Applications self-pace by awaiting every C<$send> Future whether
or not this object is present; the object additionally lets them observe queue
depth and make an earlier policy decision to conflate, coalesce, shed load, or
disconnect.

The long-term goal is universal coverage: every scope type that produces a
stream of outbound events provides C<pagi.transport>, across every transport a
server supports, so an application need not know which transport or protocol
version carries its events. A missing key therefore signals a B<capability
limitation> -- a server still being built out, or a transport whose outbound
buffer the server cannot yet measure -- not a steady state for applications to
design around. Write transport-agnostic code: treat the key's presence as the
norm and tolerate its absence per the contract above.

=head3 Methods

B<C<buffered_amount()>> -- Returns the number of bytes queued for the client but
not yet written to the network, as an integer; C<0> when the send buffer is
fully drained. This is a synchronous, non-blocking, non-destructive read -- it
neither sends, receives, nor consumes queued messages.

    my $pending = $scope->{'pagi.transport'}->buffered_amount;

If a server provides C<pagi.transport>, it B<must> implement C<buffered_amount>.

B<C<high_water_mark()>> -- Returns the buffered-byte threshold at or above which
the server applies backpressure: the Future returned by a C<$send> that would
exceed it remains pending until the buffer drains enough for the server to
continue processing the event. Awaiting that Future suspends the application
coroutine, not the Perl thread. Applications use the threshold relative to the
ceiling rather than hard-coding a byte count. Returns C<undef> if the server has
no fixed high-water mark.

B<C<low_water_mark()>> -- Returns the buffered-byte threshold the buffer must
fall back to before the server releases backpressure (the drain point). Returns
C<undef> if not applicable.

Servers SHOULD implement C<high_water_mark> and C<low_water_mark> where they use
a watermark-based backpressure model.

=head3 Backpressure callbacks

The synchronous reads above suit a producer that checks before each send. A
producer that B<cannot> self-pace with a blocking C<$send> -- an event-driven
source that pushes a message per incoming event, say -- instead wants to be
B<told> when to pause and resume. For that, the handle provides two
edge-triggered callbacks.

B<C<on_high_water($callback)>> -- Registers a callback invoked when the outbound
buffer reaches or exceeds C<high_water_mark> (backpressure engaged). If the
buffer is already at or above the mark when the callback is registered, the
callback is invoked immediately.

    $transport->on_high_water(sub { $source->pause });

B<C<on_drain($callback)>> -- Registers a callback invoked when the outbound
buffer falls back below C<low_water_mark> after having reached the high mark
(backpressure released). It is not invoked merely because the buffer is below
the low mark at registration time -- only on an actual high-then-low transition.

    $transport->on_drain(sub { $source->resume });

The two form a hysteresis cycle: C<on_high_water> fires once when the buffer
crosses up to the high mark, then C<on_drain> fires once when it falls back
below the low mark, then the cycle re-arms. The gap between the marks prevents
flapping when the buffered amount hovers near a single threshold. Multiple
callbacks may be registered for each and are invoked in registration order; the
callbacks receive no arguments (read C<buffered_amount> if the current depth is
needed).

Servers that provide C<pagi.transport> with a watermark-based backpressure model
SHOULD implement C<on_high_water> and C<on_drain>.

=head3 Applicability

=over 4

=item C<http>

Bytes queued for the current (streaming) response.

=item C<websocket>

Bytes queued for the WebSocket session.

=item C<sse>

Bytes queued for the SSE stream.

=back

=head3 Example: conflating a live feed under backpressure

When a client falls behind on a high-frequency stream, sending the next update
only while the backlog is small keeps the client B<current> (sparse but fresh)
rather than B<stale> (dense but lagging):

    my $transport = $scope->{'pagi.transport'};

    while (1) {
        my $update = await next_update();

        # Skip this frame if the client is already behind; it will get the
        # next, fresher one. Threshold relative to the server's ceiling.
        if ($transport) {
            my $ceiling = $transport->high_water_mark // 65536;
            next if $transport->buffered_amount > $ceiling / 2;
        }

        await $send->({ type => 'websocket.send', text => $update });
    }

The high-level helpers in L<PAGI::WebSocket>, L<PAGI::SSE>, and L<PAGI::Request>
expose these as C<< $obj->buffered_amount >>, C<< $obj->high_water_mark >>, and
C<< $obj->low_water_mark >>.

=head2 PAGI to PSGI Compatibility

PAGI translates keys explicitly to maintain compatibility with PSGI:

=over

=item -

C<REQUEST_METHOD> -> C<method>

=item -

C<SCRIPT_NAME> -> C<root_path>

=item -

C<PATH_INFO> -> C<path> minus C<root_path>

=item -

C<QUERY_STRING> -> C<query_string>

=item -

C<CONTENT_TYPE> -> extracted from C<headers>

=item -

C<CONTENT_LENGTH> -> extracted from C<headers>

=item -

C<SERVER_NAME>, C<SERVER_PORT> -> C<server>

=item -

C<REMOTE_ADDR>, C<REMOTE_PORT> -> C<client>

=item -

C<SERVER_PROTOCOL> -> C<http_version>

=item -

C<psgi.url_scheme> -> C<scheme>

=item -

C<psgi.version> -> C<[1, 1]> (PAGI servers MUST advertise the PSGI version they emulate when bridging)

=item -

C<psgi.input> -> constructed from C<http.request> events

=item -

C<psgi.errors> -> handled by the server as appropriate

=item -

C<psgi.streaming>, C<psgi.nonblocking>, C<psgi.multithread>, C<psgi.multiprocess> -> derived from PAGI server capabilities and advertised via PSGI adapter docs

=back

Response mappings:

=over

=item -

C<status> and C<headers> map directly to C<http.response.start>

=item -

Body content from PSGI maps directly to C<http.response.body> messages.

=back

=head2 PAGI Encoding Differences

=over

=item -

C<path>: Decoded UTF-8 string from percent-encoded input. The server first
percent-decodes C<raw_path>, then attempts UTF-8 decoding of the resulting
bytes into Unicode characters. If the bytes are not valid UTF-8, the server
B<should> fall back to the original percent-decoded bytes rather than
replacing invalid sequences or rejecting the request (Mojolicious-style
fallback). Applications needing strict UTF-8 validation can check C<raw_path>
and decode themselves with C<Encode::FB_CROAK>.

=item -

C<headers>: Represented as bytes exactly as sent/received

=item -

C<query_string>: Raw bytes from URL after C<?>, percent-encoded

=item -

C<root_path>: Unicode path string matching C<SCRIPT_NAME>

=back

=head2 Version History

=over

=item -

C<0.5> (Draft): Response body payload kinds do not mix -- inline events or one
C<file>/C<fh> event, never both, enforced by the server failing the send.
Intermediaries must not invalidate what the response head promised
(C<< trailers => 1 >>, a C<206>/C<Content-Range> range) and must preserve a
re-emitted event's other keys. States which incomplete-response signal governs
forwarding and which governs diagnostics.

=item -

C<0.4> (Draft): Settlement of in-flight I/O at disconnect -- pending sends
resolve, pending receives resolve with the disconnect event, both ordered
after the connection-state transition; loop-deferred delivery of disconnect
notifications; C<RST_STREAM> mapped to the standard disconnect transition.

=item -

C<0.3> (Draft): C<pagi.transport> flow control; C<websocket.http.response>
denial-response extension; C<on_complete> plus reworked abnormal-only disconnect
reasons; C<response_started> (MUST) and C<response_complete> (SHOULD) connection-state
accessors for per-request response progress; documented server-supplied behaviors,
HEAD response semantics, incomplete-response handling, and WebSocket over HTTP/2;
tightened SSE detection to an exact media-range match with C<< q > 0 >>;
removed C<pagi.features> and the per-send C<timeout> field.

=item -

C<0.2> (Draft): SSE POST method support (C<sse.request>), keepalive events,
disconnect reasons, C<pagi.connection>, clarified scope fields.

=item -

C<0.1> (Draft): Initial draft based on ASGI 2.5, supporting HTTP, WebSocket, and SSE.

=back

=head2 Copyright

This document has been placed in the public domain.
