Skip to content

blackbull.fault_injection.scenario_h2_client

blackbull.fault_injection.scenario_h2_client

Programmable HTTP/2 client-side scenario model.

A :class:ScenarioH2Client is a sequence of typed steps that :meth:blackbull.client.http2.HTTP2Client.execute_scenario walks in order against a live connection. This is the client-side half of the HTTP/2 toolkit: a programmable client that drives a target server through deliberate misbehaviour — a preface that never arrives, a header block opened and abandoned, a Rapid Reset burst, a window never opened.

Its twin is :mod:blackbull.fault_injection.scenario_h1, the client-side vocabulary one protocol over, and this module takes that twin's names wherever the two mean the same thing: :class:SendRawBytes, :class:ReadResponse, :class:Sleep, :class:Abort, and the fields of :class:ScenarioH2ClientResult. That matters more than it looks: a vocabulary written from the protocol rather than from its twin drifts away from it, and the drift is found by someone asking rather than by reading the code.

Two steps have no HTTP/1.1 counterpart, and both earn it:

  • :class:SendPreface — HTTP/1.1 has no connection preface.
  • :class:SendFrame — HTTP/2 is framed where HTTP/1.1 is a byte stream, so the typed step builds a frame rather than a blob.

As on the server side, the bytes are assembled here rather than by the production send path: a breaker that shares the production serialiser cannot emit a fault that serialiser has.

Abort dataclass

Hard-close the connection (transport.abort → RST on Linux).

Terminal: later steps short-circuit, exactly as on the HTTP/1.1 side.

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 :class:~blackbull.fault_injection.scenario_h2.ExpectClientFrame.

HalfClose dataclass

Shut down the sending direction only (FIN), keep reading.

Neither :class:Abort nor a full close says this. Abort sends RST, which discards whatever is buffered and leaves nothing to read; a full close ends both directions at once. A half-close is the ordinary end of a non-keep-alive exchange — "I have finished sending, I am still waiting for your answer" — and it is a distinct code path on the peer.

Not terminal: later steps still run, because continuing to read is the whole point.

ReadResponse dataclass

Read one frame from the server, or record a timeout.

ScenarioH2Client dataclass

An ordered sequence of steps, plus a name for parametrisation.

ScenarioH2ClientResult dataclass

Outcome of one :meth:HTTP2Client.execute_scenario call.

Field names come from :class:~blackbull.fault_injection.scenario_h1.ScenarioResult, its twin, so a harness reporting on one does not need a second spelling for the other.

SendFrame 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. Deliberately low-level: a fault scenario wants to set a length that disagrees with the payload, or a flag combination the typed frame classes refuse, and a step that went through FrameFactory could not.

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 :class:~blackbull.protocol.frame_types.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.

SendHeaders dataclass

Emit a HEADERS frame, with the header block built for you.

Added by the 107+108 consistency sweep, which found that every header-field fault — a malformed HPACK block, a missing or duplicated pseudo-header, a connection-specific header HTTP/2 forbids — could only be written as hand-assembled hex. Three of the sweep's rows moved from raw-bytes-only to typed with this step.

pseudo and headers are encoded with HPACK in the order given, so a scenario can put :path after a regular field (RFC 9113 §8.3 forbids it) simply by saying so. Nothing here validates: the whole point is to send what a conforming client would not.

raw_block replaces the encoded block outright, for faults HPACK itself cannot produce — a truncated block, an invalid table index, a Huffman string that does not decode. When set, pseudo and headers are ignored.

SendPreface dataclass

Write the client connection preface (RFC 9113 §3.4).

A step rather than a scenario flag, unlike ScenarioH2.send_preface on the server side: a client scenario's whole point may be to delay the preface, split it, or never send it, and a boolean cannot say "after 30 seconds".

SendRawBytes 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.

Sleep dataclass

Idle for duration seconds without reading or writing.

StepOpH2Client

Bases: str, Enum

Tag used by the JSON serialiser.

WaitForServerFrame dataclass

Read frames until one satisfies match, or the timeout wins.

A filter: non-matching frames are read, counted in wait_skipped, and passed over. Twin of :class:~blackbull.fault_injection.scenario_h2.WaitForClientFrame.

This is what makes an HTTP/2 client scenario able to observe a verdict. A single ReadResponse cannot: the first frame any correct server sends is its handshake SETTINGS, so a GOAWAY or RST_STREAM is always further down the stream, at a depth that varies by peer. A scenario that had to guess that depth was a scenario written against one server.

__getattr__(name)

PEP 562 — warn when the deprecated spelling is actually used.

A module-level assignment would alias silently; going through __getattr__ means a reader who never touches SendRawBytes never sees a warning, and one who does gets it at their own call site.

encode_frame(step)

Assemble one frame's wire bytes from step.

Here rather than in FrameBase.save() on purpose — the same rule the fault servers follow. It is also what makes declared_length possible: a header whose length disagrees with the payload is exactly the fault, and a serialiser that computed the length could not say it.

encode_headers(step)

Assemble one HEADERS frame from step.

HPACK encoding goes through the hpack package the server also uses, because a correct block is the baseline every header fault is a deviation from — hand-rolling it would make even the well-formed case a guess. raw_block is the escape hatch for blocks HPACK will not produce.

scenario_from_json(src)

Parse JSON Lines back to a :class:ScenarioH2Client.

scenario_to_json(scenario)

Serialise to JSON Lines, name on the first HEADER line.