Unblock::HTTP2

CPAN version CPANTS Kwalitee CI Interop License Perl HTTP/2

Unblock::HTTP2 is a non-blocking HTTP/2 protocol engine for Perl.

It handles HTTP/2 framing, HPACK, streams, SETTINGS, flow control, PING, GOAWAY, trailers, CONNECT, and modern priority signaling.

It does not open sockets, perform TLS, select ALPN, or run an event loop.

application or HTTP library
        |
  Uniform::HTTP messages
        |
   Unblock::HTTP2
        |
   byte transport

The transport can be Linux::Event, IO::Async, AnyEvent, Mojolicious, a blocking socket, an in-memory test connection, or something else.

Installation

From CPAN:

cpanm Unblock::HTTP2

Unblock::HTTP2 0.12 requires Perl 5.16 or newer.

Version 0.10 intentionally broke the earlier 0.04 API: Transaction->inform() was replaced by Transaction->send_informational(). There is no compatibility alias.

The distribution uses:

Uniform::HTTP  0.06+
Alien::nghttp2 0.003+

Alien::nghttp2 supplies libnghttp2 and the build flags needed by the private XS binding.

Start here

The public API is built around three objects:

HTTP messages are normal Uniform::HTTP::Request and Uniform::HTTP::Response objects.

The application-facing exchange object is a Transaction. Its main application vocabulary is intentionally small and consistent: Client->new, Server->new, request(), respond(), write(), end(), and send_informational(). HTTP/2 protocol terms such as stream ID, RST_STREAM, stream flow control, and MAX_CONCURRENT_STREAMS keep their RFC names.

Canonical Uniform::HTTP 0.06 messages use its native C ABI directly. The XS binding inspects outgoing canonical objects without a Perl FastPath view and builds received canonical objects from validated native header spans. Uniform subclasses and framework adapters continue to use the portable message API.

An event-framework stream can attach directly to Client or Server:

$self->{http2} = Unblock::HTTP2::Server->new(
    transport  => $self,
    on_request => $on_request,
);

The framework stream implements unblock_send($bytes), unblock_finish(), and unblock_abort($reason). It feeds $http->input($bytes) on network reads and handles EOF with $http->input_eof. If the host becomes congested after accepting a buffer, it calls $http->resume_output when ready.

Output is automatic. A response produced much later from a timer also gets handed to the framework without another socket read.

The original manual transport contract remains available when no host is attached:

$engine->input($bytes_from_transport);

while ($engine->want_write) {
    my $bytes = $engine->output;
    last unless length $bytes;
    $transport->write($bytes);
}

XS-backed transports can optionally use Unblock::HTTP2::NativeABI. It accepts borrowed native input buffers directly and can drain generated nghttp2 output through a native sink callback. The transport keeps ownership of input storage, and outbound buffers are borrowed only for the duration of the sink callback. Client and Server use the same ABI.

The portable input() method is available in both modes. With an attached host, output delivery is automatic. Without one, output() remains the low-level portable output path. Native integrations can discover the installed ABI with definition(), native_include_dir(), header_path(), and c_header().

Read perldoc Unblock::HTTP2::Integration, docs/INTEGRATION.md, and docs/COOKBOOK.md for the framework integration contract and runnable IO::Async, AnyEvent, and Linux::Event examples.

Unblock::HTTP2 never waits for network activity itself.

Client

use Unblock::HTTP2::Client;

my $client = Unblock::HTTP2::Client->new;

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. Each is carried by an HTTP/2 stream.

Server

use Unblock::HTTP2::Server;

my $server = Unblock::HTTP2::Server->new(
    on_request => sub {
        my ($transaction, $request) = @_;
        $transaction->respond(status => 200, body => "hello\n");
    },
);

Request body chunks arrive through on_body. on_request_end runs when the complete request, including trailers, has arrived.

Receiving request and response bodies

Incoming bodies stream by default. To accumulate a complete body on the canonical Uniform message, choose buffered reception for a connection:

my $client = Unblock::HTTP2::Client->new(
    receive_body => 'buffered',
    max_buffered_body_bytes => 64 * 1024 * 1024,
);

The default buffered limit is 64 MiB. An oversized body fails only the affected HTTP/2 stream, not other multiplexed transactions. A complete buffered body is available from the message before its completion callback.

For a particular request, on_data selects streamed incoming-body delivery, even on a connection configured for buffering:

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

The server can also register $transaction->on_data(sub { ... }) from on_request, or use on_request_end for a completed buffered request. Do not combine on_data with the older on_body callback.

on_finish runs exactly once after completion, error, or cancellation, with the transaction's final state available. See perldoc Unblock::HTTP2::Transaction for registration and callback order.

Streaming bodies

Buffered bodies can live directly on the Uniform message object.

For a streaming local body (using an existing request object, or using the named-field request() form above):

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

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

The same write() and end() API is used for a streaming server response.

write() always accepts the bytes. A false return means the transaction reached its cooperative high-water mark or the attached framework host is congested. Pause production until on_drain runs.

Incoming body bytes are automatically credited back to the peer after the body callback returns. A slow consumer can take manual flow-control ownership with:

$transaction->auto_consume(0);
$transaction->consume($bytes_processed);

Trailers and informational responses

Request and response trailers are supported through the Uniform trailer fields. Unblock sends them as HTTP/2 trailing HEADERS.

A server can send an informational response before the final response:

$transaction->send_informational(status => 103);
$transaction->respond(status => 200, body => "ready\n");

CONNECT

Ordinary CONNECT and generic Extended CONNECT are supported.

An Extended CONNECT request uses the Uniform protocol field:

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

Unblock maps this to HTTP/2 :protocol. It does not implement the tunneled protocol itself.

Connection controls

The engine exposes HTTP/2 protocol controls without exposing the private libnghttp2 session.

Examples:

$engine->ping("12345678");

$engine->update_settings(
    initial_window_size => 131_072,
);

$engine->drain;

$engine->goaway(
    error_code => Unblock::HTTP2::NO_ERROR(),
);

Received GOAWAY details are available through peer_goaway().

A Transaction can be cancelled or reset explicitly:

$transaction->cancel;

$transaction->reset(
    Unblock::HTTP2::REFUSED_STREAM(),
);

Reset error codes and whether the reset came from the peer are preserved on the Transaction.

RFC 9218 extensible priorities are supported. The old RFC 7540 dependency-tree priority model is intentionally not part of the public API.

What Unblock::HTTP2 owns

Unblock::HTTP2 owns:

What it does not own

Unblock::HTTP2 does not own:

Those responsibilities belong to the transport, application, or a higher HTTP client/server layer.

Server Push is intentionally not exposed. Clients advertise SETTINGS_ENABLE_PUSH = 0.

Testing

The normal suite runs complete client/server exchanges in memory.

CI covers:

A separate interoperability workflow tests both directions against the stock nghttp2 tools:

The interoperability and performance harnesses live under xt/ and are not included in the CPAN distribution.

More documentation

Status

Unblock::HTTP2 0.12 is feature-complete for its intended role as a reusable, event-loop-neutral HTTP/2 engine.

Future work can focus on bug fixes, interoperability, performance, or optional extensions without changing the transport boundary.

License

MIT.