blackbull.connection¶
blackbull.connection
¶
Native request representation.
ASGI scopes exist only at compatibility boundaries. Keep conversion fields in _CONNECTION_FIELDS; state and extensions remain shared after conversion.
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.
Connection
dataclass
¶
One HTTP (or WebSocket) request — the single internal representation.
Its creator builds it to the request contract: header names are lowercase
tchar, values carry no edge SP/HTAB and no CTL, host is at most one
valid authority, and scheme is lowercase. The HTTP/1.1 and HTTP/2
parsers and from_scope do; code that builds one directly must too.
The router, dispatcher, middleware and handlers rely on it and check
nothing again. The ASGI scope dict is a derived view
(as_scope). Request is a deprecated alias of this class.
cookies
property
¶
Cookies from the Cookie header, parsed once and cached.
disconnected
property
¶
True once the client dropped mid-request.
A long-running handler polls this to abandon work whose answer nobody is waiting for::
for row in rows:
if conn.disconnected:
break
It goes true when the server notices, at the next receive(),
rather than the instant the peer's FIN lands. The module-level
disconnected reads the same state at the two ASGI boundaries,
where it may live on a scope dict instead.
path_params
property
writable
¶
Router-assigned parameters with converter-coerced values.
query
property
¶
Query params as dict[str, str], parsed once and cached.
Last value wins on repeated keys (?tag=a&tag=b → {'tag': 'b'}).
This is the honest exit for what the declared-parameter form cannot
express — keys not known at registration time, and repeated keys; it
does not coerce types (that is the declared form's job, which also
answers a malformed value with 400 rather than a silent fallback).
query_list
property
¶
Return all query values per key, parsed once; query keeps only the last value.
subprotocols
property
¶
Return client-offered WebSocket subprotocols; empty for HTTP or no offer.
as_scope()
¶
Generate a fresh ASGI 3.0 scope dict (the single native→ASGI point).
Mutations of the returned dict do not affect the Connection, except
that state and extensions are shared by reference so a
buffering middleware's writes reach the handler.
body()
async
¶
Return the complete request body, draining receive at most once.
Repeated calls (and json / text) return the cached
bytes. A mid-body disconnect raises ClientDisconnected. Raises
RuntimeError if stream already consumed the body — the
channel is a single drain, so there is nothing left to buffer.
form()
async
¶
application/x-www-form-urlencoded body, parsed once and cached.
Reads the body through body, so a later json /
text call reuses the cached bytes. On a non-form
Content-Type (or no body) it returns {} rather than raising,
and does not touch the body. Multipart uploads are out of scope — use
stream and parse them manually.
from_scope(scope, receive=None)
classmethod
¶
Build a Connection from an external ASGI scope.
The result keeps the contract of a natively parsed Connection: header
names are lowercase tchar, values carry no edge SP/HTAB and no CTL,
host is at most one valid authority, and scheme is lowercase.
Raises FieldError for headers that cannot meet it. Unknown keys
are ignored; missing optional keys take the field defaults.
json()
async
¶
Parse the cached body as JSON (None on empty/invalid input).
stream()
async
¶
Yield body chunks once; mutually exclusive with body/json/text buffering.
Mixing modes raises RuntimeError; mid-body disconnect raises ClientDisconnected.
text(encoding='utf-8')
async
¶
Decode the cached body as text (errors='replace').
to_asgi_scope(*, force_asgi=False)
¶
Build the compatibility scope with its native Connection stashed.
force_asgi exercises both conversions. Preserve Connection-only OPTIONS state across that rebuild. The caller adds protocol-specific fields afterward; state and extensions remain shared for late updates.
bind_receive_channel(target, receive)
¶
Bind the raw receive channel once.
Never store a disconnect wrapper that captures conn: that creates a reference cycle. Preserve a receive channel already bound by an external ASGI host.
disconnected(target)
¶
True if the client disconnected mid-request. Accepts either a native
Connection (the app(conn, …) path) or an ASGI scope dict
(the BB_FORCE_ASGI_SCOPE / external-server path).
mark_disconnected(target)
¶
Record a mid-request client disconnect on a Connection or an
ASGI scope dict (idempotent — caller guards on disconnected).
refuse_scope(scope, send)
async
¶
Answer an ASGI scope that Connection.from_scope refused as the
native parsers answer the same request: a bare 400, or close 1002 before
accept for WebSocket.
stashed_connection(target, receive)
¶
Return (Connection, freshly_built), reusing the request's stash.
Only an external scope without a stash needs conversion. Callers apply their own state/path-parameter linking to a freshly built connection.