Replay¶
The Server is an http.Handler that replays recorded HTTP interactions. It receives incoming requests, finds a matching Tape via a Matcher, and writes the recorded response.
Basic usage¶
store, _ := httptape.NewFileStore(httptape.WithDirectory("./fixtures"))
srv := httptape.NewServer(store)
ts := httptest.NewServer(srv)
defer ts.Close()
// Requests to ts.URL are matched against recorded tapes
resp, _ := http.Get(ts.URL + "/users/octocat")
Constructor¶
The store parameter is required and must not be nil (panics if nil).
Options¶
WithMatcher¶
Sets the Matcher used to find tapes for incoming requests. If not set, DefaultMatcher() is used, which matches by HTTP method and URL path.
See Matching for all available matchers.
srv := httptape.NewServer(store,
httptape.WithMatcher(httptape.NewCompositeMatcher(
httptape.MethodCriterion{},
httptape.PathCriterion{},
httptape.QueryParamsCriterion{},
)),
)
WithFallbackStatus¶
Sets the HTTP status code returned when no tape matches the incoming request. Defaults to 404.
WithFallbackBody¶
Sets the response body returned when no tape matches. Defaults to "httptape: no matching tape found".
WithOnNoMatch¶
httptape.WithOnNoMatch(func(r *http.Request) {
log.Printf("unmatched request: %s %s", r.Method, r.URL.Path)
})
Sets a callback invoked when no tape matches an incoming request. The callback runs before the fallback response is written. Must be safe for concurrent use.
This is useful for debugging which requests are not being matched during test development.
WithCORS¶
Enables permissive CORS headers (Access-Control-Allow-Origin: *) on every replayed response and short-circuits OPTIONS preflight requests with 204. Intended for local development where a frontend dev server (e.g., localhost:3000) calls the mock backend (e.g., localhost:3001). Opt-in only.
WithDelay¶
Adds a fixed delay before every response. The delay is applied after matching but before writing the response. If the request context is cancelled during the delay (e.g., the client disconnects), ServeHTTP returns immediately without writing. A zero or negative duration is a no-op.
WithReplayTiming¶
Controls whether the server delays responses based on their recorded elapsed time (RecordedResp.ElapsedMS). Defaults to ResponseTimingInstant() (no delay, preserving pre-feature behavior).
Three modes are available:
| Mode | Behavior | Use case |
|---|---|---|
ResponseTimingInstant() | No delay (default) | Unit tests, CI |
ResponseTimingRecorded() | Delay = recorded elapsed time | Realistic replay, back-pressure testing |
ResponseTimingAccelerated(factor) | Delay = elapsed * factor | Fast but proportional; factor < 1 = faster, > 1 = slower |
// Replay with recorded timing -- each response takes as long as the original.
srv, _ := httptape.NewServer(store, httptape.WithReplayTiming(httptape.ResponseTimingRecorded()))
// Replay 10x faster than recorded.
mode, _ := httptape.ResponseTimingAccelerated(0.1)
srv, _ := httptape.NewServer(store, httptape.WithReplayTiming(mode))
Backward compatibility: Pre-feature fixtures (missing elapsed_ms field, or elapsed_ms: 0) incur no delay regardless of mode.
Additive composition with WithDelay: WithReplayTiming composes additively with WithDelay and per-fixture metadata.delay. The existing delay (user-authored "simulate slow API") runs first, then the replay timing delay runs second. The total delay is their sum. For example, if WithDelay is 100ms and the recorded elapsed time is 200ms with ResponseTimingRecorded(), the total delay before the response is written is 300ms.
WithErrorRate¶
Causes a fraction of requests to return 500 Internal Server Error with an X-Httptape-Error: simulated header instead of the recorded response. rate must be between 0.0 and 1.0 inclusive (0.0 disables error simulation, 1.0 fails every request). Panics if rate is outside [0.0, 1.0].
WithReplayHeaders¶
Injects a header into every replayed response, applied after tape matching. Overrides any header with the same key from the recorded tape. May be called multiple times to set multiple headers. Useful for environment-specific tokens, correlation IDs, or cache-control values.
srv := httptape.NewServer(store,
httptape.WithReplayHeaders("X-Request-ID", "test-run-1"),
httptape.WithReplayHeaders("Cache-Control", "no-store"),
)
How replay works¶
For each incoming request, the Server:
- Calls
Store.Listwith an empty filter to retrieve all tapes - Passes the request and all tapes to the
Matcher - If a match is found, writes the tape's response headers, status code, and body
- If no match is found, calls
OnNoMatch(if set) and writes the fallback response
The Content-Length header from the recorded response is removed and re-calculated by net/http to ensure it matches the actual body (which may have been modified by sanitization).
SSE replay¶
When the Server encounters a tape with SSEEvents (i.e., IsSSE() returns true), it switches to SSE replay mode instead of writing a regular body.
How SSE replay works¶
For an SSE tape, ServeHTTP:
- Checks that the
http.ResponseWritersupportshttp.Flusher(required for streaming). If not, returns 500. - Writes the tape's response headers, setting
Content-Type: text/event-stream,Cache-Control: no-cache, andConnection: keep-alive. - Removes
Content-Length(SSE streams are chunked). - Writes the status code and flushes.
- Iterates over
SSEEvents, applying the configuredSSETimingModedelay before each event, then writing and flushing it. - Respects context cancellation (client disconnect) between events.
SSE timing modes¶
Control inter-event timing with WithSSETiming:
// Replay with original recorded timing (default).
srv := httptape.NewServer(store, httptape.WithSSETiming(httptape.SSETimingRealtime()))
// Replay 10x faster -- useful for integration tests that need some timing
// realism without waiting for the full duration.
srv := httptape.NewServer(store, httptape.WithSSETiming(httptape.SSETimingAccelerated(10)))
// Replay instantly -- all events emitted back-to-back with no delay.
// Best for unit tests where timing is irrelevant.
srv := httptape.NewServer(store, httptape.WithSSETiming(httptape.SSETimingInstant()))
| Mode | Behavior | Use case |
|---|---|---|
SSETimingRealtime() | Original inter-event gaps from OffsetMS | UI testing, back-pressure simulation |
SSETimingAccelerated(N) | Gaps divided by N | Integration tests (fast but still sequential) |
SSETimingInstant() | Zero delay between events | Unit tests, CI pipelines |
Example: replaying LLM streaming in tests¶
func TestStreamingChat(t *testing.T) {
store, _ := httptape.NewFileStore(
httptape.WithDirectory("testdata/fixtures"),
)
// Use instant timing so the test completes immediately.
srv := httptape.NewServer(store,
httptape.WithSSETiming(httptape.SSETimingInstant()),
)
ts := httptest.NewServer(srv)
defer ts.Close()
// Point your LLM client at the mock server.
resp, err := http.Post(ts.URL+"/v1/chat/completions",
"application/json",
strings.NewReader(`{"model":"gpt-4","stream":true,"messages":[{"role":"user","content":"Hi"}]}`),
)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.Header.Get("Content-Type") != "text/event-stream" {
t.Fatal("expected SSE content type")
}
// Read events and verify your streaming logic.
scanner := bufio.NewScanner(resp.Body)
var eventCount int
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
eventCount++
}
}
if eventCount == 0 {
t.Error("expected at least one SSE event")
}
}
Using with httptest¶
The most common pattern for tests:
func TestMyAPI(t *testing.T) {
store, err := httptape.NewFileStore(
httptape.WithDirectory("testdata/fixtures"),
)
if err != nil {
t.Fatal(err)
}
srv := httptape.NewServer(store,
httptape.WithOnNoMatch(func(r *http.Request) {
t.Errorf("unmatched: %s %s", r.Method, r.URL.Path)
}),
)
ts := httptest.NewServer(srv)
defer ts.Close()
// Point your code at ts.URL instead of the real API
client := NewAPIClient(ts.URL)
user, err := client.GetUser("octocat")
if err != nil {
t.Fatal(err)
}
// assert on user...
}
Response timing¶
Response timing allows you to replay responses with realistic delays based on the recorded elapsed_ms field. This is controlled by WithReplayTiming (for the Server) and WithCacheReplayTiming (for CachingTransport).
See WithReplayTiming above for configuration details.
For CachingTransport, use WithCacheReplayTiming:
ct := httptape.NewCachingTransport(upstream, store,
httptape.WithCacheReplayTiming(httptape.ResponseTimingRecorded()),
)
The delay is applied before returning the *http.Response, so the caller of RoundTrip perceives the delay as if the upstream had taken that long to respond.
Using as a standalone server¶
The Server implements http.Handler, so it can be used with any HTTP server:
store, _ := httptape.NewFileStore(httptape.WithDirectory("./fixtures"))
srv := httptape.NewServer(store)
log.Println("Mock server on :8081")
http.ListenAndServe(":8081", srv)
For standalone use, consider the CLI which wraps this pattern.
Thread safety¶
Server is safe for concurrent use. All fields are immutable after construction. ServeHTTP can be called from multiple goroutines simultaneously.
Performance note¶
The server calls Store.List on every request, resulting in an O(n) scan over all tapes. This is acceptable for test usage with small fixture sets (up to a few hundred tapes). For large fixture sets, consider scoping fixtures by route.
See also¶
- Template Helpers -- dynamic response generation with
{{...}}expressions - Matching -- control how requests are matched to tapes
- Storage -- where tapes are loaded from
- CLI -- standalone serve mode