Skip to content

blackbull.server.protocol_registry

blackbull.server.protocol_registry

Protocol bindings own detection, framing reads and actor construction.

ConnectionActor peeks without consuming; bindings claim by ALPN or ordered cleartext detection. Port-bound raw protocols skip detection. Keep internal bindings out of the top-level public exports.

RawProtocolHandler = Callable[['AbstractReader', 'AbstractWriter', 'ProtocolContext'], Awaitable[None]] module-attribute

Async (reader, writer, ctx) -> None — owns one connection's lifetime.

ConnectionView dataclass

Per-connection inputs for the selected ProtocolBinding to construct its actor.

Http1Binding

Bases: ProtocolBinding

HTTP/1.1 — the cleartext fallback (matches any first line).

Http2Binding

Bases: ProtocolBinding

HTTP/2 — ALPN h2 or the cleartext connection preface (RFC 9113 §3.4).

ProtocolBinding

One connection-level protocol.

A binding declares how many leading bytes it needs to recognise a connection (detect_prefix_len) and whether it claims a given peeked prefix; the winner's single serve then performs its own protocol reads from a reader still positioned at the first byte, because detection peeks without consuming. Only a reader that cannot peek hands back what it took, and there a PrefixReader restores the stream.

Every protocol-specific read is the binding's, never the dispatcher's — the 24-byte HTTP/2 preface and the HTTP/1.1 \r\n request line alike — which is what keeps ConnectionActor protocol-agnostic.

claims(prefix, alpn)

Claim a shared-port connection by prefix; defaults to matches_cleartext.

matches_cleartext(first_line)

Return True if this binding owns a cleartext connection whose first line is first_line. Default: no match.

on_detect_timeout(conn) async

Called when the peer connected but sent no discriminator within the detection deadline. Default: close silently (the caller closes the transport) — appropriate for a protocol with no meaningful "you were too slow" wire message. HTTP overrides this to emit a 408. Lets ConnectionActor stay free of protocol-specific status strings.

prefix_possible(prefix)

Return True if prefix could still grow into a prefix this binding claims.

Consulted by the selector while prefix is shorter than detect_prefix_len. Default: always True — the selector keeps growing the peek to the full length. A binding that can rule prefix out cheaply overrides this and returns False, so the next binding claims without a full peek (the HTTP/2 preface's fixed first line rules a GET out on its first byte).

serve(conn) async

Drive one connection. conn.reader is positioned at the first byte of the stream, so the binding reads whatever framing it needs — detection peeked without consuming anything on its behalf.

ProtocolContext dataclass

Context passed to a non-ASGI protocol handler.

Carries connection metadata and the shared EventAggregator without exposing any ASGI concept (no scope / receive / send).

ProtocolDetector

Bases: ABC

Inspects the first bytes of a connection to identify the protocol.

Stateless — one instance is shared across all connections. First-byte sniffing on shared ports (MQTT + HTTP on one port, say), reached only when ALPN did not already name the protocol.

protocol_name abstractmethod property

Human-readable protocol name for logging.

detect(first_bytes, alpn) abstractmethod

Return True if first_bytes matches this protocol.

ProtocolRegistry

Single source of truth for all connection-level protocols.

Pre-populated with the built-in HTTP bindings; one instance per app. The cleartext-detection order is fixed: http2 (preface) before http1 (fallback) — http1 must be last because it matches any first line.

cleartext_bindings property

Ordered cleartext-detection chain (http2 then http1).

detection_order property

Bindings consulted during cleartext detection, in priority order: registered raw detectors first (shared-port protocols such as MQTT), then the ordered cleartext chain (http2 preface, http1 fallback). Cached, and rebuilt only by register.

port_bindings property

Raw bindings that need their own listening socket, keyed by port.

__bool__()

Truthy once a non-HTTP protocol is registered (HTTP is always present).

register(name, handler, *, detector=None, port=None, tls=False, stateful=True)

Register a non-ASGI protocol handler. Raises on duplicate name.

A detector enables shared-port sniffing (see ProtocolDetector). When several registered detectors could match the same first bytes, dispatch picks the first registered one — raw_bindings preserves insertion order. tls=True serves the binding's own port through the server's TLS machinery.

RawBinding

Bases: ProtocolBinding

A user-registered non-ASGI protocol, bound to its own listening port.

detect_prefix_len property

Bytes the detector needs to recognise the protocol. A detector may declare prefix_len; otherwise one byte (the common first-byte sniff, e.g. MQTT's 0x10) suffices. Port-bound bindings never detect → 0.

claims(prefix, alpn)

A raw binding claims a shared-port connection when its detector recognises the first bytes. Port-bound bindings (no detector) never claim via detection — they own their own listening socket instead.

A stateful binding stops claiming once more than one worker serves the shared listener: a worker that never saw the earlier exchange would answer wrongly. Its dedicated port is where it stays reachable.

disable_shared_dispatch()

Stop claiming shared-port connections. Called by the master.