# This file documents the revision history for Perl extension PAGI 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