NAME
PAGI::PSGI - coming to PAGI from PSGI
DESCRIPTION
If you have written PSGI applications or middleware -- and especially if you maintain or are building a web framework -- this guide is the fast path into PAGI. You already know most of it; the shapes just move around a little.
PSGI models an application as a single synchronous coderef: it takes an $env hashref and returns [$status, \@headers, \@body]. That has served Perl well for over a decade, but it cannot express a connection that stays open and exchanges many messages -- long-poll, Server-Sent Events, WebSockets -- because there is only one way in (the request) and one way out (the response).
PAGI keeps the idea you like -- your application is a coderef -- but makes it asynchronous and message-based. Instead of returning a response, your app is handed two coderefs: $receive for events coming from the client and $send for events going back, both returning Futures so backpressure is explicit.
The headline for a PSGI user is this: PAGI is a superset of PSGI, so you do not have to rewrite anything. You can run your existing PSGI application under a PAGI server today and add asynchronous, WebSocket, or SSE endpoints beside it, one route at a time. See "ADOPTING PAGI INCREMENTALLY".
THE MENTAL MODEL
Four things move when you go from PSGI to PAGI: the environment becomes a scope, the returned response becomes sent events, reading the body becomes receiving events, and middleware wraps an async sub instead of a plain one.
The request: $env becomes $scope
A PAGI application is invoked with a $scope hashref that plays the role of PSGI's $env. The keys are renamed and tidied (lower-cased header names, structured values), but the information is the same. The mapping:
REQUEST_METHODis$scope->{method}PATH_INFOis$scope->{path}(already UTF-8 decoded; the raw bytes are in$scope->{raw_path})SCRIPT_NAMEis$scope->{root_path}QUERY_STRINGis$scope->{query_string}SERVER_PROTOCOLis$scope->{http_version}psgi.url_schemeis$scope->{scheme}SERVER_NAME/SERVER_PORTare$scope->{server}REMOTE_ADDR/REMOTE_PORTare$scope->{client}CONTENT_TYPE/CONTENT_LENGTHand theHTTP_*keys are all read from$scope->{headers}- There is no
psgi.errorsstream -- error logging is the server's responsibility, not an app-visible filehandle
The one shape change worth noting is headers. PSGI flattens them into a single array with implicit pairs (['Content-Type', 'text/html', 'X-Custom', 'v']); PAGI uses an arrayref of explicit [name, value] tuples with lower-case names and byte values:
[ [ 'content-type', 'text/html' ], [ 'x-custom', 'v' ] ]
This makes duplicate headers (Set-Cookie) and header manipulation straightforward, and matches the ASGI model PAGI is modelled on. The full key table is in PAGI::Spec::Www.
The response: returning a value becomes await $send
In PSGI you build the whole response and return it. In PAGI you send it: one http.response.start event with the status and headers, then one or more http.response.body events, the last carrying more => 0.
PSGI:
my $app = sub {
my ($env) = @_;
return [
200,
[ 'Content-Type' => 'text/plain' ],
[ "You asked for $env->{PATH_INFO}\n" ],
];
};
PAGI:
use Future::AsyncAwait;
my $app = async sub {
my ($scope, $receive, $send) = @_;
await $send->({
type => 'http.response.start',
status => 200,
headers => [ [ 'content-type', 'text/plain' ] ],
});
await $send->({
type => 'http.response.body',
body => "You asked for $scope->{path}\n",
more => 0,
});
};
Bodies go out as bytes -- encode text yourself, exactly as you would when returning bytes from PSGI.
Reading the body: psgi.input becomes $receive
PSGI hands you a psgi.input stream to read the request body. In PAGI the body arrives as a sequence of http.request events; you loop on $receive until one has more false:
my $body = '';
while (1) {
my $event = await $receive->();
last if $event->{type} ne 'http.request'; # e.g. http.disconnect
$body .= $event->{body} // '';
last unless $event->{more};
}
As in PSGI, the bytes are raw and undecoded; the application decides how to interpret them.
Streaming and full duplex come for free
PSGI bolts streaming on with psgi.streaming and the delayed-response $responder->([...]) callback. PAGI has no separate streaming API: to stream, you just await $send-> more http.response.body events. And because you hold $receive and $send at the same time, you can interleave them -- which is what makes WebSockets and SSE ordinary PAGI applications rather than special cases.
Middleware wraps an async sub
PSGI middleware is a sub that takes the inner $app and returns a new app that adjusts $env or rewrites the response. PAGI middleware is exactly the same shape, one level async: it takes the inner async sub and returns a new one, free to adjust $scope before calling inner, wrap $send to rewrite outgoing events, or short-circuit entirely. Adding a response header:
sub add_header {
my ($app) = @_;
return async sub {
my ($scope, $receive, $send) = @_;
my $wrapped_send = async sub {
my ($event) = @_;
if ($event->{type} eq 'http.response.start') {
push @{ $event->{headers} }, [ 'x-wrapped', '1' ];
}
return await $send->($event);
};
return await $app->($scope, $receive, $wrapped_send);
};
}
One difference will bite you if you do not know it. In PSGI, $env is a single hashref shared down the whole stack and mutated in place, so a key one layer sets is visible everywhere. In PAGI, middleware clone the scope before modifying it, and the clone is shallow -- so a plain scalar you set in one layer does not propagate to another the way a $env key would. This is deliberate (it isolates each layer's scope edits), and the way to share state on purpose is to mutate through a reference rather than set a top-level scalar -- PAGI::Stash wraps exactly that. See "Scope is cloned per layer, not shared" in PAGI::Building for the full model.
The middleware model for framework authors is covered in depth in PAGI::Building. Server-advertised psgix.* extensions become $scope->{extensions} (see PAGI::Spec::Extensions).
PAGI IS A SUPERSET OF PSGI
Because the two models map so directly, any PSGI application can be expressed as a PAGI application by a thin translation: build a %env from $scope, call the PSGI coderef, and turn its returned arrayref into PAGI events. The whole idea fits on a screen:
sub psgi_to_pagi {
my ($psgi_app) = @_;
return async sub {
my ($scope, $receive, $send) = @_;
# ... read the request body into $body, as shown above ...
open my $input, '<', \$body;
my %env = (
REQUEST_METHOD => $scope->{method},
PATH_INFO => $scope->{path},
QUERY_STRING => $scope->{query_string} // '',
'psgi.version' => [ 1, 1 ],
'psgi.input' => $input,
# ... plus the HTTP_* headers from $scope->{headers} ...
);
my ($status, $headers, $chunks) = @{ $psgi_app->(\%env) };
await $send->({ type => 'http.response.start', status => $status, headers => $headers });
await $send->({ type => 'http.response.body', body => join('', @$chunks), more => 0 });
};
}
You do not write this. It is shown only to make the point that the relationship is a thin translation. The production adapter -- PAGI::App::WrapPSGI, in the PAGI-Tools distribution -- does the full job (complete header mapping, SCRIPT_NAME/root_path, body handling, and more).
ADOPTING PAGI INCREMENTALLY
This is the part that matters: you migrate without a rewrite.
Run your existing PSGI app under PAGI. Wrap it once:
use PAGI::App::WrapPSGI; my $pagi_app = PAGI::App::WrapPSGI->new( psgi_app => $my_psgi_app )->to_app;Now
$pagi_appruns on any conforming PAGI server. A runnable demonstration ships withPAGI-Toolsat examples/09-psgi-bridge.Add native PAGI handlers beside it. Dispatch new routes -- a WebSocket endpoint, an SSE stream, an async HTTP handler that awaits a slow backend -- to ordinary PAGI
async subs, and fall back to the wrapped PSGI app for everything else.Migrate route by route as it makes sense, or leave the bulk of your application as PSGI forever. The wrapper is not a temporary scaffold; it is a supported way to keep synchronous code synchronous.
Two things to know about running synchronous PSGI code under an asynchronous server:
- A wrapped PSGI app is still blocking.
-
It runs synchronously, and while it runs it blocks the worker's event loop -- under a single-process server, that stalls every other connection on the server. Run it the way you already run PSGI: under a pre-forking, multi-worker server. PAGI::Server's worker mode (
--workers N) gives each worker its own event loop, so a slow PSGI request ties up only its own worker while the others keep serving, exactly as it would behind Starman. Your native PAGI handlers are asynchronous and unaffected -- only the wrapped synchronous code blocks. - Streaming buffers today.
-
PAGI::App::WrapPSGIcurrently collects a streaming PSGI response fully before sending it, so a PSGI app that relies on incremental flushing will not stream through the adapter yet. For a streaming route, write a native PAGI handler instead. Ordinary request/response PSGI apps are unaffected.
FOR FRAMEWORK AUTHORS
If you maintain a framework, or want to build a new one, PAGI is a good foundation precisely because it is small: the entire contract is async sub ($scope, $receive, $send), and it is stable (see the stability notice in PAGI). Everything your framework offers -- routing, middleware, request and response objects, content negotiation -- is a layer over that, exactly as it is over PSGI today, with WebSocket, SSE, and async available without leaving the framework.
PAGI::Building walks through that layering, and examples/mini-framework is a complete little framework -- routing, path parameters, async dispatch -- in about fifty lines of plain Perl.
SEE ALSO
- PAGI::Building - building a framework or toolkit on PAGI
- PAGI::Tutorial - the protocol, taught from scratch
- PAGI::Cookbook - worked, runnable recipes
- PAGI::App::WrapPSGI - the PSGI adapter (
PAGI-Toolsdistribution) - PSGI - the synchronous predecessor
- PAGI::EventLoops - event loops, non-blocking I/O, and binding a server to a loop
AUTHOR
John Napiorkowski <jjnapiork@cpan.org>
COPYRIGHT AND LICENSE
This software is licensed under the same terms as Perl itself.