Unblock::HTTP3

CPAN version CPANTS Kwalitee CI HTTP/3 interop License Perl

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

It handles HTTP/3 framing, QPACK, request streams, trailers, informational responses, Extended CONNECT, HTTP Datagrams, Capsules, priorities, graceful shutdown, and replay-aware 0-RTT state.

It does not own UDP sockets, TLS, timers, or an event loop. Net::QUIC owns the QUIC transport.

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

Installation

From CPAN:

cpanm Unblock::HTTP3

Unblock::HTTP3 0.13 requires:

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

Version 0.13 adds bounded buffered reception and per-transaction on_data / on_finish lifecycle callbacks for the unified Unblock::HTTP layer while preserving standalone Client and Server use. It fixes retention of completed callback-delivered Transactions by keeping callback notifications weak, compacting expired weak slots without changing their ownership, and retaining strong references for pull-only notifications. Net::QUIC 0.05 remains the minimum dependency for credit-limited QUIC output and targeted shared-capacity wakeups. The native consumer ABI remains version 1.

Version 0.12 fixed NativeABI version metadata for PAUSE indexing.

Version 0.11 adds the same named-field Client request, Transaction response, and informational response forms as the HTTP1 and HTTP2 engines. Existing Uniform::HTTP objects remain fully supported.

Version 0.10 intentionally broke the earlier 0.03 Perl API. Client and Server now use new(), final responses use respond(), and the old Connection->client(), Connection->server(), and send_response() entry points are removed. There are no compatibility aliases.

Start here

The public API is built around three objects:

The common application vocabulary intentionally matches the other Unblock HTTP engines:

Client->new
Server->new
request()
respond()
write()
end()
send_informational()

HTTP/3 protocol concepts keep their HTTP/3 names.

Exact canonical Uniform::HTTP::Request and Uniform::HTTP::Response objects use the Uniform::HTTP native fast path. Uniform-compatible adapters and subclasses use the portable Perl message contract.

Client

use Unblock::HTTP3::Client;

my $client = Unblock::HTTP3::Client->new(
    quic => $quic,
);

$client->start;

my $transaction = $client->request(
    method    => 'GET',
    target    => '/',
    scheme    => 'https',
    authority => 'example.com',

    on_response => sub {
        my ($transaction, $response) = @_;
        print $response->status, "\n";
    },

    on_body => sub {
        my ($transaction, $response, $bytes) = @_;
        process_bytes($bytes);
    },

    on_complete => sub {
        my ($transaction) = @_;
        print "done\n";
    },
);

Many Transactions can be active on one Client at the same time.

The existing pull interface remains available through next_transaction() and next_informational() for integrations that prefer polling. Pull-only notifications retain a strong Transaction reference until retrieved. When callbacks deliver a Transaction, its optional pull-queue notification is weak: it remains accessible while the application or active engine holds the Transaction, but it does not retain completed response bodies indefinitely. Callback handlers should retain the Transaction themselves if they need it after completion. The Server follows the same rule for on_request.

Server

use Unblock::HTTP3::Server;

my $server = Unblock::HTTP3::Server->new(
    quic => $quic,

    on_request => sub {
        my ($transaction, $request) = @_;

        $transaction->respond(
            status => 200,
            body   => "hello\n",
        );
    },
);

$server->start;

on_body receives request body chunks. on_request_end runs after the request body and trailers have completed.

Event-loop integration

Use Net::QUIC::Driver to connect an event loop's UDP socket and timer to QUIC. Unblock::HTTP3 attaches to each ready Net::QUIC::Connection; it does not replace the Driver or accept raw TCP-style input bytes.

See the installed integration guide and the application API guide. Complete Linux::Event programs are in server and client. The transport examples require Linux::Event 0.117 or later, but it is not a distribution dependency.

Incoming body reception

Incoming request and response bodies are buffered by default, with a finite 64 MiB per-transaction limit. Configure max_buffered_body_bytes on the Client or Server to reduce this limit. The complete body is installed on the same Uniform message object before the body-completion callback; an empty completed body is the empty string. Header callbacks run before body arrival.

Register on_data at request creation or during the incoming-header callback to receive chunks without retaining a second full body. For example:

my $tx = $client->request(
    method    => 'GET',
    target    => '/',
    scheme    => 'https',
    authority => 'example.com',
    on_data   => sub {
        my ($tx, $bytes) = @_;
        process_bytes($bytes);
    },
    on_finish => sub {
        my ($tx) = @_;
        report_terminal_state($tx->state, $tx->error);
    },
);

On a Server, call $tx->on_data(sub { ... }) inside on_request to select streaming for that incoming request. Do not combine on_data with the legacy on_body callback. Registration after body delivery has started is rejected. on_finish runs once after the transaction reaches complete, cancelled, or error state; it is not merely an incoming-body completion event. A receive limit failure resets the affected HTTP/3 stream without failing unrelated transactions. Consult the Client, Server and Transaction POD for details.

Streaming bodies

Buffered bodies can live directly on the Uniform message object.

For a streaming client request:

my $transaction = $client->request(
    $request,
    stream_body => 1,
    on_drain => sub {
        my ($transaction) = @_;
        produce_more($transaction);
    },
);

$transaction->write($chunk);
$transaction->end($last_chunk);

A streaming server response uses the same Transaction API:

$transaction->respond(
    $response,
    stream_body => 1,
    on_drain => sub {
        my ($transaction) = @_;
        produce_more($transaction);
    },
);

$transaction->write($chunk);
$transaction->end;

For advanced HTTP/3 body control, Body::Stream and Body::Reader remain available. They expose explicit receive credit, cancellation, and body-specific callbacks without changing the common Transaction API.

Informational responses

A server can send a 1xx response before the final response:

$transaction->send_informational(status => 103);

$transaction->respond($final_response);

HTTP/3 does not use status 101.

Transaction state

The common lifecycle methods are:

state
error
is_complete
is_cancelled
is_error
is_terminal

HTTP/3-specific reset and STOP_SENDING details remain available separately on the Transaction.

CONNECT, Capsules, and Datagrams

Ordinary CONNECT and generic Extended CONNECT are supported.

Extended CONNECT uses the Uniform protocol field. Higher-level modules decide what a protocol such as WebSocket, WebTransport, or MASQUE means.

An Extended CONNECT Transaction can use the RFC 9297 Capsule Protocol through:

my $capsules = $transaction->capsules;

RFC 9297 HTTP Datagrams use Net::QUIC DATAGRAM support. Enable them on the Client or Server with:

enable_http_datagrams => 1

A client marks a request as using Datagrams with:

my $transaction = $client->request(
    $request,
    datagrams => 1,
);

The Transaction provides send_datagram(), next_datagram(), on_datagram(), and max_datagram_payload_size().

Priority and ORIGIN

RFC 9218 priority state is available through Transaction->priority().

Servers can advertise RFC 9412 origins with the origins constructor option. Clients read the received set with peer_origins().

0-RTT

Net::QUIC owns QUIC/TLS early-data state. Unblock::HTTP3 owns the remembered HTTP/3 SETTINGS needed to validate early requests.

Save the QUIC and HTTP/3 state from the same successful session:

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

An early client request must opt in explicitly:

my $transaction = $client->request(
    $request,
    early_data => 1,
);

0-RTT can be replayed. Unblock::HTTP3 does not automatically retry a rejected early request.

Native integrations

XS event frameworks and HTTP libraries can use Unblock::HTTP3::NativeABI.

Native integrations can discover the installed ABI with:

definition
native_include_dir
header_path
c_header

ABI version 1 accepts exact Unblock::HTTP3::Client, Unblock::HTTP3::Server, and Unblock::HTTP3::Transaction objects. Adapters and subclasses use the portable Perl API.

The native ABI does not expose libnghttp3 internals and does not take ownership of the QUIC transport.

See docs/NATIVE-ABI.md.

What Unblock::HTTP3 does not own

Unblock::HTTP3 does not own:

Those belong to Net::QUIC, the event-loop adapter, the application, or a higher-level protocol module.

Testing

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

CI tests released dependencies on several Perl versions and validates the built distribution. Separate interoperability tests cover both client and server directions against independent HTTP/3 implementations.

More documentation

License

MIT.