Skip to content

blackbull.server.sender

blackbull.server.sender

Protocol send boundaries.

Always route fragments through BaseSender._write_many; protocol senders do not choose join versus vectored writes. Writer adapters owe both paths.

AbstractWriter

Bases: ABC

Protocol-agnostic async byte-sink.

write() is the single responsibility: deliver bytes and ensure they are flushed. Backpressure, buffering, and draining are implementation details of each concrete subclass — callers never call drain() directly.

Implementors wrap a concrete transport (asyncio.StreamWriter, trio MemorySendStream, curio socket, …). BaseSender only depends on this interface, so switching the async runtime requires only a new subclass here.

close() async

Close the underlying transport. Default: no-op.

peer_is_gone()

True when the transport has already recorded the loss.

Asked before a write, because the exception the guard below catches can arrive arbitrarily later: connection_lost is delivered through call_soon, so between the transport recording the loss and the protocol learning of it there is a window in which write() drops silently and drain() returns without raising. Every write in that window is one asyncio counts and warns about.

sendfile(file, offset, count) async

Send up to count bytes from file starting at offset.

Default implementation raises NotImplementedError so callers can detect lack of support and fall back to a read+write loop. Concrete subclasses opt in when the underlying transport supports a zero-copy path (Linux sendfile(2) / loop.sendfile).

Used by the static-file middleware via the http.response.pathsend ASGI extension.

write(data) abstractmethod async

Write data to the transport and ensure it is flushed.

writelines(parts) async

Write ordered byte parts; the default joins before writing.

Transports may override this to avoid joining. Copy behavior is transport-specific.

AsyncioWriter

Bases: AbstractWriter

Adapts an asyncio-compatible stream to AbstractWriter.

The constructor accepts any object that exposes write(bytes) (sync) and drain() (async) — the asyncio StreamWriter API — so that test doubles such as MagicMock can be injected without ceremony.

drain() is called inside write() so the asyncio backpressure mechanism is handled transparently and BaseSender stays runtime-agnostic.

write_timeout (seconds, 0 = disabled) bounds the time spent in drain() waiting for the kernel send buffer to flush — the slow-read shape of slowloris, where a peer reading at 1 byte/sec blocks the drain on a TCP window that never reopens. On timeout the transport is closed and ConnectionResetError raised, so the sender treats it as a peer-side reset. The bound rides the shared deadline scanner, whose BB_DEADLINE_TICK_MS granularity is the slop on when it fires.

sendfile(file, offset, count) async

Zero-copy loop.sendfile against the underlying transport, in bounded chunks.

Raises NotImplementedError (propagated from the loop) when the transport is SSL — TLS framing happens in user-space, so the kernel cannot see the plaintext to copy. Callers must catch that and fall back to a read+write loop. Support is a property of the transport, so it is decided on the first chunk: a later chunk cannot discover that sendfile was unavailable all along.

Drains any pending writes first, under the write bound like every other drain, so buffered headers precede the file bytes in wire order. The chunking is what gives BB_WRITE_TIMEOUT somewhere to re-arm; the Internals page sizes it. Returns the octets actually sent, short of count only when the peer stopped accepting.

writelines(parts) async

Delegate ordered byte parts to StreamWriter; its transport chooses how to write.

BaseSender

Bases: ABC

Protocol senders accepting native messages, compatible ASGI events, or bytes.

Byte writes go through AbstractWriter; send parts through _write_many so its shared size gate selects joining versus vectored transport writes.

mark_client_gone()

Drop further writes when the actor detects a dead peer.

http.disconnect remains a receive-side event, never a send argument.

ConnectionWindow

Connection send credit shared by every stream (RFC 9113 §6.9.1).

Debit before awaiting writes. Window changes may make stream credit negative; the actor wakes blocked senders on connection WINDOW_UPDATE.

FlowControlStalled

Bases: Exception

The peer never granted the flow-control credit it was asked for.

Distinct from a write failure: the socket is fine and the peer is answering — it simply declines to accept the response it requested, which is the "data dribble" shape of CVE-2019-9511. Carried as its own type so the stream ends with RST_STREAM(CANCEL) (a stream we gave up on) rather than INTERNAL_ERROR (a server that broke).

HTTP1Sender

Bases: BaseSender

Translates content or ASGI HTTP send events into HTTP/1.1 wire-format bytes.

__call__ accepts two forms:

High-level (bytes body + status): await sender(body_bytes, HTTPStatus.OK, headers=[...]) Writes the status line, headers, blank line, and body in one call.

Low-level (ASGI event dict, for internal/error-handler use): await sender({'type': 'http.response.start', ...}) await sender({'type': 'http.response.body', ...})

http.response.start is buffered until http.response.body arrives so that Content-Length can be injected when the app omits it.

__call__(body, status=HTTPStatus.OK, headers=()) async

Dispatch supported native or ASGI response events and bytes.

Buffer a start until body framing is known. Streaming without Content-Length uses chunked encoding; declared trailers own the terminator. Unknown ASGI event types are logged and dropped; unsupported values raise TypeError. Once a write has found the peer gone, raise ClientDisconnected.

HTTP2Sender

Bases: BaseSender

Translates content or ASGI HTTP send events into HTTP/2 frames.

__call__ accepts four forms:

High-level (bytes body + status): await sender(body_bytes, HTTPStatus.OK, headers=[...]) Sends a HEADERS frame followed by a DATA frame.

Native (NativeResponse): await sender(NativeResponse(status=..., header=..., body=...)) One object may carry header, body, and/or trailers; the sender buffers the header arm exactly like the dict start and delegates body/trailers to the shared helpers (HEADERS + DATA [+ trailing HEADERS] coalesce).

Low-level (ASGI event dict): await sender({'type': 'http.response.start', ...}) await sender({'type': 'http.response.body', ...})

Frame object (FrameBase instance): await sender(settings_frame) Field blocks are fragmented within the peer frame-size limit.

connection_window_size property writable

The shared connection-level send window.

A property over ConnectionWindow, not a per-sender field: every sender on the connection reads and writes the same value.

adjust_initial_window(delta)

RFC 9113 §6.9.2 — adjust this sender's stream flow-control window by the change in SETTINGS_INITIAL_WINDOW_SIZE since the peer's last announcement. The window may legitimately become negative.

apply_settings(max_frame_size=None)

Apply SETTINGS parameters that do not require delta tracking.

retire_stream()

Stop this stream's pending and future output without closing its peer.

HTTP/2 senders share one transport, so stream retirement cannot use the writer's connection-wide peer_gone flag. The per-sender closed bit drops later writes, while cancellation and the window wake release work already parked inside this sender.

send_response_headers(status, headers) async

Send a complete field section immediately, leaving the stream open.

wake_window()

Wake any blocked _write_data() after a window credit change.

SenderFactory

Create protocol senders from an AbstractWriter or wrap an asyncio-compatible writer.

WebSocketSender

Bases: BaseSender

Translates ASGI websocket send events or WebSocketResponse dicts into WebSocket wire frames (RFC 6455).

__call__ accepts an ASGI event dict (as returned by WebSocketResponse): - {'type': 'websocket.send', 'text': ...} → text frame (opcode 0x1) - {'type': 'websocket.send', 'bytes': ...} → binary frame (opcode 0x2) - {'type': 'websocket.close'} → close frame (opcode 0x8) - {'type': 'websocket.accept'} → no-op (handshake already sent)

The status and headers parameters are accepted for interface consistency but are unused for WebSocket connections.

build_response_headers(encoder, stream_id, status, headers, *, end_stream, max_frame_size=DEFAULT_MAX_FRAME_SIZE)

Encode response HEADERS, adding Date and dropping connection-specific fields.

build_trailers(encoder, stream_id, headers, *, max_frame_size=DEFAULT_MAX_FRAME_SIZE)

Encode a terminal field section without pseudo-headers or connection-specific fields.