PAGI-Server
view release on metacpan or search on metacpan
t/http2/26-mandatory-validation.t view on Meta::CPAN
# sequence check, never advances $seq -- so the machine is still
# 'started_t' afterward, and the terminal body below is legal.
await $send->({ type => 'http.response.start', status => 200, trailers => 1, headers => [['content-type','text/plain']] });
await $send->({ type => 'http.response.body', body => 'x', more => 1 });
my $err = do { local $@; eval { await $send->({ type => 'http.response.trailers', headers => [['x-t','1']] }) }; $@ };
$err =~ s/\n/ /g;
await $send->({ type => 'http.response.body', body => "trailers:$err", more => 0 });
return;
}
if ($path eq '/after-complete') {
# Once the response is complete there's no valid way left to carry
# the error back in-band, so stash it in a closure var: the point is
# that this send raises through advance_http's terminal 'complete'
# state rather than silently no-op'ing because the h2_streams entry
# for a finished stream may already have been reclaimed.
await $send->({ type => 'http.response.start', status => 200, headers => [['content-type','text/plain']] });
await $send->({ type => 'http.response.body', body => 'done', more => 0 });
my $err = do { local $@; eval { await $send->({ type => 'http.response.body', body => 'extra', more => 0 }) }; $@ };
$after_complete_err = $err;
return;
}
await $report->(undef); # control path: no violation
};
my (undef, $body);
(undef, $body) = get_h2('/bad-status', app => $http_app);
like( $body, qr/must be a non-negative integer/,
'malformed status fails the send Future with validate_events => 0' );
(undef, $body) = get_h2('/body-before-start', app => $http_app);
like( $body, qr/before http\.response\.start/, 'body before start fails' );
(undef, $body) = get_h2('/unknown-type', app => $http_app);
like( $body, qr/Unrecognized event type/, 'unknown type fails' );
(undef, $body) = get_h2('/duplicate-start', app => $http_app);
like( $body, qr/dup:.*duplicate http\.response\.start/,
'duplicate start fails without disturbing the real response' );
(undef, $body) = get_h2('/file-body', app => $http_app);
is( $body, $FILE_BODY_CONTENT,
'file body on h2 streams the real file content (Task 2)' );
(undef, $body) = get_h2('/trailers', app => $http_app);
like( $body, qr/trailers:.*trailers were not declared or body is not complete/,
'trailers sent before the body is terminal fail loudly (Phase 2b Task 4: real trailers, not a stub)' );
get_h2('/after-complete', app => $http_app);
like( $after_complete_err // '', qr/response already complete/,
'send after http response complete raises on h2, not silently swallowed' );
(undef, $body) = get_h2('/ok', app => $http_app);
is( $body, 'NO-ERROR', 'a conforming app is unaffected' );
# ============================================================
# SSE: mis-sequencing after a terminal state raises, not swallowed
# ============================================================
# Once a stream is 'closed' (sse.close) or 'decline_complete', its
# h2_streams entry is reclaimed asynchronously by _h2_on_close. These probe
# sends happen on the very next tick of the same app coroutine -- before
# that reclaim can plausibly have run -- but the sequence check must not
# rely on that timing: it consults the closure-local $seq, not the h2_streams
# entry, precisely so a send after the entry is actually gone still raises.
sub sse_probe {
my (%args) = @_;
my ($conn, $stream_io, $client_sock, $server) =
create_h2c_connection(app => $args{app});
my $client = create_client();
h2c_handshake($client, $client_sock);
$client->submit_request(
method => 'GET',
path => $args{path} // '/events',
scheme => 'http',
authority => 'localhost',
headers => [['accept', 'text/event-stream']],
);
$client_sock->syswrite($client->mem_send);
exchange_frames($client, $client_sock, 20);
$stream_io->close_now;
$loop->remove($server);
}
my $sse_after_close_err;
my $sse_app1 = async sub {
my ($scope, $receive, $send) = @_;
await $send->({ type => 'sse.start', status => 200 });
await $send->({ type => 'sse.send', data => 'one' });
await $send->({ type => 'sse.close' });
my $err = do { local $@; eval { await $send->({ type => 'sse.send', data => 'late' }) }; $@ };
$sse_after_close_err = $err;
};
sse_probe(app => $sse_app1);
like( $sse_after_close_err // '', qr/after sse\.close/,
'sse.send after sse.close raises on h2, not silently swallowed' );
my $sse_decline_complete_err;
my $sse_app2 = async sub {
my ($scope, $receive, $send) = @_;
await $send->({ type => 'sse.http.response.start', status => 200, headers => [['content-type','text/plain']] });
await $send->({ type => 'sse.http.response.body', body => 'done', more => 0 });
my $err = do { local $@; eval { await $send->({ type => 'sse.http.response.body', body => 'extra', more => 0 }) }; $@ };
$sse_decline_complete_err = $err;
};
sse_probe(app => $sse_app2);
like( $sse_decline_complete_err // '', qr/decline response already complete/,
'sse.http.response.body after a completed decline raises on h2, not silently swallowed' );
# ============================================================
# WebSocket: mis-sequencing after a terminal state raises, not swallowed
# ============================================================
# Same class of bug as the SSE/HTTP carve-outs above: websocket.close itself
# ends the stream (submit_data with END_STREAM), and _h2_on_close reclaims
# the h2_streams entry for that stream asynchronously -- independent of
# protocol family. A post-close/post-denial-complete send must still raise
# through advance_websocket, not silently no-op on a "stream gone" check.
sub ws_probe {
my (%args) = @_;
my ($conn, $stream_io, $client_sock, $server) =
( run in 2.703 seconds using v1.01-cache-2.11-cpan-364913b4093 )