Proxy Mode¶
Proxy mode forwards requests to a real upstream, caches responses in a two-tier system, and falls back to cached responses when the upstream is unavailable. It combines recording and replay into a single transparent http.RoundTripper.
When to use proxy vs record vs serve¶
| Mode | Use case |
|---|---|
record | Capture a fixed set of fixtures from an upstream API. Stop recording, then switch to serve. |
serve | Replay a known set of fixtures. No upstream needed. Deterministic and offline. |
proxy | Develop against a live upstream with automatic fallback. Best for frontend dev where the backend may be flaky, slow, or occasionally offline. |
Use proxy when you want the benefits of a live upstream (fresh data, real behavior) with the safety net of cached responses. Use record + serve when you want fully deterministic, offline replay.
How it works¶
The Proxy is an http.RoundTripper that implements a two-tier caching strategy:
Request
|
v
Forward to upstream
|
+-- Success --> Save raw tape to L1 (memory)
| Save redacted tape to L2 (disk)
| Return real response
|
+-- Failure --> Look up L1 (raw, in-session cache)
|
+-- L1 hit --> Return cached response (X-Httptape-Source: l1-cache)
|
+-- L1 miss --> Look up L2 (redacted, persistent cache)
|
+-- L2 hit --> Return cached response (X-Httptape-Source: l2-cache)
|
+-- L2 miss --> Return original error
L1: In-memory cache (raw)¶
- Backed by a
MemoryStore - Contains unsanitized (raw) responses -- best fidelity within a session
- Lost when the process exits
- Checked first during fallback (lowest latency, best data quality)
L2: Disk cache (redacted)¶
- Backed by a
FileStore - Contains redacted responses (secrets stripped, PII faked)
- Persists across restarts
- Safe to commit to version control
- Checked second during fallback
X-Httptape-Source header¶
When a response comes from cache, the proxy adds an X-Httptape-Source header:
| Value | Meaning |
|---|---|
l1-cache | Response came from in-memory cache (raw, current session) |
l2-cache | Response came from disk cache (redacted, persistent) |
| (absent) | Response came from the real upstream |
This makes it easy to see in browser dev tools or logs whether a response is live or cached.
Go API¶
Basic usage¶
l1 := httptape.NewMemoryStore()
l2, _ := httptape.NewFileStore(httptape.WithDirectory("./cache"))
proxy := httptape.NewProxy(l1, l2)
client := &http.Client{Transport: proxy}
resp, err := client.Get("https://api.example.com/users")
// If upstream is reachable: real response, cached to L1 + L2
// If upstream is down: cached response from L1 or L2
With redaction¶
sanitizer := httptape.NewPipeline(
httptape.RedactHeaders("Authorization", "Cookie"),
httptape.RedactBodyPaths("$.password"),
httptape.FakeFields("my-seed", "$.user.email"),
)
proxy := httptape.NewProxy(l1, l2,
httptape.WithProxySanitizer(sanitizer),
)
The redaction pipeline is applied only to L2 writes. L1 always stores raw responses for best within-session fidelity.
Constructor¶
Both l1 and l2 must be non-nil. Panics on nil stores.
Options¶
| Option | Signature | Default |
|---|---|---|
WithProxyTransport | WithProxyTransport(rt http.RoundTripper) | http.DefaultTransport |
WithProxySanitizer | WithProxySanitizer(s Sanitizer) | no-op Pipeline |
WithProxyMatcher | WithProxyMatcher(m Matcher) | DefaultMatcher() |
WithProxyRoute | WithProxyRoute(route string) | "" |
WithProxyOnError | WithProxyOnError(fn func(error)) | nil |
WithProxyFallbackOn | WithProxyFallbackOn(fn func(error, *http.Response) bool) | transport errors only |
Fallback on 5xx¶
By default, the proxy only falls back on transport errors (connection refused, DNS failure, timeout). To also fall back on 5xx responses from the upstream:
proxy := httptape.NewProxy(l1, l2,
httptape.WithProxyFallbackOn(func(err error, resp *http.Response) bool {
if err != nil {
return true
}
return resp != nil && resp.StatusCode >= 500
}),
)
CLI¶
httptape proxy --upstream https://api.example.com \
--fixtures ./cache \
--config redact.json \
--port 8081
| Flag | Default | Description |
|---|---|---|
--upstream | (required) | Upstream URL (e.g., https://api.example.com) |
--fixtures | (required) | Path to fixture directory for L2 cache |
--config | (none) | Path to redaction config JSON (applied to L2 writes only) |
--port | 8081 | Listen port |
--cors | false | Enable CORS headers |
--fallback-on-5xx | false | Also fall back on 5xx responses from upstream |
The L1 cache is always an in-memory store managed internally. The --fixtures directory is the L2 (persistent, redacted) cache.
Docker¶
docker run --rm \
-v ./cache:/fixtures \
-v ./redact.json:/config/config.json:ro \
-p 8081:8081 \
ghcr.io/vibewarden/httptape:latest \
proxy --upstream https://api.example.com \
--fixtures /fixtures \
--config /config/config.json \
--port 8081
Docker Compose: frontend + proxy¶
services:
api-proxy:
image: ghcr.io/vibewarden/httptape:latest
command:
- proxy
- --upstream
- https://api.staging.example.com
- --fixtures
- /fixtures
- --config
- /config/config.json
- --port
- "3001"
- --cors
ports:
- "3001:3001"
volumes:
- ./cache:/fixtures
- ./redact.json:/config/config.json:ro
frontend:
build:
context: ./frontend
ports:
- "3000:3000"
environment:
VITE_API_URL: http://localhost:3001
depends_on:
- api-proxy
When the staging API is up, the frontend gets live data. When it is down, the proxy serves cached responses transparently.
Example: frontend development with fallback¶
A typical frontend development workflow using proxy mode:
- Start the proxy pointing at your staging/development API
- Build your UI -- all requests go through the proxy to the real API
- Responses are cached to disk (L2) with secrets redacted
- If the API goes down (network issue, deployment, VPN disconnect), the proxy serves cached responses
- When the API comes back, fresh responses are served and the cache is updated
# Start proxy with redaction
httptape proxy \
--upstream https://staging-api.example.com \
--fixtures ./api-cache \
--config redact.json \
--port 3001 \
--cors
# In another terminal, start your frontend
cd frontend && npm run dev
# Frontend at localhost:3000 calls proxy at localhost:3001
Check the X-Httptape-Source response header in browser dev tools to see whether each response is live or cached.
Thread safety¶
Proxy is safe for concurrent use by multiple goroutines. RoundTrip may be called from multiple goroutines simultaneously.