Skip to content

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.