Proxy Mode

Proxy mode forwards requests to real servers and optionally records responses for playback. This is useful for creating mocks from real API behavior.


Basic Proxy

Forward all requests to a backend server:

{
  "port": 4545,
  "protocol": "http",
  "stubs": [{
    "responses": [{
      "proxy": {
        "to": "https://api.example.com"
      }
    }]
  }]
}

Proxy Modes

proxyAlways

Always forward requests; record a new stub for each unique request:

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

Use when: Building a comprehensive mock from varied requests.

proxyOnce

Forward the first request, then replay the recorded response:

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

Use when: Recording a fixed set of responses for offline testing.

proxyTransparent

Forward without recording (pure reverse proxy):

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

Use when: Acting as a transparent proxy without mocking.


Predicate Generators

Control how requests are matched when generating stubs:

{
  "proxy": {
    "to": "https://api.example.com",
    "predicateGenerators": [{
      "matches": {
        "method": true,
        "path": true,
        "query": true
      }
    }]
  }
}

Available Fields

Field Description
method Match HTTP method
path Match request path
query Match query parameters
headers Match request headers
body Match request body

Selective Matching

Match only specific aspects:

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

This generates stubs that match method and path, ignoring query and body.

Case Sensitivity

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

Generation Failures

A predicate generator can also be an inject function that builds predicates in JavaScript. If that generation fails — the script throws, returns something other than a predicate array, the script pool is unavailable, or it exceeds the script timeout — Rift does not record a stub. Recording a stub with the empty/partial predicate list would produce a match-all stub that shadows every future request, so the failure is surfaced instead of hidden:

  • no stub is recorded for that request (the proxied response is still returned to the client), and
  • the proxied response carries an x-rift-generator-error header whose value names the failure — script-error, invalid-output, pool-failure, timeout, or task-panic.

A generator that legitimately returns an empty array ([]) is not a failure — it records a match-all stub as before. Only genuine generation failures skip recording and set the header.


Recording Workflow

Step 1: Create Recording Proxy

curl -X POST http://localhost:2525/imposters \
  -H "Content-Type: application/json" \
  -d '{
    "port": 4545,
    "protocol": "http",
    "stubs": [{
      "responses": [{
        "proxy": {
          "to": "https://api.example.com",
          "mode": "proxyOnce",
          "predicateGenerators": [{
            "matches": { "method": true, "path": true, "query": true }
          }]
        }
      }]
    }]
  }'

Step 2: Run Your Tests

# Run application tests against the proxy
npm test
# or
pytest

Step 3: Export Recorded Stubs

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

Step 4: Use Recorded Mocks

# Delete proxy imposter
curl -X DELETE http://localhost:2525/imposters/4545

# Load recorded mocks
curl -X POST http://localhost:2525/imposters \
  -H "Content-Type: application/json" \
  -d @recorded.json

Flat Response Form

Rift accepts a stub response in flat formstatusCode, headers, and body at the top level of the response object, with no is wrapper — and serves it identically to the wrapped form. This matters when replaying recorded or externally-generated mocks that emit flat responses:

// Flat form (no "is" wrapper)  accepted and served as a 200 with the body
{ "statusCode": 200, "body": "recorded" }

// Equivalent to the canonical wrapped form
{ "is": { "statusCode": 200, "body": "recorded" } }

statusCode defaults to 200 when omitted. If both a top-level field and an explicit is are present, is takes precedence.


Modifying Proxied Responses

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 proxy recording and the stub predicateGenerators generates all get the transformed response:

{
  "proxy": { "to": "https://api.example.com", "mode": "proxyOnce" },
  "_behaviors": {
    "decorate": "function(request, response) { response.headers['X-Proxied-By'] = 'Rift'; }"
  }
}
  • The generated stub holds the transformed body, not the behaviors, so a replay does not run them again. It carries only what addWaitBehavior and addDecorateBehavior add.
  • A proxyOnce replay served from the recording is likewise not transformed a second time.
  • A wait delays the live response, after the upstream’s latency is measured, so it is not counted in what addWaitBehavior records.
  • If a behavior fails, nothing is recorded and the next matching request goes upstream again. The failed response is served as usual: lenient by default (with the x-rift-<behavior>-error header), or a 500 under strictBehaviors (a 504 with x-rift-script-timeout when a decorate runs out of time). Recording it would replay a response the configuration did not ask for.
  • A body that is not UTF-8 reaches the behaviors base64-encoded, as a _mode: "binary" body does, and is decoded before it is served.
  • The upstream’s content-length is dropped when behaviors run, since they can change the body’s length; the served length is computed from the body.

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
  }
}

Like predicateGenerators, enabling it causes a stub to be generated from the proxied response. The live proxied response also carries an x-rift-proxy-latency: <ms> header when it is set.


HTTPS Proxy

Proxy to HTTPS Backend

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

The origin’s certificate is verified against the operating system trust store, and the connection offers http/1.1 only. A TLS failure on this hop (an untrusted issuer, a name mismatch) answers the client 502 with an x-rift-proxy-error: true header and a Proxy error: … body naming the upstream. The TLS cause itself, e.g. invalid peer certificate: UnknownIssuer, goes to the server log only.

Trusting a Private CA

Rift verifies outbound TLS against the operating system trust store. An origin issued by an internal CA — a corporate API gateway — needs that CA supplied:

rift --upstream-ca-file /etc/rift/corp-ca.pem
# or
RIFT_UPSTREAM_CA_FILE=/etc/rift/corp-ca.pem rift

The anchor is appended to the OS store, so public origins keep working. The file may hold several certificates, and a missing or unusable one stops startup. This applies to every outbound call Rift makes: proxy stub upstreams, --configfile https://…, and (on the standalone binary) the intercept listener’s WebSocket passthrough. Embedders pass upstreamCaFile / upstreamCaPem to rift_serve_admin instead. For a worked example, see TLS/HTTPS → Trusting a Private CA.

SSL_CERT_FILE / SSL_CERT_DIR are also honoured, but they replace the trust store rather than adding to it — pointing SSL_CERT_FILE at a lone private CA silently drops every public root. Use --upstream-ca-file unless you are supplying a complete bundle.

Skipping Verification (development only)

rift --upstream-tls-skip-verify      # or RIFT_UPSTREAM_TLS_SKIP_VERIFY=true

Accepts any certificate and logs a warning. Prefer --upstream-ca-file: a recording proxy with verification disabled will faithfully record MITM’d traffic.

key, cert, passphrase and ciphers on a proxy response are accepted for Mountebank compatibility and are not honoured — they are dropped on load and do not appear when the imposter is read back. Rift cannot present a client certificate to an upstream, and its outbound trust is process-wide, configured by the two flags above.


Header Manipulation

Inject Headers

Add headers to proxied requests:

{
  "proxy": {
    "to": "https://api.example.com",
    "injectHeaders": {
      "X-Forwarded-By": "Rift",
      "Authorization": "Bearer token123"
    }
  }
}

Path Rewriting

Modify the request path before forwarding to the backend:

{
  "proxy": {
    "to": "https://api.example.com",
    "pathRewrite": {
      "from": "/api/v2",
      "to": "/api/v1"
    }
  }
}

Use Cases

Version Migration:

Route v2 API calls to v1 backend during migration:

{
  "port": 4545,
  "protocol": "http",
  "stubs": [{
    "predicates": [{ "startsWith": { "path": "/api/v2" } }],
    "responses": [{
      "proxy": {
        "to": "https://legacy-api.example.com",
        "pathRewrite": {
          "from": "/api/v2",
          "to": "/api/v1"
        }
      }
    }]
  }]
}

Strip Prefix:

Remove a prefix from paths:

{
  "proxy": {
    "to": "https://backend.internal",
    "pathRewrite": {
      "from": "/gateway/service",
      "to": ""
    }
  }
}

Request to /gateway/service/users → forwards to /users


Combining Proxy with Stubs

Mix static stubs with proxy fallback:

{
  "port": 4545,
  "protocol": "http",
  "stubs": [
    {
      "predicates": [{ "equals": { "path": "/mocked" } }],
      "responses": [{
        "is": { "statusCode": 200, "body": "Mocked response" }
      }]
    },
    {
      "responses": [{
        "proxy": {
          "to": "https://api.example.com",
          "mode": "proxyTransparent"
        }
      }]
    }
  ]
}

Requests to /mocked return the static response; all others proxy to the real API.


Example: API Gateway Pattern

Create a proxy that records and allows overriding specific endpoints:

{
  "port": 4545,
  "protocol": "http",
  "stubs": [
    {
      "predicates": [{ "equals": { "path": "/health" } }],
      "responses": [{ "is": { "statusCode": 200, "body": "OK" } }]
    },
    {
      "predicates": [{ "equals": { "path": "/api/feature-flag" } }],
      "responses": [{ "is": { "body": { "enabled": true } } }]
    },
    {
      "responses": [{
        "proxy": {
          "to": "https://api.example.com",
          "mode": "proxyOnce",
          "predicateGenerators": [{
            "matches": { "method": true, "path": true }
          }]
        }
      }]
    }
  ]
}

Best Practices

  1. Start with proxyOnce - Record responses for consistent tests
  2. Use predicate generators wisely - Too specific = too many stubs
  3. Export regularly - Save recorded mocks to version control
  4. Clean up sensitive data - Review recorded responses for secrets
  5. Use proxyTransparent for debugging - See actual API responses