PAGI-Server
view release on metacpan or search on metacpan
t/sse-close.t view on Meta::CPAN
use strict;
use warnings;
use Test2::V0;
use IO::Async::Loop;
use Future::AsyncAwait;
use FindBin;
use lib "$FindBin::Bin/../lib";
eval { require Future::IO::Impl::IOAsync; 1 }
or plan skip_all => 'Future::IO::Impl::IOAsync required for SSE tests';
use PAGI::Server;
use IO::Socket::INET;
plan skip_all => "Server integration tests not supported on Windows" if $^O eq 'MSWin32';
# Regression for the sse.close send-event (HTTP/1.1):
# - sse.close is accepted (does not raise) and ends the stream;
# - sending after sse.close raises (failed Future);
# - the post-close event never reaches the wire;
# - the server closes the connection.
# Both an explicit sse.close and a plain return must end the stream identically
# on the wire (D3: keep return-to-end valid).
my $loop = IO::Async::Loop->new;
sub create_server {
my ($app) = @_;
my $server = PAGI::Server->new(
app => $app, host => '127.0.0.1', port => 0, quiet => 1, shutdown_timeout => 1,
);
$loop->add($server);
$server->listen->get;
return $server;
}
# Open an SSE GET, read raw bytes until the server closes (EOF) or a deadline,
# then drain the loop so the app coroutine finishes. Returns (wire, saw_eof).
sub sse_get {
my ($port) = @_;
my $sock = IO::Socket::INET->new(
PeerAddr => '127.0.0.1', PeerPort => $port, Proto => 'tcp', Timeout => 5,
) or return ('', 0);
print $sock "GET / HTTP/1.1\r\nHost: 127.0.0.1:$port\r\nAccept: text/event-stream\r\n\r\n";
$sock->blocking(0);
my $wire = '';
my $eof = 0;
my $deadline = time + 5;
while (time < $deadline) {
my $buf;
my $n = sysread($sock, $buf, 4096);
if (defined $n && $n > 0) { $wire .= $buf }
elsif (defined $n && $n == 0) { $eof = 1; last } # server closed the stream
$loop->loop_once(0.05);
}
close $sock;
$loop->loop_once(0.05) for 1 .. 20; # let the app coroutine run to completion
return ($wire, $eof);
}
subtest 'sse.close ends the stream; send-after-close raises' => sub {
my ($close_ok, $post_close_raised) = (0, 0);
my $app = async sub {
my ($scope, $receive, $send) = @_;
die "expected sse scope" unless ($scope->{type} // '') eq 'sse';
await $send->({ type => 'sse.start', status => 200,
headers => [ ['content-type', 'text/event-stream'] ] });
await $send->({ type => 'sse.send', event => 'tick', data => '1' });
eval { await $send->({ type => 'sse.close', reason => 'done_testing' }); $close_ok = 1; 1 };
# After sse.close, any further send MUST raise.
eval { await $send->({ type => 'sse.send', event => 'late', data => 'LATE' }); 1 }
or $post_close_raised = 1;
};
my $server = create_server($app);
my ($wire, $eof) = sse_get($server->port);
like($wire, qr/HTTP\/1\.1 200/, '200 OK');
like($wire, qr/content-type:\s*text\/event-stream/i, 'event-stream content type');
like($wire, qr/data: 1/, 'tick event delivered before close');
ok($close_ok, 'sse.close was accepted (did not raise)');
ok($post_close_raised, 'sse.send after sse.close raised');
unlike($wire, qr/LATE/, 'post-close event did not reach the wire');
ok($eof, 'server closed the connection after sse.close');
$server->shutdown->get;
};
subtest 'return-to-end still terminates the stream (D3)' => sub {
my $app = async sub {
my ($scope, $receive, $send) = @_;
await $send->({ type => 'sse.start', status => 200,
headers => [ ['content-type', 'text/event-stream'] ] });
await $send->({ type => 'sse.send', event => 'tick', data => 'R' });
return; # no sse.close -- end by returning
};
my $server = create_server($app);
my ($wire, $eof) = sse_get($server->port);
like($wire, qr/data: R/, 'event delivered');
ok($eof, 'server closed the connection on return');
$server->shutdown->get;
};
done_testing;
( run in 0.616 second using v1.01-cache-2.11-cpan-7fcb06a456a )