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.