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.01 requires Perl 5.16 or newer.

The distribution uses:

Uniform::HTTP  0.04+
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 basic transport contract is byte-in, byte-out:

$engine->input($bytes_from_transport);

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

Unblock::HTTP2 never waits for network activity itself.

Client

use Uniform::HTTP::Request;
use Unblock::HTTP2::Client;

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

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

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

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

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

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

Server

use Uniform::HTTP::Response;
use Unblock::HTTP2::Server;

my $server = Unblock::HTTP2::Server->new(
    on_request => sub {
        my ($stream, $request) = @_;

        $stream->respond(
            Uniform::HTTP::Response->new(
                status => 200,
                body   => "hello\n",
            ),
        );
    },
);

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

Streaming bodies

Buffered bodies can live directly on the Uniform message object.

For a streaming local body:

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

$stream->write($chunk);
$stream->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 stream reached its cooperative high-water mark. 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:

$stream->auto_consume(0);
$stream->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:

$stream->inform(
    Uniform::HTTP::Response->new(
        status => 103,
    ),
);

$stream->respond($final_response);

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 Stream can be cancelled or reset explicitly:

$stream->cancel;

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

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

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 TCP interoperability harness lives under xt/ and is not included in the CPAN distribution.

More documentation

Status

Unblock::HTTP2 0.01 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.