Skip to content

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   │
              └──────────┘
  1. Request arrives at Rift
  2. Rift forwards to real API
  3. Response is recorded as a stub
  4. 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:

{
  "proxy": {
    "to": "https://api.example.com",
    "mode": "proxyOnce"
  }
}

Use case: Creating stable test fixtures.

proxyTransparent

Pure reverse proxy, no recording:

{
  "proxy": {
    "to": "https://api.example.com",
    "mode": "proxyTransparent"
  }
}

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:

# Instead of https://api.example.com
export API_URL=http://localhost:4545

npm test

Every API call is recorded.

Step 3: Export Recorded Stubs

curl "http://localhost:2525/imposters/4545?replayable=true" > recorded.json

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

{
  "predicateGenerators": [{
    "matches": {
      "method": true,
      "path": true
    }
  }]
}

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:

{
  "proxy": {
    "to": "https://api.example.com",
    "addWaitBehavior": true
  }
}

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

{
  "proxy": {
    "to": "https://secure-api.example.com"
  }
}

Rift handles TLS automatically.

Skip Certificate Verification

For self-signed certs:

{
  "proxy": {
    "to": "https://internal.local",
    "cert": null
  }
}

Mutual TLS

{
  "proxy": {
    "to": "https://api.example.com",
    "key": "-----BEGIN RSA PRIVATE KEY-----\n...",
    "cert": "-----BEGIN CERTIFICATE-----\n..."
  }
}

Practical Workflows

Contract Testing

  1. Record production API responses
  2. Run consumer tests against recordings
  3. 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

  1. Use consistent predicate generators: Match enough to be specific, not so much that tests break on irrelevant changes

  2. Sanitize sensitive data: Use a decorate behavior on the proxy response to redact tokens, keys, PII before they are recorded

  3. Version your fixtures: Commit recorded responses to source control

  4. Record in isolation: One service at a time for clearer fixtures

  5. Update regularly: Re-record when APIs change

  6. 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