PAGI
view release on metacpan or search on metacpan
lib/PAGI/Tutorial.pod view on Meta::CPAN
}
await $send->($event);
};
await $app->($scope, $receive, $wrapped_send);
};
}
This is how response-touching middleware (CORS headers, compression,
content-length) works: it intercepts the event stream as it flows out. Wrapping
C<$receive> works the same way for events flowing in -- buffer a request body,
decode a format, or fold in synthetic events.
=head3 Short-Circuiting
A middleware does not have to call the inner app at all. To reject a request,
respond yourself and return without calling C<$app>:
sub require_api_key {
my ($app) = @_;
return async sub {
my ($scope, $receive, $send) = @_;
my %h = map { lc $_->[0] => $_->[1] } @{ $scope->{headers} // [] };
unless (($h{'x-api-key'} // '') eq 'secret') {
await $send->({
type => 'http.response.start',
status => 401,
headers => [ [ 'content-type', 'text/plain' ] ],
});
await $send->({ type => 'http.response.body', body => 'Unauthorized' });
return; # never reach $app
}
await $app->($scope, $receive, $send);
};
}
=head3 Errors and the Return Value
A middleware B<must> let exceptions from the inner app propagate -- never
swallow them silently. If you catch in order to log, rethrow:
my $result = eval { await $app->($scope, $receive, $send) };
if (my $err = $@) {
warn "app error: $err";
die $err; # propagate, do not swallow
}
return $result;
Note what that C<return> is for. The value an application resolves to is
B<inert>: the server assigns it no meaning (see
L<PAGI::Spec/"Application Completion Contract">). A middleware returns only to
propagate the inner app's B<Future> -- its completion and any exception. To
observe or rewrite the response, wrap C<$send>; never reach for the return
value. (Frameworks built on PAGI may use return values for their own internal
composition, but they convert them to events before handing control back to the
server.)
=head3 Stacking
Because each layer is itself an application, composing them is just nesting:
my $app = require_api_key( with_request_id( with_logging( $bare_app ) ) );
The outermost wrapper runs first on the way in and last on the way out. Order
matters: put logging outside auth if you want to log rejected requests, inside
if you do not.
Everything above is plain protocol -- no toolkit required. PAGI-Tools ships
ready-made middleware (logging, sessions, CORS, compression, and more) and
ergonomic helpers for writing your own; each is built exactly like the wrappers
here. See L<PAGI::Tools::Tutorial>.
=head2 2.9 UTF-8 Handling
PAGI follows specific rules for UTF-8 encoding/decoding:
=head3 Request Paths
$scope->{path} # UTF-8 decoded string (use this for display/matching)
$scope->{raw_path} # Raw bytes (preserves original encoding)
PAGI uses Mojolicious-style path decoding: percent-encoded bytes are first
URL-decoded, then UTF-8 decoded. If UTF-8 decoding fails (invalid byte
sequences), the original URL-decoded bytes are preserved as-is.
Example:
# URL: /users/%E4%B8%AD%E6%96%87
$scope->{path} = '/users/䏿' # (decoded)
$scope->{raw_path} = '/users/%E4%B8%AD%E6%96%87' # (raw bytes)
# Invalid UTF-8: /users/%FF%FE
$scope->{path} = '/users/\xFF\xFE' # (falls back to bytes)
$scope->{raw_path} = '/users/%FF%FE' # (raw bytes)
=head3 Query Strings
Query strings are raw bytes. Use L<URI> to parse them properly:
use strict;
use warnings;
use Future::AsyncAwait;
use URI;
use Encode qw(encode_utf8);
async sub app {
my ($scope, $receive, $send) = @_;
die "Expected http scope" unless $scope->{type} eq 'http';
# Use URI to parse query string (handles decoding automatically)
my $uri = URI->new('?' . ($scope->{query_string} // ''));
my %params = $uri->query_form;
my $name = $params{name} // 'World';
await $send->({
type => 'http.response.start',
status => 200,
headers => [['content-type', 'text/plain; charset=utf-8']],
});
( run in 1.572 second using v1.01-cache-2.11-cpan-6aa56a78535 )