Unblock::HTTP
Unblock::HTTP is the framework-neutral application layer above HTTP/1, HTTP/2, HTTP/3, and WebSocket. Protocol parsing stays in the existing engines. WebSocket sessions use the separate Unblock::WebSocket distribution; this top layer adds no XS.
Unblock::HTTP 0.02 provides reusable Server attachment, a shared Client with per-origin pools and bounded queues, and opt-in WebSocket handshakes over HTTP/1.1 Upgrade and HTTP/2/3 Extended CONNECT. Unblock::WebSocket is an optional dependency, loaded only when WebSockets are used. Cross-host Alt-Svc and post-dispatch request replay are intentionally not provided; optional framework-driven request deadlines are available. The required released engines are Unblock::HTTP1 0.12+, Unblock::HTTP2 0.12+, Unblock::HTTP3 0.13+, and Uniform::HTTP 0.06+. The separate implementation notes record development history.
One Server, many connections
Application code creates one definition:
use Unblock::HTTP::Server;
my $server = Unblock::HTTP::Server->new(
http1 => {},
http2 => {},
http3 => {},
on_request => sub {
my ($tx, $request) = @_;
$tx->respond(status => 200, body => "hello\n");
},
);
An omitted protocol is disabled. Each protocol hash can carry engine tuning
options. WebSocket remains disabled by default; enable it per protocol
using websocket => 1 and supply on_websocket($tx, $request).
HTTP/2 and HTTP/3 require a framework defer($callback) that schedules
work on a later event-loop turn. The Server advertises Extended CONNECT
only for explicitly enabled protocols. WebSocket acceptance affects
one multiplexed stream on H2/H3, but transfers a whole HTTP1 socket.
on_headers($tx, $request) runs first. By default the engine buffers up to
1 MiB, then calls on_request($tx, $request) with the complete frozen request.
Set max_buffered_body_bytes in a protocol hash to change that positive limit.
For streaming, register $tx->on_data(sub { my ($tx, $bytes) = @_; ... })
during on_headers. This suppresses full-body accumulation. on_request
still runs after successful body completion. Failed or cancelled bodies do
not reach it. Register $tx->on_finish(...) before the transaction is terminal
to observe completion, error, or cancellation exactly once.
This requires Unblock::HTTP1 0.12+, Unblock::HTTP2 0.12+, and Unblock::HTTP3 0.13+. Attachment also checks required engine methods and gives an explicit error for an incompatible older engine.
Framework attachment
For each incoming TCP/TLS connection, the adapter attaches its stream:
my $engine = $server->attach_stream(
transport => $stream,
alpn => $negotiated_alpn,
);
$engine->input($bytes);
The returned object is the actual HTTP1 or HTTP2 Server engine. Transactions also come directly from that engine. There is no forwarding connection object. The framework retains the engine and continues to own its socket and buffers.
The stream supplies unblock_send, unblock_finish, and unblock_abort.
unblock_send accepts the entire buffer or throws. A defined false result
means the bytes were accepted but the host is congested. The framework calls
resume_output() when it has capacity again.
Configure TLS using $server->alpn_protocols. This returns enabled TCP choices,
with H2 preferred. Empty ALPN selects H1 only when H1 is enabled. Unknown or
disabled protocols fail. For cleartext H2 prior knowledge, pass
protocol => 'h2' instead of alpn.
A ready HTTP/3 QUIC connection uses:
my $engine = $server->attach_quic(connection => $ready_h3_connection);
It returns a started HTTP3 Server engine. UDP and timer events continue to go
to Net::QUIC::Driver. The framework is responsible for supplying a connection
negotiated for H3. A QUIC manager can attach all ready connections to the same
definition:
my $endpoint = Unblock::HTTP::QUIC->server(
server => $server,
certificate_file => $cert,
private_key_file => $key,
send => $send_datagram,
set_timeout => $replace_timer,
);
$endpoint->start;
Read perldoc Unblock::HTTP::Integration for the adapter contract.
Shared Client
The Client now owns reusable HTTP policy, request scheduling, and per-origin connection pools. An application makes one Client for many destinations:
my $op = $client->request(
method => 'GET',
url => 'https://example.com/data',
on_response => sub {
my ($tx, $response) = @_;
print $response->status, "\\n";
},
);
The request returns an operation handle right away. Its transaction() is
undefined while waiting for a connection, then becomes the real engine
Transaction after dispatch. cancel(), state(), and error() work
without polling. A strict http_version => '3' requires HTTP/3, not a
fallback to HTTP/2. HTTPS cold requests use H2/H1 ALPN; later requests may use H3
through an authenticated, fresh, same-host Alt-Svc advertisement.
An integration supplies connect_stream, connect_quic and a defer
callback. The factory opens TCP/TLS or QUIC through its framework and calls
$ready->(...). The ready callback returns the real connected engine.
The adapter keeps that engine on its framework-owned stream or connection and
feeds input directly to it. The defer callback schedules work in a later
event-loop turn. See perldoc Unblock::HTTP::Client and
perldoc Unblock::HTTP::Integration for the exact contracts.
By default the Client buffers up to 1 MiB of response data in the chosen
engine, then calls on_response once after body completion. Supplying
on_data automatically streams the response: on_response fires when
headers arrive, then on_data($tx, $chunk), on_end($tx), and terminal
on_finish($tx). Client buffering does not add another body accumulator.
A request may set timeout => $seconds to cover queuing, establishment,
and its active HTTP transaction. The framework adapter supplies an optional
schedule_timer($seconds, $callback) constructor callback returning a timer
cancellation coderef. A timed-out active request cancels its real engine
transaction and reports a timeout on the operation. A factory can return
a cancellation coderef so a timed-out or manually cancelled connection
attempt releases its pool slot; late factory readiness is ignored.
When HTTP/3 QUIC bidi stream credit is exhausted, an unsubmitted request stays in the bounded Client queue; the H3 engine notifies when request stream availability changes, and the Client retries admission only after framework deferral. No submitted request is replayed.
The framework may call $client->network_changed when its network
identity changes. This discards idle old-network sockets and Alt-Svc
hints, while active transactions continue on their original engines
until completion. Pending operations are retried only BEFORE dispatch
using fresh connections; a transmitted request is never replayed.
$client->pool_stats returns read-only origin and pool counts to
inspect queued, connecting, active, idle, and retiring connections.
Fatal hard-limit output rejection is distinct from cooperative backpressure. The repository-only Linux::Event integration tests propagate max_pending_bytes violations as transport errors; it does not replay the rejected bytes. Unblock::HTTP1 0.12+ waits until attached output is accepted before reporting a successful server terminal callback. See docs/IMPLEMENTATION.md.
Idle connection eviction is opt-in with idle_timeout => $seconds.
When enabled, the framework supplies schedule_timer and may supply
release_connection($host, $engine, $protocol, $reason) to close its own
socket or QUIC resources. Timers are cancelled on reuse and never close
connections with active streams or queued requests waiting for peer credit.
The repository-only live integration tests also exercise streamed request and
response bodies exceeding 1 MiB on H1/H2/H3, with integrity checks and
controlled accepted-but-blocked output resumption. A preliminary
direct-engine versus unified-Client TCP loopback benchmark is included
at bench/client-vs-direct.pl. HTTP/3 additionally has
bench/h3-client-vs-direct.pl for serial requests and
bench/h3-workload-profile.pl for isolated concurrent CPU, latency
and sampled RSS workloads. Read docs/BENCHMARKING.md and
docs/PERFORMANCE.md for how to run them and interpret their
limitations. CI correctness tests do not enforce throughput rates.
The queue defaults to 128 pending requests, with at most four connections per origin and 32 concurrent streams per H2/H3 connection. An H1 connection runs one request at a time. The limits are adjustable. Pool idle expiry and WebSocket upgrades/Extended CONNECT are implemented; validated DNS HTTPS discovery and convenience framework connector classes remain future work. Authenticated same-host Alt-Svc now permits future requests to select HTTP/3. Advertisements are only learned when the TCP factory explicitly reports verified_origin => 1 after certificate and origin hostname verification. When advertised HTTP/3 is selected, the QUIC factory receives alternative_host and alternative_port and must confirm verified_origin => 1 before the request can be sent. A failed advertised QUIC connection may safely fall back before dispatch; a request already submitted to an engine is never automatically replayed.
WebSockets across HTTP/1, HTTP/2 and HTTP/3
For each enabled protocol, Server on_websocket returns the WebSocket
session options/callbacks, or undef to reject the request with HTTP 403.
The handshake is validated before creating an established
Unblock::WebSocket::Server. H1 uses HTTP 101 and a socket input-consumer
switch; H2/H3 use HTTP 200 and independent streaming CONNECT
transactions on their existing multiplexed connections.
my $server = Unblock::HTTP::Server->new(
http2 => { websocket => 1 },
http3 => { websocket => 1 },
defer => sub { $framework->defer($_[0]) },
on_request => sub { $_[0]->respond(status => 200, body => 'HTTP') },
on_websocket => sub {
return {
on_message => sub {
my ($ws, $message, $type) = @_;
$ws->send_text("echo:$message") if $type eq 'text';
},
};
},
);
A unified Client selects its WebSocket protocol explicitly with
http_version => '1.1', '2', or '3', or defaults to the
first enabled WebSocket protocol in that order:
my $operation = $client->connect_websocket(
url => 'wss://example.com/chat',
http_version => '3',
on_open => sub {
my ($ws, $tx) = @_;
$ws->send_text('hello');
},
);
The operation's websocket() accessor holds the actual WebSocket
Client once opened. H2/H3 WebSockets remain in the Client's pool until
their individual streams finish, allowing ordinary HTTP to share the
connection. HTTP3 Client WebSockets wait for the peer's current
Extended CONNECT SETTINGS before dispatch; if QUIC bidi-stream
capacity is unavailable, the original unsent operation remains
queued until credit returns. A denied/invalid handshake fails only
the relevant operation. Consult the WebSocket cookbook,
Server and Client POD for running all three protocols, the exact
transport ownership rules, backpressure, Close and callback contracts.
Examples and tests
The optional Linux::Event examples are:
examples/linux-event-server.pl: a shared Server with H1/H2 attachment.examples/linux-event-quic-server.pl: a shared Server with a QUIC manager.examples/linux-event-websocket-server.pl: H1 Upgrade or H2 Extended CONNECT WebSocket echo.examples/linux-event-websocket-client.pl: matching H1/H2 shared Client transport factory and WebSocket echo.examples/linux-event-quic-websocket-server.pl: H3 WebSocket echo on QUIC/TLS.
Each separates application behavior from framework plumbing. The core does not require Linux::Event. The distribution requires Perl 5.20; some optional example programs use newer Perl syntax. No framework adapter is installed by this distribution.
Run isolated contract tests:
prove -lr t
Run the live tests after installing the real engines, Linux::Event and Net::QUIC:
prove -l xt/linux-event-tcp.t xt/linux-event-websocket.t xt/linux-event-quic.t
The distribution suite combines isolated contract tests with real H1/H2 WebSocket exchanges, a borrowed native H1 input handoff regression, and full localhost QUIC/TLS H3 WebSocket Server/Client tests. The optional live integration suite additionally exercises real sockets, streamed bodies, QUIC flow credit and resource bounds. CI does not enforce performance or throughput thresholds.
License
MIT License.