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 )