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:WaitForRequest —
is 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.