Introduction
PAGI::Server 0.002007 is the largest release of the server since the
distribution split. Almost none of it is new features. Instead, it is the
result of a systematic conformance pass: every behavior the PAGI
specification describes was checked against what the server actually does,
on both HTTP/1.1 and HTTP/2, and every divergence was resolved — in the
server, or occasionally in the spec itself when the server's behavior
turned out to be the better answer.
Before the details, the practical takeaway: existing applications
should just work — they'll simply work better. The vast majority of
these fixes live in corners most applications never visit: race windows,
error paths, teardown ordering, multi-frame timing on a busy stream. If
your application worked on 0.002006, we expect it to run unchanged on
0.002007, and to behave better in exactly the situations you'd never
want to debug — a client vanishing mid-response, a socket error at the
wrong moment, two WebSocket frames landing close together. The handful of
deliberate breaking changes are loud and cheap to fix (details in
UPGRADING and the summary below).
The organizing principle for the whole release is normalization: from
the application's perspective, the transport should be invisible. A PAGI
application receives a scope, awaits $receive, and awaits $send — and
the events it sees, the errors it gets, and the guarantees it can rely on
should be identical whether the connection underneath is HTTP/1.1 or an
HTTP/2 stream. Before this release that was mostly true. Now it is the
tested, enforced rule.
Three highlights from the pass.
1. Event validation is now mandatory — on every send path
Previous releases had a validate_events option: turn it on in
development, turn it off in production. The conformance pass killed that
split. Outgoing-event validation now runs unconditionally on all five send
paths — HTTP/1.1, HTTP/2, WebSocket, SSE, and lifespan — and
validate_events is deprecated and ignored.
Malformed events, unknown event types, events for extensions the server
never advertised, and — most importantly — mis-sequenced events now fail
the $send Future everywhere: a body chunk before http.response.start,
a duplicate response start, a body after the response completed, trailers
that were never declared, a lifespan result sent in the wrong phase. All
of these used to be silently ignored or quietly defaulted on at least one
path. Silent acceptance is the worst possible behavior for a protocol
server: the bug surfaces later, on the wire, as a hung client or a
corrupted response, far from the line of application code that caused it.
Failing the Future puts the error exactly where the mistake was made.
This is a deliberate statement about what PAGI::Server is.
PAGI::Server is intended to be the canonical reference implementation of
the PAGI specification, and a reference implementation should be
hard-core: if the spec says an event sequence is invalid, this server
rejects it, full stop — even if that enforcement were to cost
performance. If an application runs cleanly here, it is conforming; that
guarantee is worth more than a benchmark number. Other PAGI servers are
free to make the opposite trade — skip validation, run looser and
faster — and deployments that want both can develop against
PAGI::Server's strictness and park a looser server behind nginx or
another fronting proxy in production. The ecosystem gets to choose;
the reference implementation doesn't.
Note what this means for existing code: a correct application sees no
difference at all. The only applications that can observe mandatory
validation are ones that were already sending events the server was
silently mis-handling — for them, an invisible bug becomes a visible
error with a location attached. That's the release in miniature.
As it happens, the performance worry about always-on validation turned
out to be unfounded anyway — more on that in the summary.
2. Normalizing the WebSocket send path uncovered silent data loss
The best argument for this kind of campaign is what it finds. HTTP/2
WebSocket sends used to write each frame directly to nghttp2's
submit_data. What the code didn't account for is that nghttp2 registers
only a single pending-send slot per stream: submitting a second frame
while the first was still draining destroyed the still-queued remainder of
the first instead of queuing behind it. Any two frames close enough
together — an application message racing a pong, a burst of sends under
backpressure — could silently drop or truncate data, with no error raised
anywhere.
The fix was not a patch; it was normalization. The HTTP/2 WebSocket send
path now frames every send through the same per-stream FIFO send queue and
pull-based data-provider model that HTTP/2 response streaming and SSE
already used. One send machine per stream, every frame type included.
That convergence paid for itself twice. Because the queue is the same one
the other streaming paths use, HTTP/2 WebSockets now also get the
pagi.transport extension — buffered_amount, high/low watermarks,
on_high_water/on_drain — matching HTTP/1.1 WebSockets and every other
streaming scope. An application doing its own flow control gets one
transport surface with one set of semantics, on either protocol version.
3. Connection-lifecycle events mean the same thing everywhere
The third area of normalization is what the application is told when
things end. The spec defines a small vocabulary of disconnect reason
tokens, and the server now uses them truthfully and consistently:
- Socket read and write errors report
read_error/write_errorinstead of masquerading asclient_closed— previously every socket error was indistinguishable from a clean peer disconnect. - Server-initiated teardowns report their real cause:
idle_timeout,keepalive_timeout, not a generic token or an ad hoc string. - Disconnect events are delivered exactly once per scope. An HTTP/2
WebSocket stream that hits several closing conditions in sequence
reports only the first; HTTP/1.1 no longer ghosts a second
websocket.disconnectafter a clean close. - An application that starts a response and never finishes it now
triggers the spec's forced abnormal closure on both transports —
HTTP/1.1 closes the connection without a chunked terminator, HTTP/2
sends
RST_STREAM— instead of a silent keep-alive on one and an open stream on the other. Either way the application'son_disconnectfires withserver_error, and the server logs a warning.
The same discipline extended to response headers: application-supplied
connection-specific headers (Connection, Transfer-Encoding, and
friends) are now stripped, with a warning, on every path that maps them.
On HTTP/2 this fixed real breakage — RFC 9113 forbids those fields, and an
application that set Connection: keep-alive out of HTTP/1.1 habit
previously had its response destroyed at the framing layer, the client
receiving nothing but :status.
Summary
0.002007 also brings full HTTP/2 trailer support (via Net::HTTP2::nghttp2
0.009), WebSocket keepalive over HTTP/2 with per-stream ping/pong timers,
SSE connection reuse on HTTP/1.1 keep-alive, and a long list of smaller
fixes — see the Changes file for the complete accounting. A few behaviors
changed in breaking ways where the spec demanded it, most visibly the
removal of lifespan_mode => 'off' and the raised Net::HTTP2::nghttp2
floor for HTTP/2 deployments. Both fail immediately and explicitly at
startup with instructions, not subtly at runtime — if you use neither,
nothing in this release requires you to change anything.
Two closing notes. First, the release gate runs 920 tests across 133 test
files, a large share of them added by this pass — the conformance work was
test-driven throughout, and the transport-parity claims above are asserted
pairwise, not assumed. Second, performance: despite validation now running
unconditionally on every send, benchmarking on the author's development
machine found no performance downside to any of this — if anything, the
cleanup seems to have helped a bit. And that's before the real performance
work lands: the next update is focused squarely on performance, especially
tail latency — those rare slow requests at the far end of the histogram.
Early results are promising. That's the next release's story.
Top comments (0)