blackbull.fault_injection.h1_server¶
blackbull.fault_injection.h1_server
¶
A deliberately misbehaving HTTP/1.1 server.
The HTTP/1.1 half of the fault-injection grid's server direction — the cell the toolkit did not have. Point your own HTTP/1.1 client at it and assert that the client survives what a real server can do wrong::
scenario = ScenarioH1Server(steps=[
WaitForRequest(),
SendRawBytes(b'HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\nshort'),
CloseGracefully(),
])
async with H1FaultServer(scenario) as srv:
with pytest.raises(...):
await my_client.get(f'http://{srv.host}:{srv.port}/')
It assembles every byte itself. Nothing here imports
blackbull.server.sender or blackbull.server.response, and that is
load-bearing rather than incidental: a fault server built on the
production send path cannot emit a fault the production send path has, so
the one bug class it is least able to find is the one in the code it
shares. The HTTP/2 half made the same choice — it carries its own frame
encoder rather than calling FrameBase.save(). There is a test for it
(tests/unit/test_fault_injection_h1_server.py::TestTheBreakerIsIndependent)
because the property is invisible until the day it matters.
Two safety locks, the same pair :class:H2FaultServer carries and both
in scope for security reports per SECURITY.md: it refuses to start in
a production context, and it refuses a non-loopback bind without an
explicit allow_remote=True.
H1FaultServer
¶
Serves one :class: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.
H1FaultServerError
¶
Bases: RuntimeError
Raised when the fault server refuses to run.