blackbull.request¶
blackbull.request
¶
Request body and cookie helpers.
Provides:
read_body: buffers all ASGIhttp.requestchunks into a singlebytesobject.read_json: buffers the body and parses it as JSON (Noneon empty/invalid).read_text: buffers the body and decodes it as text.cookies_from_headers: parses theCookieheader(s) into adict[str, str]straight from a headers object — the native core (whatConnection.cookiesuses).parse_cookies: the ASGI-scope-shaped wrapper of the above, for external callers that hold a scope dict.
The opt-in HTTP context object formerly named Request moved to
:class:blackbull.connection.Connection (Sprint 79 Phase 5); Request is now
a deprecated alias of Connection (see blackbull.__getattr__). This
module holds only the transport-agnostic free functions, which
:class:Connection builds on.
ClientDisconnected
¶
Bases: Exception
Raised when the client disconnects before the request body is complete.
ASGI signals a mid-body disconnect with an http.disconnect event that
carries no body/more_body keys. Treating it as end-of-message
would return a truncated upload as if it were whole, so
:func:read_body raises this instead — the handler must not process a
partial body as complete. The partial attribute holds whatever body
bytes had arrived before the disconnect.
cookies_from_headers(headers)
¶
Parse the Cookie header(s) into a dict, straight from a headers
object/iterable — no ASGI scope dict involved.
This is the native core: :meth:Connection.cookies calls it directly on
conn.headers; :func:parse_cookies is the ASGI-scope-shaped wrapper kept
for external callers that hold a scope dict. Accepts a
:class:blackbull.headers.Headers instance (uses getlist) or a plain
iterable of (name, value) bytes tuples (the ASGI 3.0 form).
parse_cookies(source)
¶
Parse the Cookie request header into a dict.
source is a mapping carrying a 'headers' key — an ASGI scope dict, or
the {'headers': conn.headers} wrapper Connection.cookies passes.
Works identically for HTTP/1.1, HTTP/2, and WebSocket. HTTP/1.1 sends a
single combined Cookie header; HTTP/2 may split it into multiple fields
(RFC 7540 §8.1.2.5). All cookie fields are collected and joined before
parsing, so both wire formats produce the same result.
Accepts either of the two header shapes that may appear on
source['headers']:
- A plain list/iterable of
(name, value)bytes tuples — the standard ASGI 3.0 form, used by external servers (uvicorn, hypercorn,httpx.ASGITransport). - A :class:
blackbull.headers.Headersinstance — what BlackBull's own server attaches as a handler ergonomics enhancement.
read_body(receive)
async
¶
Read the complete request body from the ASGI receive channel.
Collects chunks in a list and joins once, rather than the O(n²) +=
growth. A single-chunk body (the common case) is returned directly with
no intermediate copy at all (copy-reduction-http1 P1).
Raises :class:ClientDisconnected if an http.disconnect arrives
before the body is complete, so a truncated upload is never silently
returned as if whole.
read_json(receive)
async
¶
Read the request body and parse it as JSON.
Returns the parsed JSON value (dict, list, str, int,
float, bool), or None when the body is empty, not valid JSON,
or not decodable. Callers should treat None as a client error and
respond 400::
data = await read_json(receive)
if data is None:
await send(JSONResponse({'error': 'invalid JSON'},
status=HTTPStatus.BAD_REQUEST))
return
Note that a literal JSON null body also parses to None; if that
distinction matters, read the body yourself with :func:read_body.
A mid-body client disconnect propagates as :class:ClientDisconnected
rather than being reported as invalid JSON — a truncated body is a
transport failure, not a parse error.
read_text(receive, encoding='utf-8')
async
¶
Read the request body and decode it as text.
Uses errors='replace' so undecodable bytes become U+FFFD rather than
raising — a malformed body never crashes the handler. Override
encoding for non-UTF-8 payloads.
A mid-body client disconnect still propagates as
:class:ClientDisconnected: a truncated upload must not be decoded and
returned as if it were the complete text.
stream_body(receive)
async
¶
Yield the request body one http.request chunk at a time.
The streaming counterpart to :func:read_body: it never accumulates the
whole payload, so a handler that only needs to process the body (count
bytes, hash it, forward it, parse incrementally) holds one chunk at a time
instead of the entire upload. For a 20 MB upload this is the difference
between a ~64 KiB and a ~20 MB working set — and, because read_body
additionally b''.joins the chunk list, several times the throughput
under concurrency (the join is a full-payload memcpy plus GC pressure).
Empty chunks are skipped. A mid-body http.disconnect raises
:class:ClientDisconnected (with no partial — chunks already yielded
are the caller's to keep), so a truncated upload is never silently treated
as complete.