Recording and Replaying API Traffic with Proxy Mode¶
Create mocks from real API behavior automatically
Writing mock responses by hand is tedious and error-prone. What if you could: - Point Rift at a real API - Run your tests - Have Rift record every response - Replay those responses offline forever?
That's exactly what Proxy Mode does. Let's explore.
How Proxy Mode Works¶
┌─────────┐ ┌──────────┐ ┌─────────────┐
│ Test │───▶│ Rift │───▶│ Real API │
│ Suite │◀───│ (Proxy) │◀───│ │
└─────────┘ └──────────┘ └─────────────┘
│
▼
┌──────────┐
│ Recorded │
│ Stubs │
└──────────┘
- Request arrives at Rift
- Rift forwards to real API
- Response is recorded as a stub
- Future matching requests use the recorded stub
Proxy Modes¶
proxyAlways¶
Always forward to real API, record every response:
{
"port": 4545,
"stubs": [{
"responses": [{
"proxy": {
"to": "https://api.example.com",
"mode": "proxyAlways"
}
}]
}]
}
Use case: Building a comprehensive mock from varied traffic.
proxyOnce¶
Forward first request, replay recorded response for subsequent matching requests:
Use case: Creating stable test fixtures.
proxyTransparent¶
Pure reverse proxy, no recording:
Use case: Debugging, traffic inspection.
The Recording Workflow¶
Step 1: Create a Recording Proxy¶
curl -X POST http://localhost:2525/imposters \
-H "Content-Type: application/json" \
-d '{
"port": 4545,
"protocol": "http",
"name": "API Recorder",
"stubs": [{
"responses": [{
"proxy": {
"to": "https://api.example.com",
"mode": "proxyOnce",
"predicateGenerators": [{
"matches": {
"method": true,
"path": true,
"query": true
}
}]
}
}]
}]
}'
Step 2: Run Your Tests¶
Point your tests at the proxy:
Every API call is recorded.
Step 3: Export Recorded Stubs¶
The ?replayable=true parameter formats output for reloading.
Step 4: Use Recorded Mocks¶
# Load recorded mocks (no proxy, pure playback)
curl -X DELETE http://localhost:2525/imposters/4545
curl -X POST http://localhost:2525/imposters \
-H "Content-Type: application/json" \
-d @recorded.json
Now your tests run offline with recorded responses.
Predicate Generators¶
Control how Rift matches recorded responses to new requests:
Match Everything¶
{
"predicateGenerators": [{
"matches": {
"method": true,
"path": true,
"query": true,
"headers": true,
"body": true
}
}]
}
Very specific — each unique request gets its own stub.
Match Method and Path Only¶
More flexible — ignores query strings and headers.
Match with JSON Body¶
{
"predicateGenerators": [{
"matches": {
"method": true,
"path": true
}
}, {
"matches": {
"body": true
},
"jsonpath": { "selector": "$.action" }
}]
}
Match based on specific JSON fields.
Path Rewriting¶
Modify paths before forwarding:
{
"proxy": {
"to": "https://api.example.com",
"pathRewrite": {
"from": "/mock-api",
"to": "/api"
}
}
}
Request to /mock-api/users forwards to /api/users.
Use Case: API Versioning¶
{
"stubs": [{
"predicates": [{ "startsWith": { "path": "/api/v2" } }],
"responses": [{
"proxy": {
"to": "https://legacy.example.com",
"pathRewrite": {
"from": "/api/v2",
"to": "/api/v1"
}
}
}]
}]
}
Route v2 requests to v1 backend during migration.
Header Injection¶
Add headers to proxied requests:
{
"proxy": {
"to": "https://api.example.com",
"injectHeaders": {
"X-Forwarded-By": "Rift",
"Authorization": "Bearer internal-token",
"X-Request-ID": "${uuid}"
}
}
}
Useful for: - Adding authentication - Tracking proxied requests - Overriding client headers
Response Transformation¶
Behaviors on the proxy response¶
A _behaviors (or behaviors) block on a proxy response runs on the upstream's response before
anything is recorded, as in Mountebank. The client, the recording and the generated stub all get the
transformed response:
{
"proxy": { "to": "https://api.example.com", "mode": "proxyOnce" },
"_behaviors": {
"decorate": "function(request, response) { \
// Sanitize sensitive data before it is recorded \
if (response.body.apiKey) { \
response.body.apiKey = 'REDACTED'; \
} \
}"
}
}
The generated stub holds the transformed body, not the behaviors, so a replay does not run them
again. If a behavior fails, nothing is recorded and the next matching request goes upstream again.
A decorate is a script, so it needs --allowInjection.
addDecorateBehavior¶
Add a decorate behavior to the stub the proxy generates. It runs when that stub replays, not on
the live proxied response, as in Mountebank:
{
"proxy": {
"to": "https://api.example.com",
"addDecorateBehavior": "function(request, response) { \
response.headers['X-Proxied-By'] = 'Rift'; \
return response; \
}"
}
}
addWaitBehavior¶
Record the upstream's observed latency on the generated stub, so replay reproduces it as a wait
behavior. It is a boolean (default false), as in Mountebank, and does not delay the proxied
response itself:
Combining Proxy with Static Stubs¶
Mix recorded and static responses:
{
"port": 4545,
"stubs": [
{
"predicates": [{ "equals": { "path": "/health" } }],
"responses": [{
"is": { "statusCode": 200, "body": "OK" }
}]
},
{
"predicates": [{ "equals": { "path": "/api/config" } }],
"responses": [{
"is": {
"statusCode": 200,
"body": { "feature_flags": { "new_ui": true } }
}
}]
},
{
"responses": [{
"proxy": {
"to": "https://api.example.com",
"mode": "proxyOnce"
}
}]
}
]
}
Static stubs for controlled endpoints, proxy for everything else.
HTTPS Proxying¶
Basic HTTPS¶
Rift handles TLS automatically.
Skip Certificate Verification¶
For self-signed certs:
Mutual TLS¶
{
"proxy": {
"to": "https://api.example.com",
"key": "-----BEGIN RSA PRIVATE KEY-----\n...",
"cert": "-----BEGIN CERTIFICATE-----\n..."
}
}
Practical Workflows¶
Contract Testing¶
- Record production API responses
- Run consumer tests against recordings
- Catch breaking changes when recordings don't match
# Record from staging
PROXY_TARGET=https://staging.api.com ./record-fixtures.sh
# Test against recordings
npm test
# Compare with production
PROXY_TARGET=https://prod.api.com ./record-fixtures.sh
diff staging-fixtures.json prod-fixtures.json
Offline Development¶
# Record once
docker run -v $(pwd):/data zainalpour/rift-proxy \
--configfile /data/proxy-config.json
# (run tests to record)
# Export
curl http://localhost:2525/imposters/4545?replayable=true > fixtures.json
# Develop offline forever
docker run -v $(pwd):/data zainalpour/rift-proxy \
--configfile /data/fixtures.json
CI/CD with Recorded Fixtures¶
# .github/workflows/test.yml
jobs:
test:
steps:
- uses: actions/checkout@v4
- name: Start mock server
run: |
docker run -d -p 2525:2525 -p 4545:4545 \
-v ${{ github.workspace }}/fixtures:/fixtures \
zainalpour/rift-proxy \
--configfile /fixtures/api-mocks.json
- name: Run tests
run: npm test
env:
API_URL: http://localhost:4545
Save Command¶
Export imposters using the CLI:
# Save all imposters
rift save --savefile mocks.json
# Save without proxy stubs (pure responses)
rift save --savefile mocks.json --remove-proxies
The --remove-proxies flag removes proxy configurations, leaving only recorded responses.
Both rift save and GET /imposters/:port?replayable=true export the stubs the imposter is serving
now — including those a proxy recorded or the stub routes added after creation. Before Rift 0.18.1
the replayable views were built from the config the imposter was created with, so recorded stubs
were missing from them.
Best Practices¶
-
Use consistent predicate generators: Match enough to be specific, not so much that tests break on irrelevant changes
-
Sanitize sensitive data: Use a
decoratebehavior on the proxy response to redact tokens, keys, PII before they are recorded -
Version your fixtures: Commit recorded responses to source control
-
Record in isolation: One service at a time for clearer fixtures
-
Update regularly: Re-record when APIs change
-
Use proxyOnce for stability: Avoid test flakiness from API changes
What's Next?¶
You've got great mocks. Now let's deploy them:
Next Post: Production-Ready Mocking — Docker, Kubernetes, and CI/CD
How do you manage API fixtures in your projects? Share your workflows!
Tags: #ProxyMode #APIRecording #ContractTesting #MockServer #Rift #Testing