NAME

PAGI - Perl Asynchronous Gateway Interface

DEDICATION

This project is dedicated to the memory of Matt S. Trout (mst), who I wish was still around to tell me all the things wrong with my code while simultaneously offering brilliant ideas to make it better.

Matt encouraged my first CPAN contribution. Without that encouragement, PAGI and pretty much everything I've released on CPAN over 20+ years would never have happened.

Thank you, Matt. The Perl community misses you.

SYNOPSIS

# Raw PAGI application
use Future::AsyncAwait;

async sub app {
    my ($scope, $receive, $send) = @_;

    die "Unsupported: $scope->{type}" if $scope->{type} ne 'http';

    await $send->({
        type    => 'http.response.start',
        status  => 200,
        headers => [['content-type', 'text/plain']],
    });

    await $send->({
        type => 'http.response.body',
        body => 'Hello from PAGI!',
        more => 0,
    });
}

DESCRIPTION

PAGI (Perl Asynchronous Gateway Interface) is a specification for asynchronous Perl web applications, designed as a spiritual successor to PSGI. It defines a standard interface between async-capable Perl web servers, frameworks, and applications, supporting HTTP/1.1, WebSocket, and Server-Sent Events (SSE).

This document presents a high level overview of PAGI. If you are a web developer who is looking to write PAGI compliant apps, you should also review the tutorial: PAGI::Tutorial.

Beta Software Notice

WARNING: This is beta software.

This distribution has different stability levels:

Stable: PAGI Specification

The PAGI specification ($scope, $receive, $send interface) is stable. Breaking changes will not be made except for critical security issues. Raw PAGI applications you write today will continue to work.

See PAGI::Spec

Stable: PAGI::Server

The reference server has been validated against PAGI::Compliance and handles HTTP/1.1, WebSocket, and SSE correctly. However, it has not been battle-tested in production. Recommendation: Run behind a reverse proxy like nginx, Apache, or Caddy for production deployments.

Although I am marking this stable in terms of its interface, I reserve the right to make internal code changes and reorganizations. You should not rely on internal details of the server for your application, just the PAGI::Spec interface.

See PAGI::Server, PAGI::Server::Compliance.

Unstable: Everything Else

PAGI::Request, PAGI::Response, PAGI::WebSocket, PAGI::SSE, PAGI::Endpoint::Router, PAGI::App::Router, middleware, and bundled apps are subject to change. These APIs may be modified to fix security issues, resolve architectural problems, or improve the developer experience. You can use them, but I reserve the right to make breaking changes between releases as we continue to shape how these helpers work and impact the PAGI ecosystem.

If you are interested in contributing to the future of async Perl web development, your feedback, bug reports, and contributions are welcome.

COMPONENTS

This distribution includes:

PAGI::Server

Reference server implementation supporting HTTP/1.1, WebSocket, SSE, and multi-worker mode with pre-forking.

PAGI::Lifespan

Lifecycle management wrapper for PAGI applications. Handles startup/shutdown callbacks and injects shared application state into request scopes.

PAGI::Request

Convenience wrapper for HTTP request handling with body parsing, headers, and state/stash accessors.

PAGI::WebSocket

Convenience wrapper for WebSocket connections with JSON support, heartbeat, and state/stash accessors.

PAGI::SSE

Convenience wrapper for Server-Sent Events with event formatting, keepalive, and periodic sending.

PAGI::Endpoint::Router

Class-based router supporting HTTP, WebSocket, and SSE routes with parameter capture and subrouter mounting.

PAGI::App::Router

Functional router for building PAGI applications with Express-style routing.

PAGI::Middleware::*

Collection of middleware components for common web application needs.

PAGI::App::*

Bundled applications for common functionality (static files, health checks, metrics, etc.).

PAGI APPLICATION INTERFACE

PAGI applications are async coderefs with this signature:

async sub app {
    my ($scope, $receive, $send) = @_;
 ... }

Parameters

$scope

Hashref containing connection metadata including type, headers, path, method, query string, and server-advertised extensions.

$receive

Async coderef that returns a Future resolving to the next event from the client (e.g., request body chunks, WebSocket messages).

$send

Async coderef that takes an event hashref and returns a Future. Used to send responses back to the client.

Scope Types

Applications dispatch on $scope->{type}:

http

HTTP request/response (one scope per request)

websocket

Persistent WebSocket connection

sse

Server-Sent Events stream

lifespan

Process startup/shutdown lifecycle events

UTF-8 HANDLING OVERVIEW

PAGI scopes provide decoded text where mandated by the spec and preserve raw bytes where the application must decide. Broad guidance:

  • $scope-{path}> is UTF-8 decoded from the percent-encoded $scope-{raw_path}>. If UTF-8 decoding fails (invalid byte sequences), the original bytes are preserved as-is (Mojolicious-style fallback). If you need exact on-the-wire bytes, use raw_path.

  • $scope-{query_string}> and request bodies arrive as percent-encoded or raw bytes. Higher-level frameworks may auto-decode with replacement by default, but raw values remain available via query_string and the body stream. If you need strict validation, decode yourself with Encode and FB_CROAK.

  • Response bodies and header values sent over the wire must be encoded to bytes. If you construct raw events, encode with Encode::encode('UTF-8', $str, FB_CROAK) (or another charset you set in Content-Type) and set Content-Length based on byte length.

Raw PAGI example with explicit UTF-8 handling:

use Future::AsyncAwait;
use Encode qw(encode decode);

async sub app {
    my ($scope, $receive, $send) = @_;

    # Handle lifespan if your server sends it; otherwise fail on unsupported types.
    die "Unsupported type: $scope->{type}" unless $scope->{type} eq 'http';

    # Decode query param manually (percent-decoded bytes)
    my $text = '';
    if ($scope->{query_string} =~ /text=([^&]+)/) {
        my $bytes = $1; $bytes =~ s/%([0-9A-Fa-f]{2})/chr hex $1/eg;
        $text = decode('UTF-8', $bytes, Encode::FB_DEFAULT);  # replacement for invalid
    }

    my $body = "You sent: $text";
    my $encoded = encode('UTF-8', $body, Encode::FB_CROAK);

    await $send->({
        type    => 'http.response.start',
        status  => 200,
        headers => [
            ['content-type',   'text/plain; charset=utf-8'],
            ['content-length', length($encoded)],
        ],
    });
    await $send->({
        type => 'http.response.body',
        body => $encoded,
        more => 0,
    });
}

QUICK START

# Install dependencies
cpanm --installdeps .

# Run the test suite
prove -l t/

# Start a server with a PAGI app
pagi-server --app examples/01-hello-http/app.pl --port 5000

# Test it
curl http://localhost:5000/

REQUIREMENTS

  • Perl 5.18+

  • IO::Async (event loop)

  • Future::AsyncAwait (async/await support)

SEE ALSO

PAGI::Server - Reference server implementation
PAGI::Request - Convenience wrapper for request handling
PSGI - The synchronous predecessor to PAGI
IO::Async - Event loop used by PAGI::Server
Future::AsyncAwait - Async/await for Perl

CONTRIBUTING

This project is in active development. If you're interested in advancing async web programming in Perl, contributions are welcome:

  • Bug reports and feature requests

  • Documentation improvements

  • Test coverage

  • Protocol support (HTTP/2, HTTP/3)

  • Performance optimizations

AUTHOR

John Napiorkowski <jjnapiork@cpan.org>

LICENSE

This software is licensed under the same terms as Perl itself.