blackbull.server.parser¶
blackbull.server.parser
¶
parse_headers(frame)
¶
Build a native :class:Connection (http or websocket) from a
HEADERS frame, or None when the request is malformed.
Hot path on every request — kept as a module-level function so that
callers avoid the dict-lookup + parser allocation that ParserFactory
requires.
Sprint 79 Phase 4: yields a :class:Connection rather than an ASGI scope
dict (the H/2 analogue of :meth:HTTP1Actor._parse). The actor derives
the compat scope via :meth:Connection.as_scope at the dispatch boundary
until Phase 5 switches the consumers onto conn directly; the
websocket-only subprotocols scope key is likewise attached there (it is
not a :class:Connection field — see the proposal §2.1 field set), the
same way :meth:HTTP1Actor._handle_upgrade augments the derived scope.
Sprint 80 alloc hygiene (proposals/connection-alloc-hygiene.md,
2026-07-24): builds the :class:Connection once, at the very end, from
locals accumulated while walking the pseudo-headers — the same
single-construction idiom :meth:HTTP1Actor._parse already uses (Phase 1).
The previous version built a placeholder via a removed
_default_connection helper (with a throwaway Headers([]), discarded
~154 ns/req) and mutated it field by field. Every early-out below now
returns None instead of a half-built Connection — including the
(previously reachable but never-observed) host-validation-failure path,
since _request_headers_with_host already marks frame.malformed
before returning None, and every caller checks frame.malformed
before reading this function's result. The contract is now uniformly
result is None ⟺ frame.malformed. The plain-HTTP branch's final
construction goes through _build_h2_connection (Phase 2 — an
object.__new__ lean builder), not the dataclass constructor; see that
function's docstring.
Also performs request-level pseudo-header presence checks (RFC 9113
§8.3.1). Field-level checks already happened in parse_payload. On any
known-bad input — parse_payload having flagged frame.malformed, or a
missing/empty required pseudo found here (which sets frame.malformed) —
we return None rather than build a throwaway :class:Connection: the
actor's frame.malformed check answers RST_STREAM before it would read
the result, so no object is constructed on the error path.