PAGI (Perl Asynchronous Gateway Interface) Specification

Note: All code examples use modern Perl with subroutine signatures and `` for clarity. This is a documentation preference and not a requirement of the PAGI specification.

Version: 0.2 (Draft)

Introduction

While work has been done to support asynchronous response handling in PSGI (the psgi.nonblocking environment key), support for protocols with multiple input events (such as WebSockets) remained hacks. The use of non-blocking or asynchronous handling based on PSGI never took off. The PSGI specification hasn't changed since July 2013. Since then, asynchronous programming in has taken off in many programming languages, including in Perl: although Perl does not natively support the async/await pattern like Python or JavaScript, the module Future::AsyncAwait implements the pattern since 24 January 2018 for Perl versions 5.16 and up.

Considering the above and the fact that similar specifications have existed for several years now for other languages, the time has arrived to create a fully asynchronous webserver gateway interface specification for Perl.

Perl does not natively support Python's async/await pattern, but we can achieve similar functionality using Future::AsyncAwait which provides async/await syntax on top of Future.pm. Below is how we translate key ASGI components into Perl.

Abstract

This document proposes a standard interface between network protocol servers (particularly web servers) and Perl applications, intended to support multiple common protocol styles (including HTTP/1.1, HTTP/2, and WebSocket).

This base specification defines the APIs by which servers interact with and run application code. Each supported protocol (such as HTTP) includes a sub-specification detailing how to encode and decode that protocol into structured messages.

Rationale

PSGI has worked well as a standard interface for synchronous Perl web applications. However, its design is tied to the HTTP-style request/response cycle, and cannot support full-duplex protocols such as WebSockets.

PAGI preserves a simple application interface while introducing a fully asynchronous message-based abstraction, enabling data to be sent and received at any time during a connection's lifecycle.

It defines:

The primary goal is to support WebSockets, HTTP/2, and Server-Sent Events (SSE) alongside HTTP/1.1, while maintaining compatibility with existing PSGI applications through a transitional adapter layer.

Overview

PAGI consists of two main components:

Applications are written as asynchronous subroutines using Future::AsyncAwait. They receive a scope describing the connection, and two coderefs, $recv and $send, which return Futures representing event input and output.

Unlike PSGI, PAGI applications persist for the entire connection lifecycle. They process incoming events from the server and emit outgoing events in response.

Two important concepts in PAGI:

Specification Details

Connection Scope

Each incoming connection causes the application to be invoked with a scope hashref. Its keys include:

Clarification on PAGI version keys:

The scope describes the full lifetime of the connection, and persists until the connection is closed. Some protocols (e.g., HTTP) may treat each request as a distinct scope, while others (e.g., WebSocket) maintain one persistent scope for the entire session.

Specifically:

Applications may need to wait for an initial event before sending any messages, depending on the protocol specification.

If the application does not recognize or support scope->{type}, it MUST throw an exception. The PAGI server must handle such exceptions gracefully, by closing the connection promptly, optionally logging the issue and providing relevant protocol-specific error responses or messages.

This explicit requirement ensures that servers never assume application-protocol compatibility without explicit support declared by implementations.

Events

PAGI defines communication in terms of discrete events.

The type key in each event must be a namespaced string of the form protocol.message_type, such as http.request or websocket.send. This convention ensures clear protocol dispatching and avoids naming collisions.

Reserved type prefixes include:

Custom user-defined protocols should avoid clashing with these prefixes.

my $event = await $recv->();
await $send->({ type => 'http.response.start', ... });

Permitted Event Data Types

The following types are permitted in event data structures:

Note: Booleans are intentionally omitted from the core spec to avoid ambiguity in Perl, which lacks a native boolean type. In contexts where ASGI would use true/false, PAGI protocol specifications will define expected values explicitly -- usually 0 or 1, or by the presence/absence of a defined value.

Applications

PAGI applications are single coderefs returning Futures.

Applications may optionally support lifecycle hooks such as shutdown. If supported, the PAGI server should call:

$app->on_shutdown(sub {
    # Optional shutdown logic (e.g., flush logs, close DB)
});

This pattern is reserved and may be formalized in a future PAGI extension.

Applications must throw an exception if the incoming scope->{type} is not supported. This prevents the server from assuming a protocol is handled when it is not, and avoids ambiguity in multi-protocol deployments.

Each application is expected to run for the duration of a connection and may remain alive briefly after disconnect to perform cleanup. The interface is:

use Future::AsyncAwait;

async sub application ($scope, $recv, $send) {
    if ($scope->{type} ne 'http') {
        die "Unsupported protocol type: $scope->{type}";
    }

    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,
    });

    return;
}

Each application is called per-connection, and remains active until the connection closes and cleanup completes.

Application Completion Contract

The application's returned Future represents request completion. When this Future resolves, the server considers the response finished and may close the connection, write access logs, or handle the next request.

Applications must await all $send calls before their Future completes:

# CORRECT: await all sends
async sub app ($scope, $recv, $send) {
    await $send->({ type => 'http.response.start', status => 200, headers => [] });
    await $send->({ type => 'http.response.body', body => 'Hello' });
}

# WRONG: send not awaited, app Future resolves before send completes
async sub app ($scope, $recv, $send) {
    $send->({ type => 'http.response.body', body => 'Hello' })->retain;
    return;  # App Future resolves immediately, send may not complete!
}

Work scheduled via ->retain or other fire-and-forget patterns will race with connection cleanup and produce undefined behavior. The server has no visibility into retained Futures - they exist outside the application's Future tree.

Use PAGI::Middleware::Lint during development to detect incomplete responses.

Legacy Applications

Legacy (PSGI) applications are not async and follow a synchronous interface. Support for these can be implemented via a compatibility adapter.

Adapters must call the PSGI app once with $env and transform the response into PAGI event messages.

Protocol Specifications

Each protocol defines:

Common examples:

The type field in scope and events identifies the protocol and message:

$event->{type} eq 'http.request';
$scope->{type} eq 'websocket';

Applications must throw an exception if the protocol is unknown.

Current protocol specifications:

Middleware

PAGI middleware wraps an application:

use Future::AsyncAwait;

sub middleware ($app) {
    return async sub ($scope, $recv, $send) {
        my $modified_scope = { %$scope };
        $modified_scope->{custom} = 1;

        my $result = eval { await $app->($modified_scope, $recv, $send) };
        if (my $error = $@) {
            # Middleware MUST propagate exceptions clearly without silently swallowing them.
            # Optionally, middleware can log or handle the error explicitly before rethrowing.
            warn "Middleware encountered an application error: $error";
            die $error;
        }
        return $result;

        # Middleware should default to shallow cloning and explicitly deep clone if required.
    };
}

Middleware must not mutate the original $scope in-place; always clone before modification.

Cancellation and Disconnects

If the server detects client-driven cancellation (e.g., HTTP/2 resets or WebSocket disconnects), it must deliver the appropriate disconnect event to the application:

Applications receiving these events should immediately halt unnecessary work, optionally send minimal acknowledgments if the protocol allows it, and perform cleanup for open resources or background tasks. Servers must also ensure timely cleanup after the application finishes its cancellation handling.

Error Handling

Servers must raise exceptions if:

Applications should do the same when receiving malformed events.

Extra fields in events must not cause errors -- this supports forward-compatible upgrades.

If $send is called after connection closure, it should be a no-op unless specified otherwise.

Extensions

Servers may expose additional features via the extensions key in the scope:

$scope->{extensions} = {
    fullflush => {},
};

Applications may send new events such as:

await $send->({ type => 'http.fullflush' });

Only if the server declares support in extensions.

Strings and Unicode

All keys in scope and event hashrefs must be strings that are valid UTF-8 when interpreted as bytes. The UTF-8 flag is not required, but the keys must decode cleanly as UTF-8.

Byte content (e.g., body payloads) must be passed as Perl scalars without the UTF-8 flag set. Applications are responsible for encoding/decoding appropriately.

Version History

This document is placed in the public domain.