# This file documents the revision history for Perl extension PAGI 0.003001 - 2026-10-22 [Packaging] - Requires Perl 5.24 (was 5.18). Future 0.54 requires Perl 5.20, and Future is the one hard dependency of a PAGI application, so the floor had to move; it moves to 5.24 because that is where Future::AsyncAwait passes a cancel on to what an async sub awaits, and where Future::XS is available. "Perl version support" in PAGI.pm describes the support horizon: Future supports Perls at least ten years old, and an older Perl is in grace time. [Specification] - Www: an application MAY cancel a pending $receive Future (as Future->wait_any cancels the loser of a race), and that MUST NOT affect the scope: the cancelled receive consumes no event, a later receive returns the next event, and another pending receive is unaffected. No new specification version. [Documentation] - The specification and its guides no longer teach PAGI-Tools' API; they keep only pointers to it (install lines, the ecosystem description, the PSGI adapter, a see-also link). - PAGI::EventLoops no longer tells applications never to let a race cancel the live $receive; examples 14, 17 and 18 drop the same claim from their comments. - The Tutorial names Future as a PAGI application's only hard dependency, with Future::AsyncAwait as the recommended way to write one. 0.003000 - 2026-10-08 A major release. The specifications it describes, Www 0.6 (since 0.002009) and Lifespan 0.3, add requirements that servers and applications written against earlier versions may not meet. It is released alongside PAGI-Server and PAGI-Tools 0.003000, which implement them; PAGI::Upgrading covers moving an application from Www 0.5 to 0.6. [Packaging] - Installing PAGI no longer installs PAGI-Server and PAGI-Tools. Through the 0.002 releases the distribution kept them as runtime dependencies for code that relied on `requires 'PAGI'` from before the split; it is now the specification only. Require PAGI::Server and/or PAGI::Tools directly (cpanm PAGI::Server PAGI::Tools). [Specification] - Www 0.6: a server shutting down closes each accepted WebSocket with Close 1012 ("Service Restart") and reports 1012 with reason server_shutdown, on every transport, instead of dropping it as 1006. Shutdown joins the other closes the server decides (1002, 1007, 1008, 1011), so a client can tell a planned restart from a crash. - Lifespan 0.3 and Www 0.6: a shutting-down server waits, within its shutdown bound, for the application calls serving its connections to return before lifespan.shutdown, so cleanup an application starts on its disconnect finishes before the process exits. - Www 0.6: a server delivers terminal callbacks (on_disconnect, on_complete, on_end) once the end has happened and MUST NOT hold one until the application returns, since an application may await its own scope's end (as PAGI-Tools' helpers do for their on_close cleanup). [Documentation] - Cookbook: "Cleaning up when the server shuts down", with the new examples/19-shutdown-cleanup -- a WebSocket app that saves each session asynchronously on its server_shutdown disconnect. 0.002013 - 2026-10-04 [Specification] - A reminder in the Middleware section: a reference in a scope or an event is shared by every layer that receives it, and one that outlives the request (built at load time, closed over, a constant) by every request too; change only your own copy. Writing into a handed reference can leak one request's data into another's response. [Documentation] - The PSGI, Building and Tutorial middleware examples send a new event instead of pushing onto the response's headers. The Cookbook gains a MIDDLEWARE section: changing a response on its way out, and reporting back to an outer layer through a reference seeded before calling inward. 0.002012 - 2026-10-01 [Specification] - Clarify path, raw_path and root_path (Www 0.6). root_path followed by path is the path the client requested: whatever sets or extends root_path removes that prefix from path (PSGI's SCRIPT_NAME/PATH_INFO model; PAGI deliberately differs from ASGI, where path includes root_path). raw_path is the full requested path, percent-encoded, at every level: mounts leave it, a server root path is prepended. New section "Paths, Mounts and Root Paths": whole-segment prefixes, never strip a root path "only if present", root_path shape and trust, leading "//", and never authorize on raw_path. SSE scopes list raw_path, query_string and root_path among the keys they reuse. The PSGI mapping said PATH_INFO is "path minus root_path"; it is path, and REQUEST_URI is raw_path plus the query. 0.002011 - 2026-09-30 [Specification] - Transport backpressure callbacks are flow-control signals, not an audit log of buffer changes. Servers MAY evaluate watermarks after immediate, non-blocking write attempts, so a transient high-water crossing that leaves the buffer below the high mark need not fire a callback pair. Once backpressure is engaged, draining below the low mark MUST release waiting sends and invoke armed on_drain callbacks, whether the drain happens through immediate or deferred writes. - Remove the incorrect note that ASGI servers keep a connection alive after a post-completion exception; uvicorn closes it. The rule itself is unchanged: the server MAY close the connection. 0.002010 - 2026-09-22 [Specification] - Connection terminal API: add on_end and cancellation-isolated end_future observers for both clean and abnormal endings, plus WebSocket close_code and close_reason accessors. The connection object is the authoritative terminal record, available without consuming the receive queue. - Deliver all terminal callbacks and terminal Future notifications outside the application's send/receive call stack. Populate terminal facts first; notifications may arrive after the application handler returns. A receive resolved by the application's own terminal send may still resume inside that send, after the synchronous connection facts have been updated. - Resolve every pending receive when the scope ends. Subsequent WebSocket and SSE receives report the end again. After a clean SSE close or refusal, receive returns sse.disconnect without a reason; after a WebSocket HTTP refusal it returns http.disconnect. Servers may document and enforce a bound on repeated terminal receives. - An accepted WebSocket reaches clean completion only after the closing handshake and transport/stream closure. A missing peer Close at the server's deadline reports close_timeout; transport loss before that deadline remains a distinct outcome. - Add close_incomplete for a peer Close followed by transport/stream closure that does not finish within the server's bound. Preserve the peer's code and reason even on an abnormal ending; report 1005 for a peer Close with no code, and 1006/undef when the scope ends without a peer Close. HTTP and SSE close accessors remain undef. - An otherwise valid first application websocket.close racing peer closure succeeds, including when closing is already in progress. Clarify orderly HTTP/2 closure for an abandoned WebSocket stream with RFC 8441 section 5. [What may break] - Applications assuming on_complete has already run when a terminal send returns. Read response_complete for the synchronous fact, or observe on_end/end_future for terminal notification. - Code treating an application's own websocket.close code as the observed peer outcome, or assuming its send alone establishes clean completion. Read close_code/close_reason and disconnect_reason from pagi.connection. - Server implementations must provide the complete terminal API and apply the documented notification ordering and bounded WebSocket closing rules. [Packaging] - Align PAGI.pm's reported version with the distribution version; it had remained at 0.002008 while dist.ini advanced to 0.002009. 0.002009 - 2026-09-08 [Specification] - HTTP/WebSocket/SSE sub-spec 0.5 -> 0.6. New requirements, not clarifications, so $scope->{pagi}{spec_version} moves with them. The core spec version is unchanged. - Core spec wording: a disconnect resolves every pending receive Future, not only one (the Www spec already said so in the plural). - Www spec: a receive() after websocket.disconnect or sse.disconnect resolves with that event again, as http.disconnect already did; the event reports the scope's terminal state, not a message. - Www spec: a receive() after the scope's end reports the end and never invents data or a failure. After a clean end the application produced (a completed refusal, or sse.close) it resolves with sse.disconnect carrying no reason, or http.disconnect after a WebSocket refusal. Previously unspecified. A server MAY bound how many receives it answers this way on one scope. - pagi.connection is provided on every http, websocket, and sse scope; all accessors are required; abort($detail) and disconnect_detail added; new standard reason app_abort; started, clean end, and abnormal end defined per scope; the object's reason and the scope's disconnect event carry the same token. - Refusing a WebSocket handshake or an SSE stream is an ordinary HTTP response sent with the HTTP response events before websocket.accept or sse.start. websocket.http.response.* and sse.http.response.* are removed, along with the websocket.http.response extension. websocket.close before accept is an out-of-sequence send and fails. - sse.disconnect is documented as deliverable before sse.start. - sse.close is the terminal event of a started SSE stream, and the WebSocket closing handshake (websocket.close sent or websocket.disconnect received) is the terminal event of an accepted socket. Returning without them is an incomplete response; a WebSocket so abandoned is closed with code 1011 and websocket.disconnect carries 1011 / server_error. - A WebSocket refusal's status must be 300 or above; a 1xx or 2xx start fails without mutating state (101 and, on HTTP/2, any 2xx to the CONNECT are handshake acceptances). SSE refusals may carry any status. The websocket.disconnect code/reason pairings are normative where the server names the ending; peer Close-frame code and text are never replaced. [Documentation] - New PAGI::Upgrading with the 0.5 -> 0.6 guide for application authors, framework authors, and server implementers, and the known issue with mixed Accept headers. - Tutorial SSE examples use the sub-spec's sse.start and sse.send event names. - The SSE broadcaster and job-runner examples end finite streams with sse.close; the job runner refuses bad requests with an HTTP response and observes the end of the stream through pagi.connection. [What may break] - Refusing a WebSocket handshake or an SSE stream with the websocket.http.response.* or sse.http.response.* events. Those events no longer exist, so a conforming server fails the send. Send http.response.start and http.response.body on the scope before websocket.accept or sse.start instead; no extension check is needed. - Sending websocket.close before websocket.accept to get a bare 403. That send now fails as out of sequence. Refuse with an HTTP response. - Returning from a started SSE stream without sse.close. This was a clean end; it is now an incomplete response: no end-of-stream marker, the connection closes so the truncation is observable, on_disconnect fires with server_error, and the server logs an error. Add sse.close after the final sse.send. Streams that run until the client disconnects are not affected. - Returning from an accepted WebSocket without sending websocket.close or receiving websocket.disconnect. The reference server reported a clean end while the client saw 1006; it is now an incomplete response closed with 1011 and logged. Loops that leave only on websocket.disconnect are not affected. - A WebSocket refusal with a 1xx or 2xx status. Those are handshake acceptances on the wire and the send now fails. - Not affected: http scopes, which already carried pagi.connection; on_disconnect callbacks that ignore the new second argument; code that tested on_complete or disconnect_future for definedness, which is now merely redundant. 0.002008 - 2026-09-01 [Specification] - HTTP/WebSocket/SSE sub-spec 0.4 -> 0.5. These are new requirements, not clarifications, so $scope->{pagi}{spec_version} moves with them. The core spec is unchanged. - The terminal response event is defined as what it asserts: the application's claim that the response is complete (RFC 9110 S6.1), which the server renders as the chunked terminator or END_STREAM. The existing prohibitions follow from that. - The ban on synthesizing terminal framing binds every producer, not only the server. Middleware that buffers and re-emits a body MUST NOT complete an incomplete stream, nor derive an ETag, Content-Length, or other completeness-dependent metadata from the bytes it happened to see. - Says which of the two signals answers which question: absence of the terminal event governs what may be forwarded or synthesized; disconnect_reason governs only whether to raise or log. The previous wording left it to inference, and every consumer inferred it the same way and wrong. - A response body is either inline events or one file/fh event, never both. Mixing them leaves a body-transforming intermediary unable to honour the encoding it already declared; the server now fails such a send. - An intermediary MUST examine the response head before transforming a body or attaching metadata derived from one, and MUST NOT invalidate what that head promised -- trailers => 1 commits the server to chunked framing, and a 206 body is a range rather than the representation. - An intermediary re-emitting a buffered event MUST preserve its other keys, including more, whose default of 0 makes an omission an assertion of completeness. [What may break] - Sending a file/fh body after inline body bytes. Undefined before, so it worked; a conforming server now fails that send. Send the whole body inline instead, or put the leading bytes in the file. - Relying on an intermediary to finish an incomplete response. Middleware may no longer synthesize a terminal event, ETag or Content-Length on an application's behalf, so such a response now stays observably incomplete. - Middleware that transforms bodies without consulting the response head is now non-conforming, even where it happens to work. - Not affected: complete responses, file/fh alone, inline streaming to completion. 0.002007 - 2026-08-29 [Specification clarifications - resolved ambiguities] - Connection State: futures returned by disconnect_future are cancellation-isolated -- cancelling one (directly, or as the losing component of a combinator such as Future->wait_any) never disturbs the server's disconnect processing or any other returned Future. Makes explicit what the accessor's definition already promises; the racing example is correct as written. 0.002006 - 2026-08-28 [Specification - pending I/O at disconnect] - Core spec 0.4 -> 0.5, HTTP/WebSocket/SSE sub-spec 0.3 -> 0.4: defined settlement of send and receive Futures still pending at disconnect (settle by resolving, never cancel), the disconnect-transition ordering invariant, loop-deferred delivery of disconnect notifications, and the corrected HTTP/2 RST_STREAM mapping. See the Send Completion Contract in PAGI::Spec and State Transition Order in PAGI::Spec::Www. 0.002005 - 2026-08-26 [Specification clarifications - resolved ambiguities] - Consolidated the connection-level-headers rule: PAGI defines no header exceptions of its own -- where the HTTP RFCs carve one out (te with the trailers token per RFC 9113, forwarded normalized; the Connection: upgrade companion an Upgrade sender owes per RFC 9110), the server implements the RFC rather than the blanket strip. Replaces the separate 426-companion paragraph and corrects the blanket "strips all of these" overstatement; no server behavior change. 0.002004 - 2026-08-25 [Specification clarifications - resolved ambiguities] - Connection-level headers: over HTTP/1.1, when an application response carries an Upgrade header (e.g. on a 426 Upgrade Required), the server must include 'upgrade' among the tokens of the Connection header it supplies, per RFC 9110's requirement on any Upgrade sender. The application never sends Connection: upgrade itself; the server completes the pair. Closes the conflict between the server-owns-connection-headers rule and RFC 9110's 426 requirements. - The incomplete-response definition now says explicitly that it covers an application Future that fails with an exception, not only one that resolves, after http.response.start and before the terminal event. No behavior change: the neighboring clauses already treat exceptions identically, and the reference server conforms both ways. 0.002003 - 2026-08-24 [Specification clarifications - resolved ambiguities] - http.response.body payload keys (body / file / fh) are at-most-one per event; omitting all three means an empty body chunk. - Defined HEAD response semantics: the application responds as it would to GET and the server suppresses the body on every HTTP version - Content-Length passes through, no chunked framing, file/fh bodies are never opened or statted, trailers are validated and discarded. - A response left incomplete after http.response.start forces abnormal closure on every transport: the server never synthesizes the missing terminal framing, the client observably sees truncation (HTTP/1.1 close without the chunk terminator; HTTP/2 RST_STREAM such as INTERNAL_ERROR), on_disconnect fires with reason server_error, and on_complete never does. - SSE detection is an exact text/event-stream media-range match with q > 0; wildcards and explicit q=0 refusals never signal SSE. - Lifespan strict mode (operator-configured startup-failure-is-fatal) is sanctioned; a decline-tolerant mode remains the required default; an "off" switch that skips the protocol entirely is expressly nonconforming. - Over HTTP/1.1, trailers exist only in chunked framing: a server must reject http.response.trailers on a Content-Length-framed response by failing the send Future, never by silently dropping the trailers. - Delivery defines completion: an application exception raised after the response's terminal event does not un-complete the request - on_complete fires, disconnect_reason stays undef, the server should log the exception and may close the connection. [Specification clarifications - send and delivery contracts] - Sends are sequential: an application must not issue a new send on a connection before the previous send's Future resolves; the effect of overlapping sends is unspecified. - file/fh delivery failure is fail-don't-mutate: a missing or unreadable file, or a read or seek failure, fails that send's Future and leaves the response sequence state as it was, so the application may recover; once body bytes reached the wire the incomplete-response rules govern. The failed Future is the application's notification channel. - Connection-level and framing response headers belong to the server: HTTP/2 strips the connection-specific set from application responses (RFC 9113 forbids them); HTTP/1.1 must ignore or strip application-supplied Transfer-Encoding and Connection, continuing the WSGI/ASGI hop-by-hop lineage. - WebSocket denial events after websocket.accept, and sends after the application's own websocket.close, fail the send Future (the fail-don't-mutate rule); only a transport-initiated closure makes a subsequent send a race-friendly no-op. - Exceptions raised by registered callbacks (on_complete, on_disconnect, and the pagi.transport callbacks) must not prevent remaining callbacks from running or disturb server processing; servers should log them. [Documentation] - Expanded THE PAGI ECOSYSTEM in PAGI.pm: a Built on PAGI section covering Thunderhorse, PAGI::FastAPI, WebDyne, PAGI::Nano, and Uniform::HTMX::PAGI, and an Announcements section pointing at the pagi-announce mailing list. - Reworked UTF-8 HANDLING OVERVIEW in PAGI.pm to lead with the PAGI-Tools version of the example (PAGI::Request/PAGI::Response, where decoding and encoding are encapsulated at the boundary) and to present the raw-interface version as the contract toolkits implement rather than a recommended application style; the raw example now percent-decodes with URI::Escape and handles '+' as space. [Specification additions] - Widened the idle_timeout and keepalive_timeout disconnect-reason definitions to the senses in real-world use: mid-stream idleness on a quiet WebSocket or SSE session, and a WebSocket keepalive ping that received no pong. - Added 503 Service Unavailable (admission control, with Retry-After) to the generated-response catalog. - Listed pagi.transport in the HTTP, WebSocket, and SSE scope-key tables, where it was specified but unlisted. - Added optional WebSocket scope keys max_frame_size and max_receive_queue so applications can size client framing and apply their own flow control against the limits the server actually enforces. 0.002002 - 2026-08-21 [Specification 0.4 Draft] - Distinguished the canonical runtime PAGI code reference from an already-instantiated application-provider object. General-purpose runners should accept either; package-name strings and class-method to_app dispatch are not provider forms. Each loaded provider instance is normalized exactly once before dispatch, and only the resulting code reference crosses the runtime boundary. - Lifecycle handling remains part of the lifespan protocol and is not inferred by PAGI from methods on application-provider objects; deployment tools remain free to define their own non-protocol hooks. - Core and protocol versions now explicitly evolve independently. An omitted pagi.spec_version uses the default owned by the applicable protocol rather than the core version; WWW and Lifespan both use 0.1 as their conservative compatibility default. - Revised PAGI::Spec::Server to version 0.2. PAGI no longer prescribes a new(app => ...) constructor or run method for conforming servers; server construction and plugin compatibility belong to runner-specific APIs. [Specification clarifications] - Centralized the $send Future contract: successful resolution means the server has finished consuming the event and any tied application resource, and accepted its output for outbound processing; it does not mean client receipt or complete-response delivery. Before close, validation and resource failures fail the Future; post-close sends are successful no-ops, and pending send Futures carry transport-independent backpressure even without optional pagi.transport introspection. 0.002001 - 2026-06-30 [Specification updates] - Application/middleware return value is now specified as inert: the server assigns the resolved value no meaning and acts only on the Future (completion/errors) and on events plus connection facts. Frameworks may use return values for their own composition but must convert them to events at the gateway boundary; a value must not escape to the server as the result. - Middleware section: clarified that middleware propagate the inner Future (not a value) and observe/rewrite responses by wrapping $send/$receive; removed a stranded comment from the example. - Added the sse.close send-event: an explicit, application-initiated end to an SSE stream (the SSE analog of http.response.body{more=>0}), ending it immediately and decoupled from the application returning. Optional reason field is server-side metadata only -- never transmitted to the client (SSE has no close frame, unlike websocket.close). After sse.close, further sse.send/sse.comment/sse.keepalive MUST raise; sse.close is idempotent. Returning after the final sse.send remains a valid way to end a stream. - Added sse.http.response.start / sse.http.response.body send-events: before sse.start, an application MAY decline the stream and return a normal HTTP response (404, 401, a 204 to stop EventSource reconnection, a redirect, ...). Namespaced under sse. (mirroring the websocket.http.response denial events) so an sse scope only carries sse.* events. First-send-wins: after sse.start these are invalid; after sse.http.response.start the stream events are invalid (both MUST raise). Unlike the WebSocket denial extension this is a core capability (no event-stream fallback exists), so every conforming server MUST support it. [Documentation] - Cookbook: new SERVER-SENT EVENTS recipe "Ending a stream: return vs sse.close" -- the two termination paths (identical on the wire), the server-side-only reason, and observing close events by wrapping $send. - Tutorial: new Part 2 section "2.8 Middleware: Wrapping an Application" teaching the pure $app -> $app pattern (scope cloning, wrapping $send/$receive, short-circuiting, error propagation, the inert return value, and stacking). UTF-8 and async-pitfalls sections renumbered to 2.9 and 2.10. 0.002000 - 2026-06-26 [Distribution] - The PAGI distribution is now the specification: PAGI.pm plus the PAGI::Spec::* documentation. The reference server and its runner (PAGI::Server::Runner) moved to the PAGI-Server distribution, and the application toolkit (middleware, apps, endpoints, request/response, test utilities) moved to the PAGI-Tools distribution. Git history preserved. - The PAGI::Spec::* documents are now authored directly as POD. The previous markdown-to-POD build step (and its App::sdview dependency) has been removed; the spec POD is the source. - Expanded the learning material: PAGI::Cookbook (worked, runnable recipes), examples/mini-framework (a complete web framework on PAGI in about fifty lines), PAGI::PSGI (a migration guide for people coming from PSGI), PAGI::Building (a guide for framework authors), and a committed README.md. All example code targets Perl 5.18. - The spec modules are pure documentation. For backward compatibility during the transition, PAGI pulls in PAGI-Server and PAGI-Tools as runtime dependencies, so existing `requires 'PAGI'` dependents keep getting the server and toolkit. Depend on PAGI::Server / PAGI::Tools directly; this convenience dependency will be removed in a future release. [Specification updates] - pagi.connection gains response_started (MUST) and response_complete (SHOULD). response_started is true once http.response.start is emitted for the scope -- server-set, read-only, and producer-agnostic (an application response and a server-synthesized backstop both set it; the terminal callbacks carry the clean-vs-abnormal outcome). Because it lives on the shared pagi.connection object it is observable across the middleware stack, unlike a per-layer-copied scope scalar. Adds the per-request scoping note for connection (HTTP/1.1/2/3; SSE as its own scope type). - Documented scope shallow-clone semantics. Middleware clone the scope for downward-only isolation: top-level keys are private to each layer, while referenced values (the pagi.connection object, lifespan state) are shared, so a plain top-level scalar does not propagate -- share per-request state through a reference. PAGI::Spec states the normative minimum; the model is detailed in PAGI::Building, with shorter notes in PAGI::PSGI and PAGI::Tutorial. - Header byte safety is now the server's duty. On emission a server MUST reject (fail the send) any header name or value containing CR, LF, or NUL (and control characters in names) rather than forwarding or silently rewriting it (RFC 9112 S11.1, RFC 9110 S5.5). This generalizes the existing sse.send field-newline rule to all header emission and lets app-side containers such as PAGI::Headers hold values as opaque bytes. 0.001023 - 2026-05-21 - Fixed incorrect metadata and package version information - POD fixes. 0.001022 - 2026-04-06 [New Features — PAGI::Context] - New PAGI::Context factory + base class with protocol-specific subclasses (PAGI::Context::HTTP, PAGI::Context::WebSocket, PAGI::Context::SSE) - Factory inspects $scope->{type} and returns the appropriate subclass - Extensible via overridable _type_map and _resolve_class methods - Shared methods on all context types: scope accessors, stash, session, state, connection state, header lookup, path_params/path_param - has_session method for safe session availability checks - HTTP context: lazy request/response accessors, method accessor - WebSocket context: lazy websocket accessor - SSE context: lazy sse accessor - Shortcut aliases: req (request), resp (response), ws (websocket) - receive/send raw protocol escape hatches with POD examples [Breaking Changes — Endpoint Handler Signatures] - PAGI::Endpoint::Router now injects $ctx instead of ($req, $res) / ($ws) / ($sse) - PAGI::Endpoint::HTTP verb methods now receive ($self, $ctx) instead of ($self, $req, $res) - PAGI::Endpoint::WebSocket callbacks now receive ($self, $ctx, ...) instead of ($self, $ws, ...) - PAGI::Endpoint::SSE callbacks now receive ($self, $ctx, ...) instead of ($self, $sse, ...) - Endpoint middleware receives ($self, $ctx, $next) instead of ($self, $req, $res, $next) - request_class/response_class/websocket_class/sse_class replaced by context_class - context_class method on all endpoint types (defaults to 'PAGI::Context') [New Features — PAGI::Stash] - New PAGI::Stash standalone helper for per-request shared state - Strict get() that dies on missing keys with helpful error messages - Scope-based constructor works with any object that has ->scope method - Removed stash method from protocol helpers in favor of PAGI::Stash [Improvements — PAGI::Session] - Constructor refactored: new (scope-only) and from_data (raw hashref) - Added data method for raw hashref access - Chaining support on set/delete methods - Aligned error messages with PAGI::Stash conventions 0.001021 - 2026-04-01 [New Features — Unix Domain Socket Support (Experimental)] - Unix domain socket listening via `socket` constructor option or --socket CLI flag - Socket file permissions via `socket_mode` option or --socket-mode CLI flag - Stale socket cleanup (unlink before bind) and cleanup on graceful shutdown - Restrictive umask during socket bind (CVE-2023-45145 mitigation) [New Features — Multi-Listener Support (Experimental)] - `listen` constructor option: array of listener specs for simultaneous endpoints - --listen CLI flag (repeatable): auto-detects TCP (host:port) vs Unix socket (path) - Per-listener scope: Unix sockets omit `client` from scope, set `server` to [path, undef] - Works with both single-worker and multi-worker modes - _pause_accepting now iterates all listeners under EMFILE conditions [New Features — FD Reuse and systemd Socket Activation (Experimental)] - PAGI_REUSE env var protocol for fd inheritance across exec (Mojo-inspired) - Inherited fd matching: listener specs matched to inherited fds by address - `local $^F = 1023` prevents close-on-exec on listen socket fds - Automatic LISTEN_FDS/LISTEN_PID detection for systemd socket activation - Inherited Unix socket files preserved on shutdown (systemd/parent owns them) [New Features — USR2 Hot Restart (Experimental)] - USR2 signal triggers zero-downtime master re-exec in multi-worker mode - Old master fork+execs new master; fds inherited via PAGI_REUSE - New master waits for worker heartbeats, then sends TERM to old master - Old master drains connections and exits; sockets never close - Idempotent: duplicate USR2 during restart is a no-op - Failure-safe: old master continues if new master fails to start [Bug Fixes] - Guard $scope->{client} access in 5 middlewares (AccessLog, Maintenance, ReverseProxy, RateLimit, WebSocket::RateLimit) to prevent autovivification on Unix socket connections - Validate --socket-mode is a 4-digit octal value (reject ambiguous input) - Harden --listen parser: paths starting with / or . always treated as Unix sockets (prevents mis-parsing paths containing colons) - Runner load_server omits host/port when socket or listen is provided [Test Fixes] - Use condition-based waiting in HTTP/2 SSE keepalive test (eliminates flakiness) - Gate timing-sensitive multiworker/heartbeat tests behind RELEASE_TESTING 0.001020 - 2026-03-16 [Security Hardening] - Reject requests with both Transfer-Encoding and Content-Length (RFC 9112) - Validate Transfer-Encoding per RFC 9112 Section 6.1 (chunked must be final) - Validate trailer headers for CRLF injection - Validate SSE event/id/retry fields against newline injection - Prevent code injection via loop_type parameter (block eval, not string eval) - Fix TLS config to allow TLS 1.3 negotiation (SSL_version trailing colon) [Session Middleware Refactor] - Pluggable State/Store architecture for PAGI::Middleware::Session - State classes: Cookie (default), Header, Bearer, Callback - Store classes: Memory (default, dev/test), with async Future-based interface - PAGI::Session standalone helper with strict get() (dies on missing key) - PAGI::Session accepts raw data, scope hashref, or object with ->scope - Bulk operations: set(k1 => v1, k2 => v2), delete(@keys), slice(@keys), clear() - Session destroy lifecycle: deletes from store, clears cookie via State::clear() - Session regenerate lifecycle: generates new ID, deletes old, preserves data - Idempotency: middleware skips if pagi.session already in scope - Reserved keys (_destroyed, _regenerated) documented as middleware-consumed flags - Store::Cookie extracted to separate CPAN dist (PAGI-Middleware-Session-Store-Cookie) [Bug Fixes] - disable_tls skips TLS setup instead of dying - Recompile access log formatter on configure() changes - Weaken $self in _pause_accepting timer closure (reference cycle fix) - Replace Future::IO->load_impl('IOAsync') with use Future::IO::Impl::IOAsync (fixes compatibility with Future::IO 0.22+) [Cleanup] - Remove unused _log_connection_stats method 0.001019 - 2026-02-19 [New Features] - Router: rewrite _compile_path() as tokenizer with proper regex escaping, so literal path segments containing regex metacharacters (e.g., /foo.bar) are matched literally instead of as patterns - Router: inline constraint syntax {name:pattern} for path parameters (e.g., /user/{id:\d+}) validated during dispatch - Router: chainable constraints() method for applying regex constraints to named path parameters after route definition - Router: any() multi-method matcher supporting wildcard ('*') and explicit method lists (e.g., any [qw(GET POST)] => '/path' => $handler) - Router: named routes and uri_for() now support {name:pattern} syntax and any() routes - Router: group() for organizing routes under a common prefix, with three forms: callback (sub), router-object, and string (auto-require). Supports middleware, nesting, named route namespacing via as(), and conflict detection - Router: mount() now accepts a string class name with auto-require and to_app dispatch - SSE over HTTP/2 with streaming DATA frames, keepalive comments, and disconnect handling - Add PAGI::Utils::Random module for cryptographically secure random bytes - Rate limiter: periodic cleanup and safety valve for expired buckets - CORS middleware: warn when configured with wildcard origins and credentials [Security Fixes] - Fix XSS in Debug middleware panel by escaping scope values in HTML output - Add max_chunk_size limit (default 1MB) to HTTP/1.1 chunked transfer parser to prevent denial-of-service via unbounded chunk sizes - Use cryptographically secure random bytes (via /dev/urandom with Crypt::URandom fallback) in RequestId, CSRF, and Session middleware instead of rand() - Default session cookie SameSite=Lax to prevent CSRF via cross-site requests - Fix double URL-decoding in Static middleware that could allow path traversal [Bug Fixes] - Fix HTTP/2 path decoding to match HTTP/1.1 decoding pipeline - Fix HTTP/2 WebSocket path decoding to match HTTP/1.1 pipeline - Require Net::HTTP2::nghttp2 0.007+ for HTTP/2 support (was unversioned) [Improvements] - Extract _format_sse_event() and _format_sse_comment() helpers and protocol-abstracted keepalive writer for consistent SSE formatting across HTTP/1.1 and HTTP/2 0.001018 - 2026-02-15 [Bug Fixes] - Skip HTTP/2 subtests when Net::HTTP2::nghttp2 is not installed, fixing CPAN tester failures on systems without optional nghttp2 dependency 0.001017 - 2026-02-11 [New Features] - HTTP/2 support (experimental). Requires Net::HTTP2::nghttp2. Enable with --http2 flag. Supports ALPN negotiation (h2/http1.1 over TLS), cleartext h2c via connection preface detection, WebSocket over HTTP/2 (RFC 8441), streaming responses with backpressure, and configurable protocol settings (h2_max_concurrent_streams, h2_initial_window_size, etc.) - Worker heartbeat monitoring for multi-worker mode. Parent process detects workers with blocked event loops via Unix pipe heartbeat and replaces them with SIGKILL + respawn. Default 50s timeout (heartbeat_timeout option, --heartbeat-timeout CLI flag). Only detects event loop starvation — async handlers using await are unaffected regardless of duration. - Custom access log format strings (--access-log-format). Supports atoms like %a (address), %s (status), %D (duration μs), %b (body size). Enables structured JSON logging via format string. - Track response body size in access log (%b atom) [Bug Fixes] - Fix TLS performance cliff at 8+ concurrent connections caused by per-connection SSL context creation parsing the entire CA bundle. Shared SSL context via SSL_reuse_ctx gives ~26x throughput improvement. - Fix TLS in multi-worker mode — workers now SSL-upgrade connections in on_stream rather than relying on listener-level TLS - Fix SSE wire format to handle CRLF, LF, and bare CR line endings per the SSE specification - Fix multi-worker shutdown escalation — SIGKILL timer now fires correctly when workers don't exit after SIGTERM - Fix multi-worker parameter pass-through for server options - Fix flaky multi-worker TLS test [Improvements] - Add on_ssl_error callback to TLS listener setup for cleaner error handling (suppresses "EV: error in callback" noise) - Cache Server header string to avoid per-response interpolation - Merge double header loop in serialize_response_start - Cache access log timestamp at per-second granularity - Cache client_host in access log to avoid per-request getpeername syscall - Skip HTTP/2 tests when Net::HTTP2::nghttp2 is not installed - Skip SSE tests when Future::IO is not installed - Remove unused Sys::Sendfile recommendation from cpanfile 0.001016 - 2026-01-31 [New Features] - Add query parameter parsing to PAGI::SSE: query_params(), query_param(), raw_query_params(), raw_query_param() methods mirroring PAGI::Request and PAGI::WebSocket APIs - pagi-server now auto-configures Future::IO for IO::Async if installed, enabling seamless use of Future::IO-based libraries (Async::Redis, etc.) and PAGI::SSE->every(). Reports configuration in non-production mode. [Breaking Changes] - PAGI::Server (the module) no longer auto-configures Future::IO. pagi-server (the CLI) handles this automatically. Programmatic users of PAGI::Server must configure Future::IO themselves if using Future::IO-based libraries. See PAGI::Server "LOOP INTEROPERABILITY". [Improvements] - PAGI::SSE->every() now provides a clear error message if Future::IO backend is not configured, with example code showing how to fix it - New PAGI::Server documentation section "LOOP INTEROPERABILITY" explains Future::IO configuration for programmatic usage 0.001015 - 2026-01-18 [New Features] - Add query parameter parsing to PAGI::WebSocket: query_params(), query_param(), raw_query_params(), raw_query_param() methods mirroring PAGI::Request API - Add form_param($name, %opts) and raw_form_param($name) singular accessors to PAGI::Request for fetching individual form values [Deprecations] - Rename PAGI::Request param methods for naming consistency (old names deprecated with warnings): - query() -> query_param() - raw_query() -> raw_query_param() - form() -> form_params() - raw_form() -> raw_form_params() [Improvements] - Improve PAGI::Test::Response json() error diagnostics: now shows HTTP status code, Content-Type header, and body preview when JSON parsing fails. Uses croak for better stack traces pointing to test file location. - Add $JSON_ERROR_BODY_LIMIT package variable to control body preview length in json() error messages (default 1500 bytes) 0.001014 - 2026-01-16 [New Features] - Add Future::XS opt-in support for improved performance (~4% multi-worker) Use --future-xs flag or PAGI_FUTURE_XS=1 environment variable - path_param() now strict by default - dies with helpful message on missing keys, catching typos early. Use strict => 0 to opt out. [Bug Fixes] - Fix connection leak where connections weren't removed from tracking hash on EOF/disconnect, causing memory growth under sustained load - Fix POD reference to connection state specification (was pointing to non-existent file) [Improvements] - Simplify max_connections default to 1000 (removed unreliable auto-detection) - Lazy disconnect_future() allocation - Future only created when called, reducing per-request overhead for simple handlers - path_param() now delegates to path_params(), allowing subclasses to override path_params for custom parameter handling (e.g., lazy conversion from positional to named parameters) - Add _default_path_param_strict_opt() hook for subclasses to change default - Document query_params/query strict and raw options in POD 0.001013 - 2026-01-04 [New Features] - Add PAGI::Utils::handle_lifespan utility for simplified lifespan handling - Add Response getters (status, headers, content_type, body) and try setters - Support static path rewrites in PAGI::App::File - Add full-demo example demonstrating lifespan, HTTP, WebSocket, and SSE [Bug Fixes] - Fix POD formatting of nested angle brackets - Make handle_lifespan croak on wrong scope type for clearer error messages [Improvements] - Match plackup FindBin behavior for app loading - Refactor PAGI::Response to use exists() for lazy defaults - Remove deprecated path_param/path_params from PAGI::Response - Document state method in PAGI::Request - Normalize spec docs to ASCII [Build/Distribution] - Add .editorconfig for consistent editor settings - Add Thunderhorse to implementations documentation 0.001012 - 2026-01-01 [New Features] - PAGI 0.2 specification: Loop-agnostic apps, SSE POST support, keepalive events - Send-side backpressure to prevent unbounded memory growth under slow clients - Configurable write_high_watermark (64KB) and write_low_watermark (16KB) matching Python asyncio defaults for optimal memory/throughput balance - SSE disconnect reasons (client_closed, timeout, write_error, shutdown) [Bug Fixes] - Fix Lint middleware to preserve original app errors instead of masking them - Fix SSE keepalive to use chunked transfer encoding - Fix missing IO::Async::Timer::Periodic use statement - Fix missing 'method' field in SSE test scopes (per spec line 624) - Fix WebSocket scope incorrectly including 'method' field in test client - Fix middleware class resolution for nested namespaces (Auth::Basic, etc.) using ^ prefix for fully-qualified class names [Improvements] - Improved event loop portability (Future::IO now fully optional) - Improve max_connections auto-detection using getrlimit(RLIMIT_NOFILE) instead of sysconf(_SC_OPEN_MAX) for accuracy - Add fd_headroom config option (default 100) for max_connections calculation - Add TTY warning at startup when access_log goes to terminal - Security fixes from code review - PAGI spec compliance fixes for event validation [Build/Distribution] - Replace Markdown::Pod with App::sdview for spec POD generation - Fixes =item - to =item * (proper POD bullet points) - Converts module names to L<> links for metacpan 0.001011 - 2025-12-28 - Switch license from Perl_5 to Artistic_2_0 (cleaner language, OSI recommended) - Fix Ctrl-C signal handling for multi-worker mode: lifespan.shutdown now runs for all workers ensuring proper cleanup - Add -e and -M flags to Runner for inline apps (like perl -e/-M) - Add .pl and .md to MIME types in PAGI::App::File - Add exception trapping to PAGI::Test::Client (default behavior) - Add SIGNAL HANDLING documentation to PAGI::Server POD - Add SECURITY.md and CONTRIBUTING.md to repository - Document async completion contract and detect missing responses - Cookbook improvements: JWT validation, Redis sessions, redirect behavior, WebSocket heartbeat example, IO::File::WithPath for filehandles 0.001010 - 2025-12-26 - BREAKING: Removed sendfile() from PAGI::Server - use PAGI::Middleware::XSendfile with nginx/Apache X-Sendfile/X-Accel-Redirect instead for large file serving - Added handle_ranges option to PAGI::App::File for byte-range request support - Added FreeBSD CI testing - Documentation fixes for phantom module references 0.001009 - 2025-12-26 - BREAKING: PAGI::Response constructor now takes ($scope, $send) instead of ($send, $scope) for consistency with Request, SSE, and WebSocket APIs - Added request_timeout for stalled requests, ws_idle_timeout and sse_idle_timeout for idle connection cleanup (all disabled by default for performance) - Test::Client: multi-value headers/query/form support, fixed arrayref headers with form/json - Scope-based caching for Request, Response, SSE, WebSocket objects - Replace retain() with adopt_future() for proper async error handling - Added on_error callback to heartbeat and rate limit middleware - Restructured Tutorial, added Cookbook for advanced patterns 0.001008 - 2025-12-25 - Fixed 32-bit Perl compatibility in WebSocket test (pack 'Q' → 'NN') - Fixed sendfile on non-blocking sockets for FreeBSD/BSD systems - Added sendfile documentation and production recommendations - Improved test reliability: skip lsof/timing-dependent tests by default - Normalized test environment variables to use RELEASE_TESTING 0.001007 - 2025-12-24 - More broken dist metadata fixes (part 2) 0.001006 - 2025-12-24 - More broken dist metadata fixes 0.001005 - 2025-12-24 - trying to get the indexer to ignore my test libs 0.001004 - 2025-12-24 - More fixes to the code that generates the POD version of the specification in improve readability - exclude test and example libs from the CPAN indexer. 0.001003 - 2025-12-24 - Fixes to the code that generates the POD version of the specification to try and fix UTF8 and other formatting issues. 0.001002 - 2025-12-24 - Initial release. See README.md for a starting point 0.001001 - 2025-12-17 - Placeholder release