How Middleware Works
The middleware pipeline follows a simple pattern:The HttpMiddleware Interface
All middleware implements a single interface:The BaseMiddleware Abstract Class
For most middleware, you do not need to implement the fullhandle() method. The BaseMiddleware class provides a template with overridable hooks:
Registering Middleware
On an Existing Client
TheHttpClient is immutable. withMiddleware() returns a new client with the middleware appended:
Via the Builder
The builder collects middleware before creating the client:Built-in Middleware
The package ships with several production-ready middleware components.RetryMiddleware
Automatically retries failed requests with exponential backoff and jitter:Retry-After header when present.
By default, retries are limited to idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, and TRACE). This prevents automatic replay of side effects from POST and PATCH. If the endpoint provides its own idempotency guarantee, explicitly opt in and pair the policy with IdempotencyMiddleware:
Retry-After is advisory and is bounded by maxDelayMs, so a server cannot make a worker sleep for an arbitrary number of hours. An idempotency key generated by IdempotencyMiddleware is reused for every attempt of the same request regardless of whether it is placed before or after RetryMiddleware.
The jitter options are:
none— exact exponential backofffull— random delay between 0 and the calculated backoffequal— half the backoff plus a random portion of the other half
CircuitBreakerMiddleware
Prevents repeated calls to a failing service by tracking failures per host:- Closed — requests flow normally; failures are counted.
- Open — after
failureThresholdfailures, the circuit opens and all requests throwCircuitBreakerOpenExceptionforopenForSecseconds. - Half-open — after the timeout, a limited number of probe requests are allowed. If
successThresholdprobes succeed, the circuit closes. If any fail, it reopens.
IdempotencyMiddleware
Attaches a unique idempotency key to requests, which prevents duplicate processing when retries occur:EventSourceMiddleware
Parses server-sent event streams into clean payloads. See Streaming Responses for usage details.RecordReplayMiddleware
Use the immutable named constructors to record HTTP interactions and replay them without contacting the network:RecordingNotFoundException; a mismatch, exhausted session, corrupt payload, or
unsupported cassette version throws its own typed cassette exception. None of
these paths calls the next handler. If a live miss is genuinely intended, make
that choice visible at the call site:
Accept/
Content-Type headers. JSON bodies with an explicit JSON content type are
canonicalized recursively: object key order and whitespace do not matter, while
array order and scalar types do. Other bodies, including binary bodies, are
matched byte-for-byte. Authorization and transport-only options are excluded.
One middleware instance is one ordered cassette session. Repeated identical
requests replay their recorded responses in order; a request mismatch does not
advance the cursor. For non-filesystem stores, inject CassetteStore through
recordWith() or replayWith(), and customize matching or sanitization through
RecordReplayPolicy.
For streamed responses, recording returns the first upstream chunk immediately
and publishes only after natural completion. Chunks are stored as binary-safe
base64 frames and replayed one at a time through a one-shot stream; empty chunks,
embedded newlines, NUL bytes, invalid UTF-8, interrupted streams, and upstream
failures are handled explicitly. Sanitization may change chunk boundaries when a
credential spans chunks, but it must preserve the concatenated logical body.
The default cassette layout keeps UTF-8 metadata in JSON and payload bytes in
separate files. Existing pre-v1 single-file recordings are read only through the
isolated compatibility adapter used by the examples boot path; new cassettes use
the versioned layout. Prefer refreshing/migrating old fixtures before sharing
them.
Privacy still requires review: automatic sanitization targets common credentials
in headers, URLs, response metadata, and known body fields. Prompts, model
outputs, PII, and provider-specific secrets may remain in payload files. Treat
cassettes and diagnostic events as sensitive application data before committing
or publishing them.
Recordings are application data, not automatically safe test data. Built-in
sanitization masks common credentials in request/response metadata and streamed
payload fields, but prompts, model outputs, PII, and provider-specific secrets
may still be present. Review fixtures before committing or sharing them; replay
and record/replay events should be treated as sensitive diagnostic material.
Response Decoration
For middleware that needs to transform streamed responses, useBaseResponseDecorator to wrap the stream with a transformation function:
HttpResponse with a TransformStream that applies your function to each chunk. The original response is not modified.
Writing Custom Middleware
Here is a practical example of authentication middleware:Middleware Order
The order you register middleware determines the execution flow. Middleware registered first is the outermost layer:- Request flow: Logging -> Retry -> Auth -> Driver
- Response flow: Driver -> Auth -> Retry -> Logging
Middleware Stack API
TheMiddlewareStack class provides fine-grained control over the middleware collection:
See Also
- Streaming Responses — EventSourceMiddleware for SSE parsing.
- Custom Clients — create drivers that middleware wraps around.