PAGI-Server

 view release on metacpan or  search on metacpan

t/53-trailers-framing.t  view on Meta::CPAN

my $http = Net::Async::HTTP->new;
$loop->add($http);

# --- /cl-trailers: content-length response, trailers fail loudly, and the --
# app-return afterward now closes the connection abnormally (Phase 3 Task 2).
# Raw socket, not $http: we need to observe the connection actually close
# (EOF with no further bytes) rather than have a pooled client's transparent
# reconnect hide it. Deliberately no "Connection: close" request header --
# HTTP/1.1 keep-alive is the client's default; the server must veto it
# anyway. $http itself is left untouched here, so its first use below (for
# /head-trailers) is naturally a fresh connection.
my @cl_warnings;
{
    local $SIG{__WARN__} = sub { push @cl_warnings, $_[0] };

    my $cl_sock = IO::Socket::INET->new(
        PeerAddr => '127.0.0.1',
        PeerPort => $port,
        Proto    => 'tcp',
        Timeout  => 5,
    ) or die "connect failed: $!";

    print $cl_sock "GET /cl-trailers HTTP/1.1\r\n";
    print $cl_sock "Host: 127.0.0.1:$port\r\n";
    print $cl_sock "\r\n";

    $cl_sock->blocking(0);
    my $cl_response = '';
    my $deadline = time + 5;
    while (time < $deadline) {
        my $buf;
        my $n = sysread($cl_sock, $buf, 4096);
        if (defined $n) {
            last if $n == 0;   # EOF: server closed the connection
            $cl_response .= $buf;
        }
        $loop->loop_once(0.1);
    }
    close $cl_sock;

    like( $cl_response, qr/^HTTP\/1\.1 200/, '/cl-trailers: status line 200' );
    like( $cl_response, qr/\r\n\r\nok\z/,
        '/cl-trailers: content-length-framed body "ok" fully delivered, then EOF (connection closed, no more bytes)' );
}

like( $T::CL_ERR, qr/requires chunked framing/,
    'trailers on content-length response fail the Future' );

$loop->loop_once(0.1) for 1..3;

is( $T::CL_DISC_REASON, 'server_error',
    '/cl-trailers: app-return after the failed trailers send reports on_disconnect(server_error)' );

ok(
    (scalar grep { /returned with an incomplete response/i } @cl_warnings),
    '/cl-trailers: incomplete-response warning logged'
);

# --- /chunked-trailers: control -- raw socket, trailers actually on the wire

# Net::Async::HTTP doesn't expose trailers (see t/02-streaming.t), so read
# the raw chunked framing off the wire ourselves. Socket-pump idiom lifted
# from t/52-mandatory-validation.t's $ws_handshake_and_drain; the chunk
# grammar being asserted against (hex chunk-size line, CRLF-terminated
# chunks, "0\r\n" + trailer headers + CRLF as the terminator) is the same
# grammar t/16-chunked-validation.t exercises against the parser.
my $raw_request = sub {
    my ($port, $method, $path) = @_;
    my $sock = IO::Socket::INET->new(
        PeerAddr => '127.0.0.1',
        PeerPort => $port,
        Proto    => 'tcp',
        Timeout  => 5,
    ) or return undef;

    print $sock "$method $path HTTP/1.1\r\n";
    print $sock "Host: 127.0.0.1:$port\r\n";
    print $sock "Connection: close\r\n";
    print $sock "\r\n";

    $sock->blocking(0);
    my $response = '';
    my $deadline = time + 5;
    while (time < $deadline) {
        my $buf;
        my $n = sysread($sock, $buf, 4096);
        if (defined $n) {
            last if $n == 0;   # EOF: server closed after Connection: close
            $response .= $buf;
        }
        $loop->loop_once(0.1);
    }
    close $sock;
    return $response;
};

my $chunked_raw = $raw_request->($port, 'GET', '/chunked-trailers');
ok( defined $chunked_raw, '/chunked-trailers: connected' ) or diag('connection failed');
like( $chunked_raw, qr/Transfer-Encoding:\s*chunked/i,
    '/chunked-trailers uses chunked framing' );
like( $chunked_raw, qr/\r\n2\r\nok\r\n/,
    '/chunked-trailers body chunk "ok" present' );
like( $chunked_raw, qr/0\r\nx-t: 1\r\n\r\n/,
    '/chunked-trailers trailer header actually transmitted after the final chunk (control, unchanged)' );

# --- /empty-trailers, /absent-trailers: empty/absent trailer headers yield --
# the same bare "0\r\n\r\n" terminator as the no-trailers chunked end
# (validator: headers OPTIONAL, empty list validates via a zero-iteration
# loop; h1 writer: `$event->{headers} // []` makes absent and explicit-empty
# byte-identical on the wire).
for my $path (qw(/empty-trailers /absent-trailers)) {
    my $raw = $raw_request->($port, 'GET', $path);
    ok( defined $raw, "$path: connected" ) or diag('connection failed');
    like( $raw, qr/Transfer-Encoding:\s*chunked/i, "$path uses chunked framing" );
    like( $raw, qr/\r\n2\r\nok\r\n0\r\n\r\n\z/,
        "$path terminates with a bare 0\\r\\n\\r\\n (no trailer header lines) immediately after the final chunk" );
}

# --- /head-trailers: HEAD accept-and-discard --------------------------------

my $head_res = $http->HEAD("http://127.0.0.1:$port/head-trailers")->get;



( run in 1.430 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )