blackbull.fault_injection¶
blackbull.fault_injection
¶
Deliberately malformed peers and scenario helpers.
Choose the scenario vocabulary by protocol and by whether the target is a client or server. Fault servers refuse production mode and remote binds without allow_remote=True; see docs/guide/fault_injection.md.
Abort
dataclass
¶
Hard-close the connection (transport.abort → RST on Linux).
Distinct from the graceful writer.close() / wait_closed()
in HTTP1Client.__aexit__. Subsequent steps short-circuit; the
executor stops walking the scenario after an Abort.
Category
¶
Bases: str, Enum
Why a differential example was (not) accepted.
Subclassing str makes the values JSON-serialisable directly
and keeps assert ctx.category == 'OK'-style sites readable.
CloseGracefully
dataclass
¶
Send a GOAWAY then close cleanly.
Subsequent scenario steps short-circuit (this is a terminator
just like Abort). error_code is one of the
ErrorCodes values;
last_stream_id advertises the last stream the server is
willing to process — pass 0 to refuse all client streams,
or the highest accepted stream ID otherwise.
ExpectClientFrame
dataclass
¶
Consume exactly one frame and record whether it matched; never filter.
ExpectRequest
dataclass
¶
Read one request head and record whether it matched.
A guard, not a filter: nothing is skipped and the connection stays in
step. It asks whether the client under test is behaving as the
scenario assumes — a fault staged against Expect: 100-continue tests
nothing if the client never sent that header.
A mismatch is recorded, not raised:
ScenarioH1ServerResult.expectations collects one
(match, matched) pair per step.
ExpectResponse
dataclass
¶
Read one response and record whether it matched.
A guard, not a filter: nothing is skipped and the executor moves on either way. It answers is the peer behaving as this scenario assumes? — and a scenario whose premise silently failed would otherwise look like a pass.
Twin of
ExpectRequest.
ExpectServerFrame
dataclass
¶
Read one frame and record whether it matched.
A guard, not a filter: nothing is skipped and the executor moves
on either way. Twin of
ExpectClientFrame.
H1CExpectResponse
dataclass
¶
Read one response and record whether it matched.
A guard, not a filter: nothing is skipped and the executor moves on either way. It answers is the peer behaving as this scenario assumes? — and a scenario whose premise silently failed would otherwise look like a pass.
Twin of
ExpectRequest.
H1CSendRawBytes
dataclass
¶
Push raw bytes onto the connection.
byte_interval > 0 transmits one byte at a time with that delay
between bytes — the primitive slowloris-style stall that lets
scenarios express trickled headers or trickled bodies without
dropping to a raw asyncio socket.
H1CWaitForResponse
dataclass
¶
Read responses until one satisfies match, or the timeout wins.
A filter: non-matching responses are read, counted in
wait_skipped, and passed over. The role-axis twin of
WaitForRequest,
and it exists for the same reason — a scenario that has to know
exactly how many messages precede the interesting one is a scenario
written against a particular peer.
On HTTP/1.1 a non-zero wait_skipped is worth reading: responses
arrive in request order, so skipping one means the scenario is a
response further along than it thinks.
H1FaultServer
¶
Serves one ScenarioH1Server to each connecting client.
stop()
async
¶
Stop accepting and cancel anything still mid-scenario.
The cancellation is the part that matters: a scenario whose last
step is a long Sleep is the normal shape here — that is how
"the server never answers" is expressed — so a teardown that waited
for handlers would wait out the sleep on every such case.
wait_for_connection_done(timeout=5.0)
async
¶
Block until a client has connected and the scenario finished.
H1SAbort
dataclass
¶
Hard-close the connection (transport.abort → RST on Linux).
Terminal: later steps short-circuit.
H1SCloseGracefully
dataclass
¶
Close cleanly (FIN) after whatever has been written.
Terminal. What the client sees is an orderly EOF mid-body, not the
reset Abort sends.
H1SEndChunkedBody
dataclass
¶
The zero-length chunk that terminates a chunked body.
trailers are emitted before the final CRLF; omit this step to stage
a body that never terminates.
H1SEndHeaders
dataclass
¶
The blank line that ends the head. Omit it to stage a head that never finishes.
H1SHalfClose
dataclass
¶
Shut down the sending direction only (FIN), keep reading.
Not terminal — later steps still run, which is the whole point.
Abort is not a substitute: it sends RST, discarding what is buffered
and leaving nothing to read.
H1SSendChunk
dataclass
¶
One chunk of a chunked body (RFC 9112 §7.1).
declared_size sets the chunk-size line independently of the data —
the HTTP/1.1 twin of SendFrame.declared_length, and the single most
common framing fault there is. extension appends a chunk
extension; terminator can be replaced to stage a bad CRLF.
H1SSendHeader
dataclass
¶
Emit a header; fold=True emits obs-fold (RFC 9112 §5.2).
H1SSendRawBytes
dataclass
¶
Push arbitrary bytes at the client.
The only way this server emits anything. byte_interval > 0
transmits one byte at a time with that delay, which is how a trickled
status line or a slow header block is expressed — the bytes are all
legal and the pacing is the fault, so it cannot be spelled any other
way.
H1SSendStatusLine
dataclass
¶
Emit status-line fields without validation; malformed values are intentional.
H1SSleep
dataclass
¶
Idle for duration seconds, holding the connection open.
Distinct from a slow send: nothing is written at all, so this is what a client's own response deadline is measured against.
H2Abort
dataclass
¶
Hard-close the connection (transport.abort → RST on Linux).
H2CAbort
dataclass
¶
Hard-close the connection (transport.abort → RST on Linux).
Terminal: later steps short-circuit, exactly as on the HTTP/1.1 side.
H2CExpectServerFrame
dataclass
¶
Read one frame and record whether it matched.
A guard, not a filter: nothing is skipped and the executor moves
on either way. Twin of
ExpectClientFrame.
H2CHalfClose
dataclass
¶
Shut down the sending direction only (FIN), keep reading.
Not terminal — later steps still run, which is the whole point.
Abort is not a substitute: it sends RST, discarding what is buffered
and leaving nothing to read.
H2CReadResponse
dataclass
¶
Read one frame from the server, or record a timeout.
H2CSendFrame
dataclass
¶
Emit one frame, built here rather than by the production sender.
payload is the frame payload; the 9-byte header is assembled from
the other fields. Low-level on purpose: a length that disagrees with
the payload, or a flag combination the typed frame classes refuse, is
unreachable through FrameFactory.
declared_length overrides the header's length field without
changing the bytes actually written — the direct way to express "the
peer lied about how much is coming".
frame_type is the raw type byte: a FrameTypes
member (which is a one-byte bytes), or an int for a type the
enum does not name — an unregistered type being itself a fault worth
staging.
H2CSendPreface
dataclass
¶
Write the client connection preface (RFC 9113 §3.4).
A step, not the boolean ScenarioH2.send_preface the server side
has: delaying or splitting the preface is the fault, and a flag cannot
say "after 30 seconds".
H2CSendRawBytes
dataclass
¶
Push arbitrary bytes at the server.
The escape hatch, and the same name and fields the HTTP/1.1 client-side
vocabulary uses. byte_interval > 0 transmits one byte at a time
with that delay.
H2CSleep
dataclass
¶
Idle for duration seconds without reading or writing.
H2CWaitForServerFrame
dataclass
¶
Filter frames until match or timeout; count and consume nonmatches in wait_skipped.
H2FaultServer
¶
Programmable HTTP/2 server emitting a ScenarioH2.
Async context manager: enter to bind + start accepting, exit to
shut down. self.url is set after enter and gives the URL a
real h2c client can dial.
Parameters¶
scenario:
The ScenarioH2 the executor walks for each accepted
connection.
host:
Bind host. Defaults to '127.0.0.1' — binding to a
non-localhost interface is rejected unless allow_remote
is set (the misbehaviour mode is for local tests only).
port:
Bind port. Defaults to 0 (kernel-assigned random port).
allow_remote:
Bypass the localhost-only safety check. Off by default.
ssl_context:
Optional ssl.SSLContext. When provided, the server
terminates TLS and self.url is https://...; ALPN must
offer h2 so clients negotiating H/2 over TLS (httpx, curl
--http2, …) connect cleanly. When None (default), the
server speaks plaintext h2c — usable with prior-knowledge
clients only.
Attributes¶
url:
http://<host>:<port>/ after start, or https://... when
a TLS context is provided.
last_result:
ScenarioH2Result from the most recently completed
connection. None before any client has connected.
wait_for_connection_done(timeout=5.0)
async
¶
Block until a client has connected and the scenario finished.
H2FaultServerError
¶
Bases: RuntimeError
Raised when H2FaultServer cannot start.
The most common cause is running in a production context: the
deliberate-misbehaviour machinery refuses to start when the framework's
production signal (BLACKBULL_ENV=production) — or the explicit
BB_PRODUCTION override — is set, regardless of how it was reached.
H2SAbort
dataclass
¶
Hard-close the connection (transport.abort → RST on Linux).
H2SCloseGracefully
dataclass
¶
Send a GOAWAY then close cleanly.
Subsequent scenario steps short-circuit (this is a terminator
just like Abort). error_code is one of the
ErrorCodes values;
last_stream_id advertises the last stream the server is
willing to process — pass 0 to refuse all client streams,
or the highest accepted stream ID otherwise.
H2SExpectClientFrame
dataclass
¶
Consume exactly one frame and record whether it matched; never filter.
H2SHalfClose
dataclass
¶
Shut down the sending direction only (FIN), keep reading.
Not terminal — later steps still run, which is the whole point.
Abort is not a substitute: it sends RST, discarding what is buffered
and leaving nothing to read.
H2SSendFrame
dataclass
¶
Emit a typed frame; use SendRawBytes for unsupported or malformed encodings.
A declared_length override changes only the header, not the emitted payload.
H2SSendRawBytes
dataclass
¶
Push arbitrary bytes onto the connection.
Escape hatch for malformed frames (illegal type byte, length
exceeding SETTINGS_MAX_FRAME_SIZE, etc.) that the typed
SendFrame path will not produce.
byte_interval > 0 transmits one byte at a time with that
delay — useful for stalled-handshake patterns where the client
is expected to enforce a preface-completion timeout.
H2SSleep
dataclass
¶
Idle for duration seconds without reading or writing.
H2SWaitForClientFrame
dataclass
¶
Consume until a frame matches, skipping nonmatches.
Timeout records a miss and proceeds to the next step.
H2Sleep
dataclass
¶
Idle for duration seconds without reading or writing.
HalfClose
dataclass
¶
Shut down the sending direction only (FIN), keep reading.
Not terminal — later steps still run, which is the whole point.
Abort is not a substitute: it sends RST, discarding what is buffered
and leaving nothing to read.
ReadResponse
dataclass
¶
Read one HTTP/1.1 response from the connection.
timeout bounds the entire status-line + headers + body read.
On timeout the executor records the outcome on the
ScenarioResult and does not raise — the caller decides
whether to treat that as a transport-fail or normal outcome.
Scenario
dataclass
¶
A sequence of steps the executor walks against one connection.
from_bytes(raw)
classmethod
¶
Decode every byte string into a runnable fuzz scenario, including empty input.
Truncated input returns the completed steps. Abort discards remaining bytes; SEND payload lengths are uint16. This decoder must not raise on arbitrary input.
from_json(src)
classmethod
¶
Parse JSON Lines back to a Scenario.
Skips blank lines so files that end with a trailing newline (the conventional git-friendly shape) parse cleanly.
A HEADER line carries the name and is optional on the way in:
a file without one parses to an unnamed scenario.
to_json()
¶
Serialize JSON Lines with base64 byte payloads and a first HEADER carrying name.
well_formed(raw_request, *, response_timeout=5.0)
classmethod
¶
Wrap a complete raw HTTP/1.1 request as a one-shot scenario.
Equivalent to "send these bytes, then read one response".
Used by the Hypothesis well_formed_scenario_strategy in the
HTTP/1.1 differential suite.
ScenarioH1Server
dataclass
¶
An ordered sequence of steps, plus a name for test parametrisation.
steps is a tuple, as on ScenarioH2: a frozen dataclass
holding a mutable list is a frozen container of mutable contents.
ScenarioH1ServerResult
dataclass
¶
What the executor observed while running a scenario.
Field names match ScenarioH2Result
wherever the two mean the same thing, so a harness that reports on one
half does not need a second spelling for the other.
ScenarioH2
dataclass
¶
Sequence of steps a programmable H2 server walks per connection.
Two control knobs sit outside the step list because they apply to the whole connection, not to one step:
send_preface: whether the server sends the standardSERVER_PREFACE_BYTES+ initial SETTINGS at handshake time. Most catalogue scenarios want this (real H2 clients require it before proceeding); setFalseto exercise client behaviour against a server that skips the handshake.initial_settings: tuple of(setting_id, value)pairs the server advertises in its initial SETTINGS frame. Used by the "exhausted window" and "illegal SETTINGS" catalogue entries to inject a hostile starting state before the first step runs.
ScenarioH2Client
dataclass
¶
An ordered sequence of steps, plus a name for parametrisation.
ScenarioH2ClientResult
dataclass
¶
Outcome of one HTTP2Client.execute_scenario call.
Field names come from
ScenarioResult, its
twin, so a harness reporting on one does not need a second spelling for
the other.
ScenarioH2Result
dataclass
¶
Outcome of one ScenarioH2 run.
Mirrors ScenarioResult's
shape so callers can write uniform pytest assertions across
protocols.
ScenarioResult
dataclass
¶
Collect scenario outcomes; fields are not mutually exclusive.
A timeout may precede a later response; response is newest, received holds all.
SendFrame
dataclass
¶
Emit a typed frame; use SendRawBytes for unsupported or malformed encodings.
A declared_length override changes only the header, not the emitted payload.
SendRawBytes
dataclass
¶
Push arbitrary bytes onto the connection.
Escape hatch for malformed frames (illegal type byte, length
exceeding SETTINGS_MAX_FRAME_SIZE, etc.) that the typed
SendFrame path will not produce.
byte_interval > 0 transmits one byte at a time with that
delay — useful for stalled-handshake patterns where the client
is expected to enforce a preface-completion timeout.
SideOutcome
dataclass
¶
One side's response in a differential pair.
Exactly one of (response, exception) is populated. timed_out
is True if the failure was an asyncio.TimeoutError from
the per-example wait_for; we record it separately because timeouts
are semantically distinct from other transport errors.
Sleep
dataclass
¶
Idle for duration seconds without sending or reading.
Useful for post-headers idle, mid-keep-alive idle, and pre-response stall scenarios where the server is expected to time out and close.
StepOp
¶
Bases: str, Enum
Tag for serialising / decoding a step. str mixin so values
drop straight into JSON without a custom encoder.
StepOpH1Server
¶
Bases: str, Enum
Tag used by the JSON serialiser.
StepOpH2
¶
Bases: str, Enum
Tag used by the JSON serialiser.
StepOpH2Client
¶
Bases: str, Enum
Tag used by the JSON serialiser.
WaitForClientFrame
dataclass
¶
Consume until a frame matches, skipping nonmatches.
Timeout records a miss and proceeds to the next step.
WaitForRequest
dataclass
¶
Block until a request head arrives, optionally one that matches.
A filter: with match set, heads that do not match are read and
skipped and the step keeps waiting, which on a pipelined connection
(RFC 9112 §9.3.2) is how a scenario breaks the POST and not the GET.
On timeout expiry the executor records the miss and proceeds.
Skipping desyncs the connection, because HTTP/1.1 responses are
positional: a head passed over is one the scenario can no longer
answer. The count is on ScenarioH1ServerResult.requests_skipped.
ExpectRequest is the guard that skips nothing.
WaitForResponse
dataclass
¶
Read responses until one satisfies match, or the timeout wins.
A filter: non-matching responses are read, counted in
wait_skipped, and passed over. The role-axis twin of
WaitForRequest,
and it exists for the same reason — a scenario that has to know
exactly how many messages precede the interesting one is a scenario
written against a particular peer.
On HTTP/1.1 a non-zero wait_skipped is worth reading: responses
arrive in request order, so skipping one means the scenario is a
response further along than it thinks.
WaitForServerFrame
dataclass
¶
Filter frames until match or timeout; count and consume nonmatches in wait_skipped.
__getattr__(name)
¶
Resolve deprecated spellings with a caller-attributed warning.
categorize(ng, bb)
¶
Bucket a differential example into a Category.
Order of the checks matters: both-rejected wins over individual transport failures so we don't flag inputs that nginx also refused.
frame_matches(frame, match)
¶
Return True iff frame satisfies every key in match.
Recognised keys: type, stream_id, flags_set,
flags_unset, error_code. Unknown keys fail closed — an
unrecognised match key is almost certainly a typo in a catalogue
entry, and silently matching on a missing key would hide the bug.
error_code is what turns "a GOAWAY arrived" into "the peer
rejected this for that reason", and a frame carrying no error code
never matches it. Both HTTP/2 roles share this function, so the key
is available to the broken server and the broken client alike.
make_self_signed_h2_context()
¶
Return a server-side SSLContext that negotiates h2 via ALPN.
The returned context loads an ephemeral RSA self-signed cert from
a tempdir and advertises h2 (and http/1.1 as a fallback)
in the ALPN protocol list. Use it with H2FaultServer's
ssl_context= parameter.
The certificate path is attached to the returned object as
bb_ca_cert_path: str so callers can hand it to a client
library (e.g. httpx.AsyncClient(verify=ctx.bb_ca_cert_path))
instead of disabling certificate validation.
Requires cryptography — installed via the [fault-injection]
extra (pip install 'blackbull[fault-injection]').
normalize_response(resp)
¶
Drop volatile / framing-only headers; preserve status + body.
request_matches(head, match)
¶
Return True iff head satisfies every key in match.
Recognised keys: method, target, version, header,
header_absent. Unknown keys fail closed — an unrecognised key
is almost certainly a typo in a scenario, and silently matching on a
key nobody reads would hide it. frame_matches on the HTTP/2 side
made the same choice for the same reason.
header takes (name, value); pass value=None to match on
presence alone. header_absent takes a name.
response_matches(response, match)
¶
Return True iff response satisfies every key in match.
Recognised keys: status, reason, version, header,
header_absent, body_contains. Unknown keys fail closed —
an unrecognised key is almost certainly a typo in a scenario, and
silently matching on a key nobody reads would hide it.
request_matches and frame_matches made the same choice for the
same reason.
header takes (name, value); pass value=None to match on
presence alone. header_absent takes a name.
run_scenario(host, port, scenario)
async
¶
Execute scenario against (host, port) and return (outcome, wire_bytes).
Drives the scenario through HTTP1Client.execute_scenario,
which itself never raises; this wrapper only adds an outer
asyncio.wait_for so a runaway scenario (e.g. trickled bytes
plus a long read timeout) can't blow the per-side budget.
Returns the captured wire bytes (both servers receive identical bytes, so a single capture is enough for failure diagnostics).
scenario_h1_server_from_json(src)
¶
Parse JSON Lines back to a ScenarioH1Server.
scenario_h1_server_to_json(scenario)
¶
Serialise scenario to JSON Lines (one step per line).
The name sits on the first line under the op HEADER, keeping the
file one line-oriented stream with no out-of-band metadata.
Payloads are hex, not base64: a fault scenario's bytes are frequently not valid UTF-8 and are read against a packet capture by eye.
scenario_h2_client_from_json(src)
¶
Parse JSON Lines back to a ScenarioH2Client.
scenario_h2_client_to_json(scenario)
¶
Serialise to JSON Lines, name on the first HEADER line.
scenario_h2_from_json(src)
¶
Parse JSON Lines back to a ScenarioH2.
scenario_h2_to_json(scenario)
¶
Serialize JSON Lines; the first HEADER carries send_preface and initial_settings.
serialize_frame(frame)
¶
Convert a FrameBase instance to wire bytes.
Restricted to the classes in ROUND_TRIP_FRAME_CLASSES, and to frames
require_canonical accepts; anything else goes through
SendRawBytes.