Skip to content

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.Headers instance — 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.