NAME

Unblock::HTTP::Server - Shared HTTP server policy and connection attachment

SYNOPSIS

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

# For each accepted connection, in the framework adapter:
my $engine = $server->attach_stream(
    transport => $stream,
    alpn => $negotiated_alpn,
);
$engine->input($bytes);

IMPLEMENTATION STATUS

This feature branch implements reusable attachment and ordinary buffered or streamed requests. It requires the receive/lifecycle feature branches of the three engines; released H1/H2 0.11 and H3 0.12 do not provide these methods. HTTP/1.1 Server Upgrade and HTTP/2 Server Extended CONNECT are verified opt-in paths using Unblock::WebSocket. HTTP/2 Client Extended CONNECT is also verified and documented in Unblock::HTTP::Client. HTTP/3 Server Extended CONNECT is verified by real QUIC/TLS loopback. HTTP/3 Client Extended CONNECT is also implemented and verified over real QUIC/TLS, including peer SETTINGS, stream-credit admission and cancellation. Initial Client factories and bounded pooling are implemented there. See docs/IMPLEMENTATION.md. This is not a release candidate.

CONSTRUCTOR

new

Enable each protocol with http1 => {}, http2 => {}, or http3 => {}. An omitted protocol is disabled. At least one is required. Each hash can contain engine tuning options. Policy copies hash and array containers so changing a caller's settings does not change future connections. Engine-specific object values retain their identity.

on_request is required. on_headers and on_error are optional. Connection and callback options may not be hidden inside a protocol's tuning hash.

Each protocol defaults to bounded buffered reception with a 1 MiB limit. Set max_buffered_body_bytes in its protocol hash to change the positive byte limit. receive_body is managed by this layer; select streaming with $tx->on_data(...) during on_headers. The engine owns the buffer.

Request callbacks

on_headers($tx, $request) runs after headers, before body delivery. The same real engine transaction and Uniform request are passed to on_request($tx, $request) once the body and trailers finish successfully. In buffered mode, $request->body holds the complete body, including an empty string for an empty body. The message is frozen before on_request. A failed or cancelled body does not call on_request.

For streamed requests:

on_headers => sub {
    my ($tx) = @_;
    $tx->on_data(sub {
        my ($tx, $bytes) = @_;
        consume_chunk($bytes);
    });
    $tx->on_finish(sub {
        my ($tx) = @_;
        record_outcome($tx->state);
    });
},
on_request => sub {
    my ($tx) = @_;
    $tx->respond(status => 204);
},

on_data selects streaming and suppresses full-body accumulation. It must be registered before the first body byte. on_request still runs after successful streamed reception. on_finish runs once after completion, cancellation, or error; it waits for the response side too. Register it before the transaction is terminal. Its exceptions do not undo cleanup. on_error retains the engine's arguments: transaction, error, and an optional protocol error code. Cancellation alone is not an error.

websocket => 1 enables server-side HTTP/1.1 WebSocket Upgrade, HTTP/2 Extended CONNECT, and HTTP/3 Extended CONNECT server path, only when on_websocket is supplied. All default to disabled. HTTP/2 and HTTP/3 WebSockets require defer, which schedules execution on a later event-loop turn. The relevant Extended CONNECT setting is advertised only when that protocol's WebSocket policy is enabled. HTTP/3 Client WebSocket is also supported. For H2/H3 WebSocket CONNECT transactions, on_websocket replaces ordinary on_headers/on_request delivery; other HTTP requests are unchanged.

HTTP/1.1 WebSocket Upgrade

The optional Unblock::WebSocket distribution is loaded only for WebSocket handshakes. The existing HTTP1 engine handles the opening HTTP request and writes the 101 response. An established WebSocket uses the same framework transport and write queue. No HTTP transaction or byte-stream wrapper is introduced.

my $server = Unblock::HTTP::Server->new(
    http1 => { websocket => 1 },
    on_request => sub {
        my ($tx) = @_;
        $tx->respond(status => 200, body => 'ordinary HTTP');
    },
    on_websocket => sub {
        my ($tx, $request) = @_;
        return {
            on_message => sub {
                my ($ws, $message, $type) = @_;
                $ws->send_text("echo:$message") if $type eq 'text';
            },
            on_open => sub {
                my ($ws, $tx, $request) = @_;
                # Invoked after the 101 response entered the host queue.
            },
        };
    },
);

When an HTTP/1.1 request includes WebSocket handshake indicators, on_websocket($tx, $request) selects connection options. It must return a hash reference of Unblock::WebSocket::Server constructor callbacks/options, or undef to reject the upgrade with HTTP 403. subprotocols and permessage_deflate are passed through the WebSocket handshake negotiator and its negotiated options are applied to the established session. Invalid handshakes receive HTTP 400, without switching the connection. Incorrect application-provided options are application errors.

An attached HTTP1 transport with this policy must implement the usual unblock_send, unblock_finish and unblock_abort methods, plus:

sub unblock_upgrade {
    my ($transport, $websocket) = @_;
    $transport->{consumer} = $websocket;
}

The hook switches subsequent input, EOF, errors and output-resume calls to the provided WebSocket engine; it must be synchronous. It runs once, only after the HTTP1 engine has accepted the complete 101 response into the host output queue. Unblock::HTTP transfers take_remainder() from the HTTP1 engine into the new WebSocket engine after the hook (and after on_open), preserving buffered frames received in the same input call as the HTTP request. The host must keep its existing ordered output queue and must not send the same remainder again. HTTP1 retains its own transaction identity.

This first slice is regression-tested with the public input() path. For native borrowed-input integration, the provider may retain an unconsumed window after INPUT_SWITCH; that window is not in take_remainder(). The provider must hand the unconsumed native bytes to its successor consumer as required by the HTTP1 NativeABI contract. This layer does not yet install a native successor consumer on behalf of a framework.

Normal HTTP requests continue to reach on_request, even with WebSocket enabled. on_headers continues to run for both HTTP and WebSocket requests. One definition may serve many separate attached transports. For a WebSocket Close handshake, the existing Unblock::WebSocket transport contract controls graceful shutdown and unblock_finish. The framework still owns sockets, TLS, input dispatch and writable notifications.

METHODS

HTTP/2 Extended CONNECT WebSocket Server

Opt in with http2 => { websocket => 1 } and the same on_websocket($tx, $request) callback used for HTTP/1.1. When HTTP/2 WebSocket support is enabled, provide a defer coderef on the Server:

my $server = Unblock::HTTP::Server->new(
    http2 => { websocket => 1 },
    defer => sub {
        my ($work) = @_;
        $framework->defer($work);   # Always a later event-loop turn
    },
    on_request => sub {
        my ($tx) = @_;
        $tx->respond(status => 200, body => 'normal HTTP2');
    },
    on_websocket => sub {
        my ($tx, $request) = @_;
        return {
            on_message => sub {
                my ($ws, $payload, $type) = @_;
                $ws->send_text("echo:$payload") if $type eq 'text';
            },
            on_open => sub {
                my ($ws, $tx, $request) = @_;
            },
        };
    },
);

This policy advertises HTTP/2 SETTINGS_ENABLE_CONNECT_PROTOCOL. Without it, the unified Server explicitly disables advertising. Conflicting manual SETTINGS value 8 or enable_connect_protocol => 0 is rejected. HTTP/2 WebSocket is an Extended CONNECT with :protocol set to websocket, returning HTTP 200 (not 101). The upgrade affects one stream, not the TCP/TLS connection. Other streams remain ordinary HTTP2.

Incoming stream DATA goes directly through the real Transaction's on_data() to its own Unblock::WebSocket::Server. A small internal stream-specific bridge accepts outgoing WebSocket frames and queues them into the existing Transaction's write() on the provided deferred event-loop turn. The bridge respects the Transaction's on_drain() and calls end() only after all queued frames are accepted. WebSocket errors reset their own stream, not the shared HTTP/2 connection. No application or framework adapter needs to manually ferry DATA to WebSocket. The callback can return undef to reject the attempt with HTTP 403, and an invalid handshake gets HTTP 400.

defer MUST schedule work after the current HTTP/2 parser callback returns, never run it synchronously. Synchronous execution would reenter nghttp2 and is unsupported. The framework continues to send and receive ordinary HTTP2 bytes through its existing unblock_send, input, input_eof, transport_error, and resume_output. It does not call unblock_upgrade for HTTP/2. As a result, one attached H2 engine can handle multiple WebSockets and ordinary requests simultaneously. The transaction and socket remain owned by the existing H2 engine and framework.

HTTP/2 Client WebSocket support and HTTP/3 Extended CONNECT remain separate, unfinished milestones. This H2 Server slice does not make any change to the lower HTTP2 or WebSocket distribution.

attach_stream

Accepts transport and either alpn or protocol. The transport provides unblock_send, unblock_finish, and unblock_abort. It retains the returned real HTTP1 or HTTP2 Server engine and feeds bytes directly to it. There is no intermediate connection wrapper and no transaction wrapper.

Missing or empty ALPN selects HTTP/1.1 only if HTTP/1.1 is enabled. Unknown or disabled protocols fail. Explicit protocol => 'h2' is for cleartext prior knowledge. HTTP/3 cannot be attached to a byte stream.

HTTP/3 Extended CONNECT WebSocket Server

Opt in with http3 => { websocket => 1 }, supply on_websocket($tx, $request) and defer($work) as for HTTP/2, and attach each ready Net::QUIC connection with attach_quic. The unified Server advertises SETTINGS_ENABLE_CONNECT_PROTOCOL (HTTP/3 setting 8) only when explicitly enabled. A WebSocket CONNECT uses a normal QUIC bidirectional request stream, not a separate UDP socket or QUIC connection. The handshake is HTTP 200 (not HTTP 101).

Incoming WebSocket DATA uses the existing H3 Transaction on_data. An internal deferred per-stream writer queues outbound WebSocket frames and hands them to the same Transaction through write(), using on_drain to honor stream backpressure. The WebSocket Close handshake ultimately ends the stream. Application denial returns HTTP 403; invalid WebSocket handshake returns HTTP 400. Ordinary HTTP/3 traffic continues to use its normal callbacks. defer must never invoke the scheduled callback inline from an H3 parser callback. The framework remains responsible for QUIC UDP delivery, timeout scheduling, writable readiness and connection lifecycle.

The actual Net::QUIC 0.05 loopback/TLS integration, WebSocket frames, ordinary stream isolation, Close and HTTP 400/403 tests passed both full upper integration workflows at source b43532e92917d5850b6abc78c16485e3e16acc37. The separate H3 Client implementation has also passed full integration at source a3f429edf66562a55ab23e0d5b55a8e3eb989b7c; see Unblock::HTTP::Client for the Client-specific OPTIONS and callbacks.

attach_quic

Accepts connection containing a ready, open QUIC connection negotiated for HTTP/3. Returns a started real HTTP3 Server engine. The caller retains it until the connection closes. UDP and timer events still go to the QUIC Driver, never to an HTTP byte input method.

protocols

Returns enabled wire protocol names in list context: http/1.1, h2, and/or h3.

alpn_protocols

Returns enabled TCP/TLS ALPN choices, preferring h2 over http/1.1. The framework uses this list when configuring TLS. HTTP/3 uses a separate QUIC listener and is not included in this list.

supports

supports($protocol) reports whether a wire protocol name is enabled.

OWNERSHIP

One Server definition can serve many connections. It does not own listeners, sockets, engine registries, or a network queue. The framework retains each returned engine. Engine callbacks retain their original identity.

LICENSE

MIT License.