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.scriptandflowStateextensions, 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:
behaviorswithout underscore prefixbehaviorsas an arraystatusCodeas a stringproxy: nullalongsideis
Best Practices
- Set Content-Type - Always include appropriate Content-Type header
- Use JSON for APIs - Return
bodyas object for automatic serialization - Include error details - Meaningful error responses help debugging
- Use proxy for recording - Record real API responses for reliable mocks
- Keep injection simple - Complex logic is harder to maintain