PAGI-Tools
view release on metacpan or search on metacpan
lib/PAGI/Tools/Cookbook.pod view on Meta::CPAN
=head2 Deciding "did the handler produce a response?"
A framework that lets handlers populate C<< $ctx->res >> and then auto-sends it
needs to know, after the handler returns, whether to send, skip, or fall through
to the next route. That is a three-way decision, and it is a precedence ladder â
not a single predicate:
# After running the handler, which mutated $ctx->res:
if ($ctx->res->is_sent) {
# The handler already took the connection itself â e.g. it called
# ->writer($send) for live streaming, or used SSE/WebSocket. Do nothing;
# sending again would be a double-send.
}
elsif ($ctx->res->has_body_source || $ctx->res->has_status) {
# The handler registered a body, or set a status (a bare 204 / redirect
# has no body but IS a response). Send it once, through the guard.
await $ctx->respond($ctx->res);
}
else {
# The handler touched nothing send-able â fall through to the next match
# (or a 404).
}
Three things make this ladder correct:
=over 4
=item * B<Check C<is_sent> first.> A handler that called C<< ->writer($send) >>
has already emitted headers (C<writer> marks the response sent), but
C<has_body_source> stays false because the live-writer path bypasses the body
slots. Without the C<is_sent> guard you would mistake an actively-streaming
response for "nothing produced".
=item * B<C<|| has_status> is required.> A C<302> redirect or a C<204> can have
no body source, yet it is a real response. C<has_status> catches the
status-only case so you do not 404 a legitimate empty-body response.
=item * B<Never inspect the private slots.> Use C<has_body_source>, not
C<< exists $res->{_stream} >> and friends â those are private and may change
(see L<PAGI::Response/SUBCLASSING (FRAMEWORK INTEGRATION)>). The predicate is
the supported way to ask.
=back
If instead your framework has handlers B<return> the response (PAGI's own
endpoint contract), you do not need this ladder at all: the returned value is
the answer, and "fall through" is an explicit sentinel you define, not an
inference from an empty response.
=head1 TESTING
Use L<PAGI::Test::Client> to test apps directly without a running server:
use strict;
use warnings;
use Test2::V0;
use PAGI::Test::Client;
# Load your app
my $app = require './app.pl';
my $client = PAGI::Test::Client->new(app => $app);
subtest 'GET /' => sub {
my $res = $client->get('/');
is $res->status, 200, 'status is 200';
like $res->text, qr/Hello/, 'body contains Hello';
};
subtest 'POST /api/users' => sub {
my $res = $client->post('/api/users',
json => { name => 'Alice' },
);
is $res->status, 201, 'status is 201';
is $res->json->{id}, 1, 'returns user id';
};
subtest 'form submission' => sub {
my $res = $client->post('/login',
form => { user => 'admin', pass => 'secret' },
);
is $res->status, 302, 'redirects after login';
# Session cookies persist across requests
my $dashboard = $client->get('/dashboard');
is $dashboard->status, 200, 'authenticated access';
};
subtest 'custom headers' => sub {
my $res = $client->get('/api/data',
headers => { Authorization => 'Bearer token123' },
);
is $res->status, 200;
};
done_testing;
See L<PAGI::Test::Client> for the full API including WebSocket and SSE testing.
=head1 SEE ALSO
=over 4
=item * L<PAGI::Tutorial> - Learn the PAGI protocol from the ground up
=item * L<PAGI::Tools::Tutorial> - Guide to middleware, helpers, and built-in apps
=item * L<PAGI::App::Router> - Functional routing
=item * L<PAGI::Endpoint::Router> - Class-based routing
=item * L<PAGI::Middleware::Session> - Session management
=item * L<PAGI::App::WebSocket::Chat> - Multi-room chat application
=item * L<PAGI::Middleware::XSendfile> - Delegate file serving to reverse proxy
=item * L<PAGI::Test::Client> - Test apps without a running server
=back
( run in 1.448 second using v1.01-cache-2.11-cpan-6aa56a78535 )