Error Injection
Test your application's error handling with one-shot errors, stream truncation, and timed disconnects. aimock provides three mechanisms for simulating failures.
One-Shot Errors
Queue an error that fires on the next request and auto-removes itself. Useful for testing retry logic.
const mock = new LLMock();
await mock.start();
mock.onMessage("hello", { content: "Hi!" });
// Queue a 429 rate limit error for the next request
mock.nextRequestError(429, {
message: "Rate limit exceeded",
type: "rate_limit_error",
});
// First request → 429 error
const res1 = await fetch(`${mock.url}/v1/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "gpt-4",
messages: [{ role: "user", content: "hello" }],
}),
});
expect(res1.status).toBe(429);
// Second request → normal response (error auto-removed)
const res2 = await fetch(`${mock.url}/v1/chat/completions`, { /* same */ });
expect(res2.status).toBe(200);
Stream Truncation
Abort a streaming response after a specific number of SSE chunks. Tests that your application handles partial streams gracefully.
mock.on(
{ userMessage: "long story" },
{ content: "This is a very long response that will be cut short" },
{ truncateAfterChunks: 3 } // Abort after 3 SSE chunks
);
Model Length Stops
Model misbehavior tests malformed tool arguments,
refusals, and other response failures on the openai-chat wire. The
stop-length-mid-tool fault cuts the selected tool call's argument string and
omits later tool calls.
In streaming mode, the response ends with finish_reason: "length", an
optional usage chunk, and [DONE]. In non-streaming mode, the response
contains the partial arguments and the same length finish reason. The HTTP response
completes normally, but the tool arguments are incomplete.
Use truncateAfterChunks or disconnectAfterMs to test an
interrupted connection. These settings can also interrupt a semantic fault before its
clean terminal reaches the client. See
chaos precedence when combining faults.
Timed Disconnect
Disconnect after a specified number of milliseconds. Simulates network timeouts and connection drops.
mock.on(
{ userMessage: "slow" },
{ content: "This response will never complete" },
{ disconnectAfterMs: 100 } // Kill connection after 100ms
);
Error Fixtures in JSON
{
"fixtures": [
{
"match": { "userMessage": "error-test" },
"response": {
"error": {
"message": "Rate limited",
"type": "rate_limit_error"
},
"status": 429
}
},
{
"match": { "userMessage": "partial" },
"response": { "content": "This gets cut off" },
"truncateAfterChunks": 2
},
{
"match": { "userMessage": "timeout" },
"response": { "content": "Never finishes" },
"disconnectAfterMs": 50
}
]
}
Interruption Behavior
-
truncateAfterChunks— counts SSE data lines sent; aborts on the Nth chunk -
disconnectAfterMs— starts a timer when the response begins; kills the connection when it fires - If both are set, whichever fires first wins
-
Interrupted requests are recorded in the journal with
response.interrupted: trueandresponse.interruptReason -
A client that disconnects is recorded the same way, with
response.interruptReason: "client aborted"(see Client Disconnects)
Client Disconnects
When the client hangs up before the reply is complete, aimock stops delivery. It sends no more chunks and skips the remaining waits. This applies to these endpoints, streaming and non-streaming:
- OpenAI Chat Completions, Anthropic Messages and OpenAI Responses
- Gemini and Vertex AI, and Gemini Interactions
- Cohere v2 chat
- Bedrock InvokeModel and Converse, and their streaming variants
- Ollama
/api/chatand/api/generate - Audio transcription streams
- WebSocket: OpenAI Responses, OpenAI Realtime and Gemini Live
On WebSocket, a client that closes its TCP connection without sending a close frame is also a disconnect. aimock closes the connection at once (code 1006, "Connection lost"), stops the turn and journals it.
The journal entry records how the reply ended:
-
response.interrupted: trueandresponse.interruptReason: "client aborted"when the client left. -
On HTTP,
response.status: 0when no byte of the reply reached the client. Otherwise the status stays the one aimock sent. -
response.interruptReason: "disconnect"when aimock tore the response down itself, for example with the chaosdisconnectaction or a server-side destroy. This is not blamed on the client. -
Fixture interruptions (
truncateAfterChunks,disconnectAfterMs) keep their own reasons.
nextRequestError() is one-shot: it fires once and auto-removes itself. For
persistent error fixtures, use addFixture() with an error response.
Accepted status codes: integers 200–999. The same range applies
to nextRequestError(), POST /__aimock/error and a
fixture’s response.status. 600–999 is unassigned rather than
invalid — Node sends it and a client sees it — so it is allowed on purpose.
Outside the range the status could never reach the client: 1xx cannot terminate a
response and would hang the request until the client times out, and anything below 100,
above 999 or fractional makes Node throw, which loses the injected error and returns a
generic 500. nextRequestError() throws a RangeError at the
call site; the control API returns 400; a fixture fails validation.
See also: Chaos Testing — for probabilistic failure injection. Chaos testing adds configurable rates for dropped requests (500), malformed JSON responses, and disconnects that trigger based on probability rather than deterministic fixture matching. Use error injection for specific, reproducible failure scenarios; use chaos testing for resilience testing under unpredictable conditions.