PAGI

 view release on metacpan or  search on metacpan

docs/specs/www.mkdn  view on Meta::CPAN


Keys:

- `type` -- `"http.request"`
- `body` (Bytes, default `""`) -- Request body chunk
- `more` (Int, default `0`) -- `1` if more body data is forthcoming, otherwise `0`

### Response Start - `send` event

Note: Protocol servers are NOT required to flush on `http.response.start`, giving flexibility to emit an error response in case of internal application errors before data is sent.

Transfer-Encoding headers sent by the application must be ignored. Content-Encoding (e.g. gzip) is under application control.

Keys:

- `type` -- `"http.response.start"`
- `status` (Int) -- HTTP status code
- `headers` (ArrayRef[ArrayRef[Bytes]], default `[]`) -- Response headers
- `trailers` (Int, default `0`) -- `1` if trailers will be sent after body via `http.response.trailers`, otherwise `0`

### Response Body - `send` event

Keys:

- `type` -- `"http.response.body"`
- `body` (Bytes, default `""`) -- Response body chunk
- `file` (String) -- Absolute path to file for server to open and stream
- `fh` (Filehandle) -- Already-open filehandle for server to stream
- `offset` (Int, default `0`) -- Byte offset to start reading from (for range requests)
- `length` (Int, optional) -- Number of bytes to send (omit to read until EOF)
- `more` (Int, default `0`) -- Indicates more body content to follow (`1` if true, otherwise `0`). **Ignored for `file` and `fh` responses** which are implicitly complete.

The `body`, `file`, and `fh` keys are **mutually exclusive** - exactly one MUST be provided per event. Applications **MUST** provide `body` as encoded bytes. For text content, this typically means UTF-8 encoding before sending. The `Content-Length` h...

**Note:** When using `file` or `fh`, the response is implicitly complete after the file/handle contents are sent. The `more` key is ignored for these response types - there is no need to specify `more => 0`.

When `file` or `fh` is provided, servers MUST stream the file contents efficiently:

- Servers SHOULD stream large files in chunks to avoid memory bloat
- Servers MAY use zero-copy mechanisms (sendfile, splice) when appropriate
- The `offset` and `length` keys enable range request support (e.g., HTTP 206 Partial Content)
- For production file serving, consider using XSendfile middleware to delegate to a reverse proxy
- When using `file`, the server opens the file, streams it, and closes it
- When using `fh`, the application retains ownership and **MUST** close the handle after the `$send->()` Future completes

**Error Handling:**

- If `file` cannot be opened (not found, permission denied), the `$send->()` Future MUST fail with an appropriate exception
- If `fh` is invalid or closed, the `$send->()` Future MUST fail immediately
- Applications SHOULD validate file existence before sending `http.response.start` to avoid incomplete responses

**Validation:**

- `offset` MUST be a non-negative integer
- `length` MUST be a non-negative integer if provided
- If `offset` exceeds file size, servers SHOULD send zero bytes

**Examples:**

```perl
# Full file streaming
await $send->({
    type => 'http.response.body',
    file => '/var/www/static/large-video.mp4',
});

# Range request (bytes 1000-1999)
await $send->({
    type => 'http.response.body',
    file => '/var/www/static/document.pdf',
    offset => 1000,
    length => 1000,
});

# Streaming from already-open filehandle
open my $fh, '<:raw', '/tmp/generated-report.csv' or die $!;
await $send->({
    type => 'http.response.body',
    fh => $fh,
});
close $fh;  # Application MUST close after send Future completes
```

### Response Trailers - `send` event

Only valid when `http.response.start` was sent with `trailers => 1`. After trailers are transmitted the server MUST consider the response body complete.

Keys:

- `type` -- `"http.response.trailers"`
- `headers` (ArrayRef[ArrayRef[Bytes]], default `[]`) -- Trailer headers encoded the same way as response headers (lower-case names, byte values)

### Disconnected Client - `send` exception

If the client disconnects or cancels the connection, servers MUST send an explicit `disconnect` event to the application.

Any subsequent `$send` invocation **must** fail its returned `Future` (or throw) with a Perl exception class that indicates the disconnect (e.g., `PAGI::Error::Disconnected`). Servers MUST NOT expose Python exceptions such as `OSError`.

Applications MUST gracefully handle disconnect events by:
- Immediately halting unnecessary processing upon disconnect
- Optionally sending minimal final acknowledgment messages
- Executing asynchronous cleanup of resources as necessary.

### Disconnect - `receive` event

Sent to the application if receive is called after a response has been sent or after the HTTP connection has been closed.

Keys:

- `type` -- `"http.disconnect"`

### Connection State

The `pagi.connection` scope key provides a mechanism for applications to detect client disconnection **without consuming messages from the receive queue**. This addresses a fundamental limitation where checking for disconnect via `receive()` may inad...

#### The Problem

In PAGI's pull-based receive model, the only way to know if a client has disconnected is to consume the next message:

```perl
my $message = await $receive->();

docs/specs/www.mkdn  view on Meta::CPAN

- Setting `interval => 0` stops the keepalive timer
- Comments do not trigger client's `onmessage` handler

**Example:**
```perl
# Enable keepalive with 30s interval
await $send->({
    type     => 'sse.keepalive',
    interval => 30,
    comment  => 'ping',
});
```

### SSE Send Timeout

The `sse.send` event supports an optional `timeout` field:

- `timeout` (Number, optional) -- Send timeout in seconds. If the write does not complete within this time, the Future fails and the connection is closed.

**Example:**
```perl
await $send->({
    type    => 'sse.send',
    data    => 'hello',
    timeout => 5,
});
```

### SSE Disconnect - `receive` event

Sent to the application if the client disconnects or if the server shuts down the SSE stream after `sse.start`.

- `type` -- `"sse.disconnect"`
- `reason` (String, default empty) -- Human-readable disconnect reason

**Common reasons:**

| Reason | Meaning |
|--------|---------|
| `'client disconnect'` | Client closed connection normally |
| `'write error'` | Failed to write (keepalive or send) |
| `'send timeout'` | Send timeout exceeded |

## PAGI to PSGI Compatibility

PAGI translates keys explicitly to maintain compatibility with PSGI:

- `REQUEST_METHOD` -> `method`
- `SCRIPT_NAME` -> `root_path`
- `PATH_INFO` -> `path` minus `root_path`
- `QUERY_STRING` -> `query_string`
- `CONTENT_TYPE` -> extracted from `headers`
- `CONTENT_LENGTH` -> extracted from `headers`
- `SERVER_NAME`, `SERVER_PORT` -> `server`
- `REMOTE_ADDR`, `REMOTE_PORT` -> `client`
- `SERVER_PROTOCOL` -> `http_version`
- `psgi.url_scheme` -> `scheme`
- `psgi.version` -> `[1, 1]` (PAGI servers MUST advertise the PSGI version they emulate when bridging)
- `psgi.input` -> constructed from `http.request` events
- `psgi.errors` -> handled by the server as appropriate
- `psgi.streaming`, `psgi.nonblocking`, `psgi.multithread`, `psgi.multiprocess` -> derived from PAGI server capabilities and advertised via PSGI adapter docs

Response mappings:

- `status` and `headers` map directly to `http.response.start`
- Body content from PSGI maps directly to `http.response.body` messages.

## PAGI Encoding Differences

- `path`: Decoded UTF-8 string from percent-encoded input. The server first
  percent-decodes `raw_path`, then attempts UTF-8 decoding of the resulting
  bytes into Unicode characters. If the bytes are not valid UTF-8, the server
  **should** fall back to the original percent-decoded bytes rather than
  replacing invalid sequences or rejecting the request (Mojolicious-style
  fallback). Applications needing strict UTF-8 validation can check `raw_path`
  and decode themselves with `Encode::FB_CROAK`.
- `headers`: Represented as bytes exactly as sent/received
- `query_string`: Raw bytes from URL after `?`, percent-encoded
- `root_path`: Unicode path string matching `SCRIPT_NAME`

## Version History

- `0.2` (Draft): SSE POST method support, keepalive events, disconnect reasons,
  clarified scope fields (method required for HTTP/SSE, not for WebSocket)
- `0.1` (Draft): Initial draft based on ASGI 2.5, supporting HTTP, WebSocket, and SSE.

## Copyright

This document has been placed in the public domain.



( run in 0.734 second using v1.01-cache-2.11-cpan-140bd7fdf52 )