NAME

PAGI::Server::EventValidator - Mandatory outgoing-event validation

SYNOPSIS

use PAGI::Server::EventValidator;

# Shape validation (per protocol family)
PAGI::Server::EventValidator::validate_http_send($event, \%opts);

# Send-sequence validation (pure state-transition functions)
my $next_state = PAGI::Server::EventValidator::advance_http($state, $event);

DESCRIPTION

This module is the shared, mandatory validator for every event a PAGI application sends to the server. PAGI::Server::Connection calls it on every send path (HTTP/1, HTTP/2, WebSocket, SSE) and PAGI::Server calls it on the lifespan send path; validation cannot be disabled and runs in every environment, including production. It checks that:

  • Required fields are present

  • Field types are correct

  • Mutually exclusive fields are handled properly

  • The event type is recognized (and, for extension-gated types, the extension is enabled)

  • Events arrive in a legal order for their protocol family (see the advance_* functions)

A malformed, mis-sequenced, unrecognized, or unadvertised-extension event causes the corresponding $send Future to fail; see PAGI::Server::Connection for how these functions are wired into each send path.

FUNCTIONS

validate_http_send($event, $opts)

Validates HTTP send events: http.response.start, http.response.body, http.response.trailers, http.fullflush. $opts is an optional hash reference of the form { extensions => \%scope_extensions }; http.fullflush croaks with "Extension not enabled: fullflush" unless $opts->{extensions}{fullflush} exists. Any other event type croaks with "Unrecognized event type '$type' for http protocol".

validate_websocket_send($event, $opts)

Validates WebSocket send events: websocket.accept, websocket.send, websocket.close, websocket.keepalive, websocket.http.response.start, websocket.http.response.body. $opts is an optional hash reference of the form { extensions => \%scope_extensions }; the two websocket.http.response.* types croak with "Extension not enabled: websocket.http.response" unless $opts->{extensions}{'websocket.http.response'} exists. Any other event type croaks with "Unrecognized event type '$type' for websocket protocol".

validate_sse_send($event, $opts)

Validates SSE send events: sse.start, sse.send, sse.comment, sse.keepalive, sse.close, sse.http.response.start, sse.http.response.body, http.fullflush. $opts is an optional hash reference of the form { extensions => \%scope_extensions }; http.fullflush croaks with "Extension not enabled: fullflush" unless $opts->{extensions}{fullflush} exists. Any other event type croaks with "Unrecognized event type '$type' for sse protocol".

sse.keepalive's comment field is optional (defaults to ''), but when present it is validated here, at arm time: it must be a defined non-reference string that round-trips Encode::encode('UTF-8', $comment, Encode::FB_CROAK), or this call croaks with "sse.keepalive 'comment' must be a UTF-8-encodable string". This keeps an unencodable comment from surfacing later as an uncaught die inside the keepalive timer tick.

validate_lifespan_send($event)

Validates lifespan send events: lifespan.startup.complete, lifespan.startup.failed, lifespan.shutdown.complete, lifespan.shutdown.failed. For the *.failed types, message is optional but must be a defined non-reference string when present. Any other event type croaks with "Unrecognized event type '$type' for lifespan protocol".

advance_http($state, $event)

Pure send-sequence transition function for the HTTP family. Does not validate event shape; call validate_http_send separately first. States: initial, started, started_t (trailers declared), awaiting_trailers, complete. Starting state is initial.

From initial, http.response.start advances to started, or to started_t if the start event's trailers field is true. From started or started_t, http.response.body events with a true more field (and no file/fh) keep the same state; a terminal body chunk (more false, absent, or a file/fh body) advances started to complete and started_t to awaiting_trailers. http.response.trailers only succeeds from awaiting_trailers, advancing to complete. http.fullflush is legal in started, started_t, and awaiting_trailers and leaves the state unchanged.

Croaks: any event in initial other than http.response.start ("cannot send '<type' before http.response.start">); http.response.start outside initial ("cannot send duplicate http.response.start"); http.response.trailers outside awaiting_trailers ("cannot send http.response.trailers: trailers were not declared or body is not complete"); any event once complete ("cannot send '<type': response already complete">).

advance_websocket($state, $event)

Pure send-sequence transition function for the WebSocket family. Does not validate event shape; call validate_websocket_send separately first. States: connecting, accepted, denial, denial_complete, closed. Starting state is connecting.

From connecting: websocket.accept advances to accepted; websocket.close advances to closed; websocket.http.response.start (a denial) advances to denial. From accepted: websocket.send and websocket.keepalive keep accepted; websocket.close advances to closed. From denial: websocket.http.response.body with a true more field keeps denial; a terminal body chunk advances to denial_complete.

Croaks: websocket.send/websocket.keepalive before accept ("cannot send '<type' before websocket.accept">); any denial or accept event once accepted ("cannot send '<type' after websocket.accept">); any non-body event once denial has started ("cannot send '<type' after websocket.http.response.start">); any event once closed ("cannot send '<type' after websocket.close">, including a second websocket.close - unlike SSE, WebSocket close is not idempotent); any event once denial_complete ("cannot send '<type': denial response already complete">).

advance_sse($state, $event)

Pure send-sequence transition function for the SSE family. Does not validate event shape; call validate_sse_send separately first. States: initial, streaming, declining, decline_complete, closed. Starting state is initial.

From initial: sse.start advances to streaming; sse.http.response.start (a decline) advances to declining. From streaming: sse.send, sse.comment, sse.keepalive, and http.fullflush keep streaming; sse.close advances to closed. From declining: sse.http.response.body with a true more field keeps declining; a terminal body chunk advances to decline_complete. From closed, sse.close is idempotent and stays closed.

Croaks: any event in initial other than sse.start/decline start ("cannot send '<type' before sse.start">); a decline start after sse.start ("cannot decline with sse.http.response.start after sse.start"); a duplicate sse.start ("cannot send duplicate sse.start"); any other event once streaming ("cannot send '<type' after sse.start">); any non-body event once declining has started ("cannot send '<type' after sse.http.response.start">); any event other than sse.close once closed ("cannot send '<type' after sse.close">); any event once decline_complete ("cannot send '<type': decline response already complete">).

advance_lifespan($state, $event)

Pure send-sequence transition function for the lifespan family. Does not validate event shape; call validate_lifespan_send separately first. States: startup_pending, running, shutdown_pending, finished. Starting state is startup_pending. The server itself moves running to shutdown_pending when it emits the shutdown event; this function does not perform that transition.

From startup_pending: lifespan.startup.complete advances to running; lifespan.startup.failed advances to finished. From shutdown_pending: lifespan.shutdown.complete or lifespan.shutdown.failed advances to finished. Every other ($state, $event) combination croaks with "cannot send '<type' during lifespan phase '<state>'">.

check_header_value($value)

Checks a single header value for byte safety: dies with a \n-terminated message if $value contains a CR, LF, or null byte. Returns $value unchanged on success.

check_header_name($name)

Checks a single header name for byte safety: dies with a \n-terminated message if $name contains a CR, LF, or null byte, and dies with a separate \n-terminated message if it contains any other control character. Returns $name unchanged on success.

validate_headers($headers, $event_type)

Validates a headers field shared by every event shape that carries one (http.response.start, http.response.trailers, websocket.accept, websocket.http.response.start, sse.http.response.start, sse.start). $event_type is the event type string used in croak messages. Croaks unless $headers is an array reference of 2-element array references, each holding a defined, non-reference name and value; each name and value is then passed through check_header_name and check_header_value. Returns nothing.

SEE ALSO

PAGI::Server, PAGI::Server::Connection