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 )