PAGI-Server
view release on metacpan or search on metacpan
t/02-streaming.t view on Meta::CPAN
use strict;
use warnings;
use Test2::V0;
use IO::Async::Loop;
use Net::Async::HTTP;
use Future::AsyncAwait;
use PAGI::Server;
plan skip_all => "Server integration tests not supported on Windows" if $^O eq 'MSWin32';
# Step 2: Streaming Responses and Disconnect Handling
# Tests for examples/02-streaming-response/app.pl
my $loop = IO::Async::Loop->new;
# Test 1: Streaming response with multiple chunks
subtest 'Streaming response uses chunked Transfer-Encoding' => sub {
my $simple_streaming_app = async sub {
my ($scope, $receive, $send) = @_;
die "Unsupported: $scope->{type}" if $scope->{type} ne 'http';
while (1) {
my $event = await $receive->();
last if $event->{type} ne 'http.request';
last unless $event->{more};
}
await $send->({
type => 'http.response.start',
status => 200,
headers => [['content-type', 'text/plain']],
});
await $send->({ type => 'http.response.body', body => "Chunk 1\n", more => 1 });
await $send->({ type => 'http.response.body', body => "Chunk 2\n", more => 1 });
await $send->({ type => 'http.response.body', body => "Chunk 3\n", more => 0 });
};
my $server = PAGI::Server->new(
app => $simple_streaming_app,
host => '127.0.0.1',
port => 0,
quiet => 1,
);
$loop->add($server);
$server->listen->get;
my $port = $server->port;
my $http = Net::Async::HTTP->new;
$loop->add($http);
my $response = $http->GET("http://127.0.0.1:$port/")->get;
is($response->code, 200, 'Response status is 200 OK');
is($response->header('Transfer-Encoding'), 'chunked', 'Response uses chunked Transfer-Encoding');
my $body = $response->decoded_content;
like($body, qr/Chunk 1.*Chunk 2.*Chunk 3/s, 'Response body contains all chunks in order');
$server->shutdown->get;
$loop->remove($server);
};
# Test 2: Multiple body chunks arrive in order
subtest 'Multiple http.response.body events work correctly' => sub {
my $streaming_app = async sub {
my ($scope, $receive, $send) = @_;
die "Unsupported: $scope->{type}" if $scope->{type} ne 'http';
while (1) {
my $event = await $receive->();
last if $event->{type} ne 'http.request';
last unless $event->{more};
}
await $send->({
type => 'http.response.start',
status => 200,
headers => [['content-type', 'text/plain']],
});
await $send->({ type => 'http.response.body', body => "First\n", more => 1 });
await $send->({ type => 'http.response.body', body => "Second\n", more => 1 });
await $send->({ type => 'http.response.body', body => "Third\n", more => 0 });
};
my $server = PAGI::Server->new(
app => $streaming_app,
host => '127.0.0.1',
port => 0,
quiet => 1,
);
$loop->add($server);
$server->listen->get;
my $port = $server->port;
my $http = Net::Async::HTTP->new;
$loop->add($http);
my $response = $http->GET("http://127.0.0.1:$port/")->get;
is($response->code, 200, 'Response status is 200');
my $body = $response->decoded_content;
like($body, qr/First\nSecond\nThird\n/, 'Body contains all chunks in order');
$server->shutdown->get;
$loop->remove($server);
};
# Test 3: Streaming without trailers terminates correctly
subtest 'Streaming without trailers terminates correctly' => sub {
my $no_trailer_app = async sub {
my ($scope, $receive, $send) = @_;
die "Unsupported: $scope->{type}" if $scope->{type} ne 'http';
while (1) {
my $event = await $receive->();
last if $event->{type} ne 'http.request';
last unless $event->{more};
}
await $send->({
type => 'http.response.start',
status => 200,
headers => [['content-type', 'text/plain']],
});
await $send->({ type => 'http.response.body', body => "Data 1\n", more => 1 });
await $send->({ type => 'http.response.body', body => "Data 2\n", more => 0 });
};
my $server = PAGI::Server->new(
app => $no_trailer_app,
host => '127.0.0.1',
port => 0,
quiet => 1,
);
$loop->add($server);
$server->listen->get;
my $port = $server->port;
my $http = Net::Async::HTTP->new;
$loop->add($http);
t/02-streaming.t view on Meta::CPAN
$server->shutdown->get;
$loop->remove($server);
};
# Test 4: Streaming with trailers (verify body content)
# Note: Net::Async::HTTP doesn't easily expose trailers, but we verify the body
# Trailers are verified manually with: curl -s -D - http://localhost:5000/
subtest 'Streaming with trailers - body content correct' => sub {
my $trailer_app = async sub {
my ($scope, $receive, $send) = @_;
die "Unsupported: $scope->{type}" if $scope->{type} ne 'http';
while (1) {
my $event = await $receive->();
last if $event->{type} ne 'http.request';
last unless $event->{more};
}
await $send->({
type => 'http.response.start',
status => 200,
headers => [['content-type', 'text/plain']],
trailers => 1,
});
await $send->({ type => 'http.response.body', body => "Body content\n", more => 0 });
await $send->({
type => 'http.response.trailers',
headers => [['x-test-trailer', 'trailer-value']],
});
};
my $server = PAGI::Server->new(
app => $trailer_app,
host => '127.0.0.1',
port => 0,
quiet => 1,
);
$loop->add($server);
$server->listen->get;
my $port = $server->port;
my $http = Net::Async::HTTP->new;
$loop->add($http);
my $response = $http->GET("http://127.0.0.1:$port/")->get;
is($response->code, 200, 'Response status is 200');
is($response->header('Transfer-Encoding'), 'chunked', 'Response uses chunked encoding');
my $body = $response->decoded_content;
like($body, qr/Body content/, 'Body content is correct');
$server->shutdown->get;
$loop->remove($server);
};
# Test 5: Client disconnect detection
subtest 'Client disconnect stops streaming app' => sub {
my $disconnect_detected = 0;
my $chunks_sent = 0;
my $slow_streaming_app = async sub {
my ($scope, $receive, $send) = @_;
die "Unsupported: $scope->{type}" if $scope->{type} ne 'http';
my $loop = IO::Async::Loop->new;
# Drain request body
while (1) {
my $event = await $receive->();
last if $event->{type} ne 'http.request';
last unless $event->{more};
}
await $send->({
type => 'http.response.start',
status => 200,
headers => [['content-type', 'text/plain']],
});
# Start watching for disconnect
my $disconnect_future = $receive->();
for my $i (1..5) {
# Check if client disconnected
if ($disconnect_future->is_ready) {
$disconnect_detected = 1;
last;
}
$chunks_sent++;
await $send->({ type => 'http.response.body', body => "Chunk $i\n", more => ($i < 5) ? 1 : 0 });
# Wait between chunks (allows disconnect to be detected)
if ($i < 5 && $loop) {
await $loop->delay_future(after => 0.2);
}
}
};
my $server = PAGI::Server->new(
app => $slow_streaming_app,
host => '127.0.0.1',
port => 0,
quiet => 1,
);
$loop->add($server);
$server->listen->get;
my $port = $server->port;
# Use raw socket connection so we can disconnect mid-stream
require IO::Socket::INET;
my $sock = IO::Socket::INET->new(
PeerHost => '127.0.0.1',
PeerPort => $port,
Proto => 'tcp',
Timeout => 5,
);
ok($sock, 'Connected to server');
# Send HTTP request
print $sock "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
# Read just the headers and first chunk
my $data = '';
$sock->blocking(0);
# Give server time to start streaming
my $timeout = time + 2;
while (time < $timeout) {
$loop->loop_once(0.1);
my $buf;
my $n = $sock->sysread($buf, 4096);
if (defined $n && $n > 0) {
$data .= $buf;
# Once we have some body data, disconnect
if ($data =~ /Chunk 1/) {
last;
}
}
}
like($data, qr/HTTP\/1\.1 200/, 'Received HTTP response');
like($data, qr/Chunk 1/, 'Received first chunk');
# Close connection abruptly
close($sock);
# Give server time to detect disconnect and finish
$loop->loop_once(0.5);
# The app should have detected disconnect before sending all 5 chunks
# Note: Due to timing, the app may or may not have detected the disconnect
# depending on how fast the TCP close propagates. We'll verify the mechanism works.
ok($chunks_sent >= 1, "Sent at least 1 chunk (sent $chunks_sent)");
$server->shutdown->get;
$loop->remove($server);
};
done_testing;
( run in 0.588 second using v1.01-cache-2.11-cpan-995e09ba956 )