Unblock::HTTP3

CPAN version CPANTS Kwalitee CI HTTP/3 interop License Perl nghttp3 HTTP/3

Unblock::HTTP3 is a non-blocking HTTP/3 protocol engine for Perl.

HTTP/3 is HTTP carried over QUIC. Unblock::HTTP3 handles the HTTP/3 layer while Net::QUIC handles QUIC and TLS.

application or HTTP library
        |
  Uniform::HTTP messages
        |
   Unblock::HTTP3
        |
      Net::QUIC
        |
        UDP

Unblock::HTTP3 does not own a UDP socket, timer, TLS configuration, or event loop. Those stay below Net::QUIC, so the HTTP/3 engine can be used with different operating systems and event loops.

Uniform::HTTP supplies the request and response objects. Alien::nghttp3 supplies libnghttp3 for HTTP/3 framing and QPACK.

Installation

From CPAN:

cpanm Unblock::HTTP3

Unblock::HTTP3 0.02 requires:

Perl            5.20+
Alien::nghttp3  0.01+
Net::QUIC       0.04+
Uniform::HTTP   0.05+

Start here

Most code works with three things:

Unblock::HTTP3 uses the canonical Uniform message classes directly. HTTP/3 priority, reset, and STOP_SENDING state live on the Transaction rather than on the message object.

An HTTP/3 Connection wraps an existing Net::QUIC::Connection:

use Unblock::HTTP3::Connection;

my $h3 = Unblock::HTTP3::Connection->client(
    quic => $quic,
);

$h3->start;

Your event-loop adapter continues to drive Net::QUIC. Unblock::HTTP3 never blocks waiting for network activity.

Sending a request

A client can submit a normal Uniform::HTTP::Request:

use Uniform::HTTP::Request;

my $request = Uniform::HTTP::Request->new(
    method    => 'GET',
    target    => '/',
    scheme    => 'https',
    authority => 'example.com',
);

my $tx = $h3->request($request);

When the final response headers arrive, the Transaction becomes available from the Connection:

while (my $ready = $h3->next_transaction) {
    my $response = $ready->response;

    print $response->status, "\n";
    print $response->body if $response->has_buffered_body;
}

Many Transactions can be active at once. Each Transaction keeps its own request and response paired even when responses arrive out of order.

Receiving a request

A server receives new requests as Transactions:

while (my $tx = $h3->next_transaction) {
    my $request  = $tx->request;
    my $response = $tx->response;

    $response->status(200);
    $response->header('Content-Type', 'text/plain');
    $response->body("hello\n");

    $tx->send_response;
}

The server-side Response is mutable until it is sent.

Bodies

Buffered bodies are the default.

A buffered request or response body lives on the Uniform message object:

my $body = $response->body;

For large or incremental bodies, use streaming instead.

A server can stream a response:

my $body = $tx->response_body(
    on_drain  => sub { ... },
    on_cancel => sub { ... },
);

$body->write($chunk);
$body->complete;

write() returns false when the bytes were accepted but the producer should pause until on_drain runs.

A client can receive a response without buffering the complete body:

my $tx = $h3->request(
    $request,
    receive_body => {
        on_data => sub {
            my ($reader, $chunk) = @_;
            process($chunk);
        },
        on_end => sub {
            my ($reader) = @_;
            ...
        },
    },
);

Streaming receive credit is returned as the application consumes data.

Trailers and informational responses

Request and response trailers are supported.

Uniform::HTTP keeps trailers separate from normal headers and preserves field order and duplicates.

Servers can also send 1xx informational responses before the final response:

$tx->send_informational(
    Uniform::HTTP::Response->new(
        status => 103,
    ),
);

HTTP/3 does not use status 101.

CONNECT

Basic CONNECT tunnels are supported.

Generic Extended CONNECT is also supported. A server enables it with:

my $h3 = Unblock::HTTP3::Connection->server(
    quic                    => $quic,
    enable_extended_connect => 1,
);

An Extended CONNECT request uses the Uniform protocol field.

Unblock::HTTP3 does not assign meaning to protocol names. Higher-level modules decide what protocols such as WebTransport, WebSocket, or MASQUE mean.

Extended CONNECT Transactions can also use the generic RFC 9297 Capsule Protocol through:

my $capsules = $tx->capsules;

HTTP Datagrams

RFC 9297 HTTP Datagrams are supported over Net::QUIC's QUIC DATAGRAM support.

Enable them on the HTTP/3 connection:

my $h3 = Unblock::HTTP3::Connection->client(
    quic                  => $quic,
    enable_http_datagrams => 1,
);

A client marks a request as using HTTP Datagrams when it creates the Transaction:

my $tx = $h3->request(
    $request,
    datagrams => 1,
);

$tx->send_datagram($bytes);

The Transaction also provides next_datagram, on_datagram, and max_datagram_payload_size.

The higher-level protocol still decides what the Datagram payload means.

ORIGIN

Servers can advertise the RFC 9412 Origin Set extension:

my $h3 = Unblock::HTTP3::Connection->server(
    quic => $quic,
    origins => [
        'https://example.com',
        'https://www.example.com',
    ],
);

A client can read the advertised entries with:

my $origins = $h3->peer_origins;

Before a complete ORIGIN frame arrives this returns undef. An explicit empty ORIGIN frame returns an empty array reference. Invalid received origin entries are ignored.

Request priority

RFC 9218 priority can be supplied through the normal Uniform Priority header:

my $request = Uniform::HTTP::Request->new(
    method    => 'GET',
    target    => '/',
    scheme    => 'https',
    authority => 'example.com',
    headers   => [
        [ Priority => 'u=1, i' ],
    ],
);

It can be inspected or changed on a live Transaction:

$tx->priority(
    urgency     => 0,
    incremental => 0,
);

Urgency is from 0 through 7, where 0 is most urgent.

0-RTT

Net::QUIC owns QUIC/TLS early-data state. Unblock::HTTP3 owns the remembered HTTP/3 SETTINGS needed to decide what can safely be sent before the new server SETTINGS frame arrives.

Save both values from the same successful session:

my $quic_state = $quic->early_data_state;
my $h3_state   = $h3->peer_settings_state;

On a resumed connection, give each value back to the layer that created it.

An HTTP/3 request sent before the handshake finishes must opt in explicitly:

my $tx = $h3->request(
    $request,
    early_data => 1,
);

0-RTT can be replayed. Unblock::HTTP3 does not automatically retry an early request if QUIC rejects it.

On the server, early request bytes can be parsed before the handshake finishes, but the Transaction is not exposed to application code until the QUIC handshake completes and the early data has not been rejected. Early HTTP Datagrams are bounded and held with the Transaction until that point. This is the safe default required by the HTTP early-data replay rules.

See Unblock::HTTP3::Connection and docs/ARCHITECTURE.md for the complete SETTINGS persistence rules.

Correctness and limits

Unblock::HTTP3 validates HTTP/3 message rules before sending and while receiving.

This includes:

It also provides configurable limits for buffered bodies, streaming receive queues, field sections, QPACK, and HTTP Datagram queues.

Protocol errors are kept at the narrowest correct scope when possible. A bad request stream does not automatically destroy unrelated multiplexed requests.

The normative coverage audit and native-library boundaries are documented in docs/RFC-COMPLIANCE.md.

Extensions

The engine provides generic extension hooks without assigning application semantics to them:

This is the intended foundation for higher-level HTTP/3 protocols.

What Unblock::HTTP3 does not own

Unblock::HTTP3 does not own:

Those responsibilities stay in Net::QUIC, the event-loop adapter, or the application.

HTTP/3 Server Push is not exposed because the libnghttp3 version used by this release does not implement it.

Testing

The normal test suite uses real kernel UDP sockets, TLS, QUIC, and HTTP/3.

CI tests released CPAN dependencies on Perl 5.20, 5.28, 5.36, and 5.44 and also validates the built distribution.

Interoperability CI covers both directions: the Unblock::HTTP3 client talks to independent public HTTP/3 servers, and a pinned quic-go client drives an Unblock::HTTP3 server over real loopback UDP/TLS/QUIC/HTTP/3. These tests stay outside normal CPAN installation tests.

More documentation

License

MIT.