Skip to content

blackbull.fault_injection.scenario_h1_server

blackbull.fault_injection.scenario_h1_server

Programmable HTTP/1.1 server-side scenario model.

A :class:ScenarioH1Server is a sequence of typed steps that :class:~blackbull.fault_injection.h1_server.H1FaultServer walks in order against a connected client. This is the server-side half of the HTTP/1.1 toolkit: a programmable server that drives a target client through deliberate misbehaviour — a status line delivered a byte at a time, a Content-Length that overstates the body, a chunked body that stops mid-chunk, a connection dropped mid-response.

The symmetric client-side half — a programmable client driving a real server — is :mod:blackbull.fault_injection.scenario_h1. Its vocabulary looks similar and is not reusable here: ReadResponse and SendBytes name the other end of the wire. Two vocabularies, because there are two roles.

Everything a scenario emits is raw bytes, deliberately. There is no typed SendResponse step, because a response object would be built by the production response path, and a fault server that shares the production serialiser cannot produce a fault that serialiser has. The HTTP/2 half made the same choice for the same reason (it carries its own frame encoder rather than calling FrameBase.save()).

Abort dataclass

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

Terminal: later steps short-circuit.

CloseGracefully dataclass

Close cleanly (FIN) after whatever has been written.

Terminal. The difference from :class:Abort is what the client sees — an orderly EOF mid-body rather than a reset — and clients do not always treat the two alike, which is the point of having both.

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

EndHeaders dataclass

The blank line that ends the head. Omit it to stage a head that never finishes.

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 answers a different question from :class:WaitForRequestis the client under test behaving as this scenario assumes? A scenario that stages a fault against Expect: 100-continue is testing nothing at all if the client never sent that header, and without this the run would look like a pass.

A mismatch is recorded, not raised: ScenarioH1ServerResult.expectations collects one (match, matched) pair per step, so a scenario reports what it assumed alongside what it got.

Deliberately not called WaitForRequest(match=...) even though the grammar is the same: reusing a name for a different meaning is the thing the 107+108 consistency sweep was run to prevent.

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.

ScenarioH1Server dataclass

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

steps is a tuple, matching :class:ScenarioH2 — a frozen dataclass holding a mutable list is a frozen container of mutable contents, and the HTTP/2 half settled the question first.

ScenarioH1ServerResult dataclass

What the executor observed while running a scenario.

Field names match :class:~blackbull.fault_injection.scenario_h2.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.

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

SendHeader dataclass

Emit one header line.

fold writes it as an obs-fold continuation (RFC 9112 §5.2, which deprecates the form and requires a recipient to reject or normalise it) — expressible before only as a hand-built byte string.

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

SendStatusLine dataclass

Emit a status line, field by field.

Added by the 107+108 consistency sweep. Nothing validates: a status line with no reason phrase, an impossible version, or a three-digit code that is not a status are all faults worth staging, and a typed step that refused them would be useless here. What it buys over raw bytes is that the shape is legible — a reader sees which field the scenario is bending.

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

StepOpH1Server

Bases: str, Enum

Tag used by the JSON serialiser.

WaitForRequest dataclass

Block until a request head arrives, optionally one that matches.

A scenario that writes before the request is read is testing a different thing — an unsolicited response — and can simply omit this step. On timeout expiry the executor records the miss and proceeds, matching WaitForClientFrame on the HTTP/2 side.

With match set, heads that do not match are read and skipped, and the step keeps waiting — the same filter-over-a-stream meaning WaitForClientFrame has. On HTTP/1.1 that stream is a pipeline (RFC 9112 §9.3.2), so this is how a scenario misbehaves at one request among several: answer the GET normally, break on the POST.

Skipping desyncs the connection, and that is not hidden. HTTP/1.1 responses are positional — a skipped request is one the scenario can no longer answer, so everything after it is off by one. On HTTP/2 the equivalent is harmless because streams are independent; here it is a fault in its own right, staged deliberately or not at all. The count lands on ScenarioH1ServerResult.requests_skipped so a scenario author reads it from the result rather than deducing it.

Use :class:ExpectRequest when the question is "did the client send what this scenario assumes" — that one reads a single head and skips nothing.

encode_chunk(step)

One chunk, with the size line free to disagree with the data.

encode_chunked_terminator(step)

The zero chunk, plus any trailer fields.

encode_header(step)

One header line, or an obs-fold continuation of the previous one.

encode_status_line(step)

HTTP/1.1 200 OK\r\n, or whatever the step says instead.

parse_request_head(head)

Split a request head into the fields :func:request_matches reads.

Deliberately lenient: this parses what a client under test actually sent, including things a conforming parser would reject, because a scenario may well be waiting for exactly that. A malformed request line yields empty strings rather than raising — a scenario matching on method simply will not match it.

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.

scenario_from_json(src)

Parse JSON Lines back to a :class:ScenarioH1Server.

scenario_to_json(scenario)

Serialise scenario to JSON Lines (one step per line).

The name sits on the first line under the op HEADER, so the file is one line-oriented stream with no out-of-band metadata — the convention :func:blackbull.fault_injection.scenario_h2.scenario_to_json set.

Payloads are hex rather than base64 or an escaped string: a fault scenario's bytes are frequently not valid UTF-8 and are meant to be read by a human comparing them against a packet capture.