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