Skip to content

blackbull.native

blackbull.native

Native response message for the H1 send path (native-ization, Sprint 92).

The unified response message BlackBull's own server carries on the native path. One class replaces the ASGI start/body/trailers dicts: a response object may carry any combination of header (status line + headers), body (chunks), and trailers, so a complete response is one object and one send, while streaming is header-object then body-chunk objects.

Design invariants (validated in scratch/send-model-c.py):

  • Presence is is not None, never truthiness — an empty body is a real body (204-style). Middleware must preserve None when transforming (header or [] turns None into [] and the sender mis-detects a header — a real bug caught in the scratch model).
  • DX via properties, not the wire shaperesp.header is a zero-copy view with get/append/getlist/__contains__/__len__/ __iter__ (BlackBull Headers-like); body is a plain bytes with content_length/is_empty/content_type helpers.
  • Server hot path may read the raw slots (_header/_body) to skip the property+view overhead; the properties exist for middleware and handler-facing DX.
  • to_asgi() is the boundary conversion — 1 object → ASGI event list, used only at conversion boundaries: the external ASGI edge (asgi=True / external hosts) and the middleware native-read arms (symmetric with :meth:Connection.as_scope).

NativeResponse

A response on the native send path: header and/or body and/or trailers.

header is None when absent (never [] — presence is decided by is not None). body is None when absent; b'' is a real empty body. more_body marks a non-terminal body chunk (streaming). expects_trailers preserves the ASGI http.response.start trailers: True flag so the sender withholds the terminal chunk until the trailers event (lossless full-form compat — a terminal body before trailers would otherwise corrupt chunked framing).

complete(status, header, body) classmethod

Header plus terminal body — a whole response in one object.

to_asgi()

Convert to the ASGI event list (http.response.* dicts).

One object → one or more ASGI events, in wire order. Used only at conversion boundaries — the external ASGI edge (external hosts / asgi=True) and the middleware native-read arms (cache, compression); the native H1 sender path never materialises these dicts.

with_trailers(status, header, body, trailers) classmethod

Header, body and trailers together — the gRPC unary shape.

more_body is True by construction, and load-bearing: HTTP2Sender only takes its trailers-coalescing path for a non-terminal body chunk, holding HEADERS + DATA so they flush with the trailing HEADERS in one write. END_STREAM rides the trailers either way (RFC 9113 §8.1).

NativeWSMessage

One message on the native WebSocket send channel.

The WS counterpart of :class:NativeResponse, and it exists for the same reason. HTTP got a native send message in Sprint 92/93 and the sender a native arm; WebSocket was carried along as "conn is native, no scope" while its event channel stayed ASGI-shaped — so websocket.* dicts still travelled object → middleware → actor → sender on BlackBull's own path. The handler never saw them (that is the :class:~blackbull.websocket.WebSocket object's whole point), but everything under it did.

Three kinds, discriminated by :attr:kind rather than by which of seven fields happens to be set — the variants carry disjoint payloads, so a tag reads better here than the presence test that suits NativeResponse's combinable arms:

  • ACCEPTsubprotocol / headers; completes the handshake.
  • SEND — exactly one of text (str) or data (bytes).
  • CLOSEcode / reason.

data rather than bytes: the ASGI key is bytes, but a slot of that name shadows the builtin at every use site inside the class. :meth:to_asgi maps it back for the boundary.

to_asgi()

Convert to the ASGI websocket.* event list.

Used only at conversion boundaries — the external ASGI edge and the raw (conn, receive, send) compat surface — exactly like :meth:NativeResponse.to_asgi on the HTTP side.