# Revision history for PAGI-Tools
0.002002 - 2026-07-15
- PAGI::Middleware::Session: non-'http' scopes that carry a headers key
(WebSocket, SSE upgrades) now get a READ-ONLY session populated the same
way 'http' does, instead of being skipped; nothing is ever saved for them,
and an unresolvable id yields an empty hashref rather than a missing key.
See the new "NON-HTTP SCOPES" POD section.
- PAGI::Middleware::CSRF: new 'enforce' config -- 'header' (default,
unchanged: unsafe methods need a matching header token or a 403) or 'app'
(issue-only: mint/stash the token for every method, never auto-reject, and
let the app validate). 'app' exists because the middleware cannot read
form params without buffering the body stream.
- PAGI::Context: add csrf_token and csrf_verify (timing-safe token check).
Both inherit into PAGI::Nano contexts.
- REMOVED PAGI::Middleware::CSRF's 'token_param' config: it was dead (the
form-field lookup was never implemented). Use enforce => 'app' with
$ctx->csrf_verify for form tokens.
- PAGI::Utils::SecureCompare: new module for the constant-time string compare
shared by PAGI::Middleware::CSRF and PAGI::Context.
- Docs: PAGI::Middleware::CSRF's USAGE section no longer contradicts itself
(a JS getCookie() against an HttpOnly cookie); it now documents the header
and form flows separately.
- t/integration/sse-decline-end-to-end.t now skips unless PAGI::Server is
>= 0.002005, the release that added the sse.http.response.* decline
protocol it exercises. Fixes a CPAN Testers FAIL against 0.002001 (older
servers answer an unmatched SSE route with 500 instead of 404).
- PAGI::Middleware::Auth::Bearer: _secure_compare now rejects undef arguments
instead of numifying them, so two undef (or undef vs empty) signatures no
longer compare equal.
- PAGI::Middleware::CSRF: new 'secure' config (default 0) to set the Secure
attribute on the CSRF cookie; enable for HTTPS deployments.
- PAGI::WebSocket / PAGI::SSE: a per-message callback that dies in the
iteration helpers (each_message/each_text/each_bytes/each_json, SSE each()
and every()) now runs on_close cleanup before re-raising, instead of
bypassing it. WebSocket::run() is unchanged -- it isolates errors and keeps
serving.
- PAGI::Context::WebSocket / PAGI::Context::SSE: a terminal disconnect driven
through $c->run now fires the underlying object's on_close (previously it
only recorded the reason, so on_close never fired and is_closed/is_connected
lied). Both also gain on_close() stubs pointing at
$c->websocket->on_close / $c->sse->on_close.
- PAGI::Test::Client: start() now drives the event loop during lifespan
startup and dies on failure, instead of busy-waiting a 5s deadline without
advancing the loop (which hung off-loop startup hooks and ignored
lifespan.startup.failed).
0.002001 - 2026-06-30
- PAGI::Endpoint / PAGI::Endpoint::Router: route-level event middleware is now
spec-pure factory form, composed at build time. The vestigial call() and the
old 4-arg ($scope, $receive, $send, $next) middleware form are removed; the
endpoint owns the value-flow chain and responds at the framework boundary
(await $next->() returns the handler's response value). WS/SSE route-level
middleware now fail loudly (unsupported), and a value-flow non-response error
names the offending method.
- PAGI::SSE: close() now sends an explicit sse.close event (with an optional
server-side reason) and wakes a parked run(); close() is async so on_close
callbacks run to completion.
- PAGI::Endpoint::SSE: await keepalive() so the sse.keepalive event is actually
emitted on a fully-async server.
- PAGI::Utils::is_response: one predicate for "did the handler return a
response?", shared by the endpoint and router dispatch paths so the
diagnostics stay consistent.
- PAGI::SSE / PAGI::Context::SSE: add is_connected, derived from the SSE
wrapper's own state machine (true while started and not closed), mirroring
PAGI::WebSocket. Context::SSE::is_connected overrides the base method, which
reads pagi.connection (not applicable to sse) and therefore wrongly reported
a live stream as disconnected. Like the WebSocket counterpart it is "not
falsely stale" rather than a live network probe.
- PAGI::WebSocket->deny() no longer records the HTTP denial status as a
WebSocket close code; close_code is undef after an HTTP denial (a denial
sends a response, not a close frame). The bare-403 fallback still reports
1008.
- PAGI::WebSocket try_send_text/bytes/json no longer fabricate a 1006
"Connection lost" close (and no longer mark the socket closed) when a send
errors. A failed send returns false without mutating connection state; a
real disconnect is still detected via the websocket.disconnect event.
- Docs: PAGI::SSE scope caching described accurately (cached while
referenced via a weak ref, not a guaranteed singleton);
PAGI::Request::Negotiate content-negotiation example reads the Accept
header through PAGI::Request instead of the (fatal) $scope->{headers}{...};
try_send_* documented as best-effort with a send-method ladder; a cookbook
recipe on declining an SSE request (404 / 401 / 204 stop-reconnect);
examples and Endpoint POD migrated off the removed 4-arg middleware form.
- REMOVED PAGI::Middleware::WebSocket::Compression. permessage-deflate
(RFC 7692) is a WebSocket framing-layer feature -- a compressed message is
signalled by the RSV1 frame bit, which only the server's framing layer can
set. A middleware at the PAGI event layer can deflate the payload bytes but
cannot flag the frame, so the deflated bytes went out in a normal RSV1=0
frame (garbage on the wire). It also wrote a non-spec `extensions` key on
websocket.accept (the event allows only `subprotocol`/`headers`), which the
reference server ignores -- and that server explicitly does not implement
permessage-deflate (RSV bits forced to 0). If WebSocket compression is
wanted, it must be a server feature, not an application middleware.
- PAGI::App::Router: an unmatched SSE or WebSocket route now returns a real
404 instead of crashing the connection. The 404 is emitted with the event
family that matches the scope -- sse.http.response.* on SSE and
websocket.http.response.* on WebSocket, plain http.response.* on HTTP --
since those scopes reject a plain http.response.* before a stream or
handshake starts. A custom not_found app still takes over for every scope
type. Requires a server supporting the sse.http.response.* decline events
(PAGI-Server 0.002005+).
0.002000 - 2026-06-26
- Split out of the PAGI distribution into its own distribution (git history
preserved).
- New PAGI::Tools module anchors the distribution.
- The application runner ships in PAGI-Server as PAGI::Server::Runner.
- B<BREAKING>: PAGI::Response is now a value — factories build detached
responses, body methods return $self, respond($send) sends, to_app makes any
response mountable; Endpoint HTTP handlers RETURN a response value
(await $res->json(...) no longer sends).
- B<BREAKING>: PAGI::Request->configure/->config removed; path_param strict and
multipart limits are now per-call options.
- PAGI::Headers: ordered, case-insensitive, multi-value header container
(rejects undef values; to_hash); Response and Request route headers through it.
- is_sent now reads response_started off the pagi.connection object, not a scope
scalar, so it survives middleware's scope copy. Adds PAGI::Test::ConnectionState.
- PAGI::Response->has_body_source: predicate for whether a body source was
registered.
- PAGI::Response: charset follows the body — text/html/send gain
"; charset=utf-8", json() stays bare application/json.
- PAGI::Utils::to_app coerces coderefs, component objects, and class names; every
composition point accepts anything it accepts.
- builder's enable() accepts configured middleware instances.
- PAGI::App::Router: coderef route middleware can transform the channel.
- PAGI::Endpoint::Router route middleware are value-flow (await $next->() returns
the handler's response); App::Router dispatch returns the matched route's value.
- PAGI::Context: $ctx->text/html/json/redirect shorthands,
assert_http/websocket/sse guards, on_default catch-all, raw_send accessor.
- Backpressure/lifecycle helpers: on_complete, buffered_amount/high_water_mark/
low_water_mark, on_high_water/on_drain/is_writable, WebSocket->deny().
- PAGI::Request->multipart_stream: pull-based streaming multipart parsing with
app-controlled sinks; mutually exclusive with the buffered body methods.
- PAGI::App::Redirect and ::NotFound build a PAGI::Response value (modules kept
for the dynamic case).
- PAGI::Lifespan coerces its app arg via to_app, and reports shutdown-handler
failures instead of swallowing them.
- Spec: URLMap sets root_path; WrapCGI builds SCRIPT_NAME from root_path.
- Canonical scope-adding middleware route through the modify_scope helper.
- Documented the PAGI::Response subclassing seam for framework authors.
- Docs: recipes are now PAGI::Tools::Cookbook; Tutorial/Cookbook examples
corrected against the current API; chat-showcase example uses PAGI::App::Router
+ PAGI::Lifespan.
- For changes prior to 0.002000, see the Changes file of the PAGI distribution
(versions up to 0.001023).