Skip to content

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 standard SERVER_PREFACE_BYTES + initial SETTINGS at handshake time. Most catalogue scenarios want this (real H2 clients require it before proceeding); set False to 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.