blackbull.request¶
blackbull.request
¶
Request-body and cookie helpers.
read_body buffers; use streaming for bodies that should not accumulate. read_json returns None for an empty or invalid body. Request is a deprecated Connection alias, not an independent request model.
ClientDisconnected
¶
Bases: Exception
Raised when the client disconnects before the request body is complete,
and by send once a write has found the client gone.
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
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: Connection.cookies calls it directly on
conn.headers; parse_cookies is the ASGI-scope-shaped wrapper kept
for external callers that hold a scope dict. Accepts a
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
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.
Raises 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 read_body.
A mid-body client disconnect propagates as 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
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 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.
Empty chunks are skipped. A peer that vanishes mid-body raises
ClientDisconnected (with no partial — chunks already yielded
are the caller's to keep), so a truncated upload is never silently treated
as complete.