Responses

Responses define what an imposter returns when a stub’s predicates match.


Response Types

is (Static Response)

Return a fixed response:

{
  "is": {
    "statusCode": 200,
    "headers": {
      "Content-Type": "application/json",
      "X-Custom-Header": "value"
    },
    "body": {
      "message": "Success",
      "data": { "id": 1 }
    }
  }
}

proxy (Forward Request)

Forward requests to a real server and optionally record responses:

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

inject (Dynamic Response)

Generate responses with JavaScript:

{
  "inject": "function(request, state, logger) { return { statusCode: 200, body: 'Request path: ' + request.path }; }"
}

Static Responses (is)

Status Codes

{ "is": { "statusCode": 201 } }
{ "is": { "statusCode": 400 } }
{ "is": { "statusCode": 500 } }

Status codes can also be specified as strings for compatibility with some tools:

{ "is": { "statusCode": "200" } }
{ "is": { "statusCode": "404" } }

Headers

{
  "is": {
    "statusCode": 200,
    "headers": {
      "Content-Type": "application/json",
      "Cache-Control": "no-cache",
      "X-Request-Id": "abc123"
    }
  }
}

Non-string header values are accepted and coerced. JSON numbers and booleans are converted to their string form, matching Mountebank, so a config like this is valid rather than a 400:

{ "is": { "headers": { "Content-Length": 124, "X-Cache-Hit": true } } }

sends Content-Length: 124 and X-Cache-Hit: true. Arrays may mix types the same way; null, objects and nested arrays are still rejected.

Automatic Content-Type. When a response has a JSON body (an object or array, not a string) and the stub sets no Content-Type header, Rift adds Content-Type: application/json. The check is case-insensitive — configuring content-type, CONTENT-TYPE, or any other casing suppresses the default rather than producing a duplicate header. A string body never gets an automatic Content-Type.

Body Types

String body:

{ "is": { "body": "Hello, World!" } }

JSON body (auto-serialized):

{
  "is": {
    "body": {
      "users": [
        { "id": 1, "name": "Alice" },
        { "id": 2, "name": "Bob" }
      ]
    }
  }
}

XML body:

{
  "is": {
    "headers": { "Content-Type": "application/xml" },
    "body": "<?xml version=\"1.0\"?><user><id>1</id></user>"
  }
}

Binary body (base64):

{
  "is": {
    "body": "SGVsbG8gV29ybGQ=",
    "_mode": "binary"
  }
}

The body is standard base64 (with padding); _mode: "binary" tells Rift to decode it before serving. Omit _mode (or set "text") for a normal text/JSON body.


Request Interpolation

Static (is) responses can echo values from the incoming request using ${request.…} tokens. Rift substitutes them into the response body and header values before any behaviors run:

Token Resolves to
${request.path} Request path
${request.method} HTTP method
${request.body} Raw request body
${request.query.<name>} Query parameter <name>
${request.headers.<name>} Request header <name> (case-insensitive)
${request.pathParams.<name>} Path parameter <name> captured from the stub’s routePattern

${request.pathParams.<name>} only resolves when the stub declares a top-level routePattern (e.g. /users/:id) whose shape matches the request path — see Route patterns.

{
  "is": {
    "statusCode": 200,
    "headers": { "X-Echo-Path": "${request.path}" },
    "body": "You called ${request.method} ${request.path} with q=${request.query.q}"
  }
}

A request GET /search?q=rust returns You called GET /search with q=rust and an X-Echo-Path: /search header.

These ${request.…} tokens are distinct from the free-form ${name} placeholders that the copy and lookup behaviors fill in — the two do not collide, because only tokens beginning with request. are treated as request interpolation. On the proxy path, only the body is interpolated (not headers).


Response Cycling

When a stub has multiple responses, they cycle through in round-robin order. Each request returns the next response in the sequence, wrapping back to the first after reaching the end.

Basic Cycling

{
  "stubs": [{
    "predicates": [{ "equals": { "path": "/cycle" } }],
    "responses": [
      { "is": { "statusCode": 200, "body": "Response 1" } },
      { "is": { "statusCode": 200, "body": "Response 2" } },
      { "is": { "statusCode": 200, "body": "Response 3" } }
    ]
  }]
}

Requests return responses in order:

  • Request 1 → “Response 1”
  • Request 2 → “Response 2”
  • Request 3 → “Response 3”
  • Request 4 → “Response 1” (cycles back)
  • Request 5 → “Response 2”

Use Cases

Simulating intermittent failures:

{
  "responses": [
    { "is": { "statusCode": 200, "body": "OK" } },
    { "is": { "statusCode": 200, "body": "OK" } },
    { "is": { "statusCode": 503, "body": "Service Unavailable" } }
  ]
}

Every third request fails - useful for testing retry logic.

Simulating state changes:

{
  "stubs": [{
    "predicates": [{ "equals": { "path": "/order/status" } }],
    "responses": [
      { "is": { "body": { "status": "pending" } } },
      { "is": { "body": { "status": "processing" } } },
      { "is": { "body": { "status": "shipped" } } },
      { "is": { "body": { "status": "delivered" } } }
    ]
  }]
}

Each poll returns the next order status.

Returning different data:

{
  "stubs": [{
    "predicates": [{ "equals": { "path": "/random-quote" } }],
    "responses": [
      { "is": { "body": { "quote": "Be the change you wish to see." } } },
      { "is": { "body": { "quote": "Stay hungry, stay foolish." } } },
      { "is": { "body": { "quote": "Think different." } } }
    ]
  }]
}

Cycling with Repeat Behavior

Use the repeat behavior to return the same response multiple times before advancing:

{
  "responses": [
    {
      "is": { "statusCode": 200, "body": "Success" },
      "_behaviors": { "repeat": 3 }
    },
    { "is": { "statusCode": 500, "body": "Error" } }
  ]
}

This returns “Success” three times, then “Error” once, then cycles:

  • Requests 1-3 → “Success”
  • Request 4 → “Error”
  • Requests 5-7 → “Success”
  • Request 8 → “Error”

See Behaviors for more on the repeat behavior.

Cycling State

  • Cycling state is per-stub - each stub maintains its own position
  • State resets when the imposter is deleted and recreated
  • State is not persisted - restarting Rift resets all cycling positions

Mixed Response Types

Cycling works with any response type - you can mix is, proxy, and inject:

{
  "responses": [
    { "is": { "statusCode": 200, "body": "Cached response" } },
    { "proxy": { "to": "https://api.example.com" } }
  ]
}

First request returns cached data, second proxies to real API, then cycles.


Rift Extensions: Controlled State Management

Rift-Specific Feature: The following features use Rift’s _rift.script and flowState extensions, which are not available in Mountebank.

Standard response cycling is global - all users share the same position in the cycle. This can cause unpredictable behavior in multi-user scenarios. Rift provides flow state and scripting for controlled, isolated state management.

Comparison: Cycling vs Flow State

Capability Mountebank Cycling Rift Flow State
State scope Global (all users) Per flow_id (isolated)
State persistence Lost on restart Redis backend available
Complex logic Not possible Full scripting support
Time-based rules Not possible TTL + timestamp checks
Per-user tracking Not possible Use user ID as flow_id

Per-User Retry Simulation

With standard cycling, if User A triggers the first failure, User B gets the second failure. With Rift flow state, each user gets their own retry sequence:

{
  "port": 4545,
  "protocol": "http",
  "_rift": {
    "flowState": { "backend": "inmemory", "ttlSeconds": 300, "flowIdSource": "header:X-User-Id" }
  },
  "stubs": [{
    "predicates": [{ "equals": { "path": "/api/resource" } }],
    "responses": [{
      "_rift": {
        "script": {
          "engine": "rhai",
          "code": "fn respond(ctx) { let attempts = ctx.state.incr(\"attempts\"); if attempts <= 2 { http(503, #{ error: \"Temporary failure\", attempt: attempts, user: ctx.flowId }).header(\"Retry-After\", \"1\") } else { pass() } }"
        }
      },
      "is": { "statusCode": 200, "body": "{\"status\": \"success\"}" }
    }]
  }]
}

Now each user experiences their own retry sequence:

  • User A: Fail → Fail → Success
  • User B: Fail → Fail → Success (independent of User A)

Quota Exhaustion with Reset

Rift-Only: Track quota consumption over time with manual or automatic reset.

{
  "_rift": {
    "flowState": { "backend": "inmemory", "ttlSeconds": 86400, "flowIdSource": "header:X-Api-Key" }
  },
  "stubs": [
    {
      "predicates": [{ "equals": { "path": "/api/expensive-operation" } }],
      "responses": [{
        "_rift": {
          "script": {
            "engine": "rhai",
            "code": "fn respond(ctx) { if ctx.request.header(\"X-Api-Key\") == () { return http(401, #{ error: \"API key required\" }); }; let used = ctx.state.get_or(\"quota_used\", 0); let limit = 1000; if used >= limit { http(402, #{ error: \"Quota exceeded\", used: used, limit: limit }) } else { ctx.state.set(\"quota_used\", used + 1); pass() } }"
          }
        },
        "is": { "statusCode": 200, "body": "{\"result\": \"expensive computation\"}" }
      }]
    },
    {
      "predicates": [{ "equals": { "method": "POST", "path": "/api/quota/reset" } }],
      "responses": [{
        "_rift": {
          "script": {
            "engine": "rhai",
            "code": "fn respond(ctx) { ctx.state.delete(\"quota_used\"); http(200, #{ message: \"Quota reset\" }) }"
          }
        }
      }]
    }
  ]
}

When to Use Each Approach

Scenario Use Mountebank Cycling Use Rift Flow State
Simple round-robin responses Overkill
Test sees responses in order Not needed
Multi-user with isolated state
Time-based rate limiting
Complex conditional logic
State survives restart ✅ (Redis)
Per-session behavior

See Scripting and Fault Injection for more examples.


Proxy Responses

Forward requests to real servers and optionally record for later playback.

Proxy Modes

proxyAlways - Always forward, record each response:

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

proxyOnce - Forward first request, replay recorded response:

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

proxyTransparent - Forward without recording:

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

Predicate Generators

Control how recorded stubs are created:

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

Adding Behaviors to Proxied Responses

{
  "proxy": {
    "to": "https://api.example.com",
    "addDecorateBehavior": "function(request, response) { response.headers['X-Proxied'] = 'true'; return response; }"
  }
}

Injection Responses

Generate dynamic responses using JavaScript:

{
  "inject": "function(request, state, logger) { \
    var userId = request.path.split('/')[2]; \
    return { \
      statusCode: 200, \
      headers: { 'Content-Type': 'application/json' }, \
      body: JSON.stringify({ id: userId, name: 'User ' + userId }) \
    }; \
  }"
}

Request Object

Available properties in injection function:

request.method    // "GET", "POST", etc.
request.path      // "/api/users/123"
request.query     // { page: "1" }
request.headers   // { "content-type": "application/json" }
request.body      // Request body (string or parsed JSON)

State Object

Persist data across requests:

function(request, state, logger) {
  // Initialize counter
  state.counter = state.counter || 0;
  state.counter++;

  return {
    statusCode: 200,
    body: { count: state.counter }
  };
}

Logger Object

Write to Rift logs:

function(request, state, logger) {
  logger.info("Processing request to " + request.path);
  return { statusCode: 200 };
}

Response Templates

Use EJS templates for dynamic content:

{
  "is": {
    "statusCode": 200,
    "headers": { "Content-Type": "application/json" },
    "body": "{ \"path\": \"<%- request.path %>\", \"timestamp\": \"<%- new Date().toISOString() %>\" }"
  },
  "_behaviors": {
    "decorate": "function(request, response) { return response; }"
  }
}

Error Responses

Client Errors (4xx)

{
  "is": {
    "statusCode": 400,
    "body": { "error": "Bad Request", "message": "Invalid input" }
  }
}

{
  "is": {
    "statusCode": 401,
    "headers": { "WWW-Authenticate": "Bearer" },
    "body": { "error": "Unauthorized" }
  }
}

{
  "is": {
    "statusCode": 404,
    "body": { "error": "Not Found" }
  }
}

Server Errors (5xx)

{
  "is": {
    "statusCode": 500,
    "body": { "error": "Internal Server Error" }
  }
}

{
  "is": {
    "statusCode": 503,
    "headers": { "Retry-After": "60" },
    "body": { "error": "Service Unavailable" }
  }
}

Alternative Formats

Rift supports several alternative response formats for compatibility with various tools that generate Mountebank configurations.

proxy: null with is Response

Some tools include "proxy": null alongside an is response. This is accepted and the null proxy is ignored:

{
  "responses": [{
    "is": {
      "statusCode": 200,
      "body": "Hello"
    },
    "proxy": null
  }]
}

Combined Alternative Format

A complete example using multiple alternative formats:

{
  "responses": [{
    "behaviors": [{ "wait": 100 }],
    "is": {
      "statusCode": "201",
      "headers": { "Content-Type": "application/json" },
      "body": "{\"created\": true}"
    },
    "proxy": null
  }]
}

This example shows:

  • behaviors without underscore prefix
  • behaviors as an array
  • statusCode as a string
  • proxy: null alongside is

Best Practices

  1. Set Content-Type - Always include appropriate Content-Type header
  2. Use JSON for APIs - Return body as object for automatic serialization
  3. Include error details - Meaningful error responses help debugging
  4. Use proxy for recording - Record real API responses for reliable mocks
  5. Keep injection simple - Complex logic is harder to maintain