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 (requires --allowInjection):
{
"inject": "function(request, state, logger) { return { statusCode: 200, body: 'Request path: ' + request.path }; }"
}
fault (Connection Fault)
Break the connection instead of answering, as Mountebank’s fault response does:
{ "fault": "CONNECTION_RESET_BY_PEER" }
CONNECTION_RESET_BY_PEER and RANDOM_DATA_THEN_CLOSE are Mountebank’s names; Rift also accepts EMPTY_RESPONSE, MALFORMED_RESPONSE_CHUNK and the short aliases listed in Fault Injection. An unrecognised name is served as a 500 with an Unknown fault body.
Where behaviors and _rift apply
_behaviors/behaviors and a response-level _rift block are read on an is response (and the flat form below). On a proxy, inject or fault response they are ignored.
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):
A JSON body is re-serialized, so its numbers are printed from their parsed value. Rift parses floats with correct rounding, so {"n": 7e23} is served as 7e23 and a 17-digit double such as 0.10018513143495411 keeps its digits. Only a number wider than a 64-bit integer, or with more significant digits than a double holds, is served rounded; send such a value as a string body if it must be exact.
{
"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>. A key repeated in the URL renders comma-joined, in order — ?color=red&color=green gives red,green — the same value a query predicate matches against |
${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.
A header value can only carry certain bytes, and an interpolated one is built from data the client sent. When a ${request.…} token appears in a header value, Rift removes the characters a header value cannot hold — CR, LF, NUL and the other ASCII control characters — from the substituted text, and logs a rift::template warning naming what it removed. The warning carries port, stub (the stub’s index) and stub_id alongside removed, so on a server running many imposters you can grep straight back to the stub that produced it. A horizontal tab and any non-ASCII character are legal in a header value and are kept byte-exact. The same repair covers every other way substituted text reaches a header: the {{ }} templating grammar and the text the copy and lookup behaviors splice in.
The repair is deliberately narrow: it covers only the substituted value, never the literal text you wrote around it. A control character written literally into a header in your config is an authoring error rather than client data, so it still fails that response with a 500 — even when the same header also contains a token. Response bodies are never filtered this way.
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).
What a token substitutes is served as the client sent it and is never read as a ${request.…}, copy or lookup token again. With _rift.templated, a {{request.query.q}} that renders ${request.headers.authorization} stays that literal text: it does not reflect a request header the client named. Likewise a copy or lookup never expands a token that arrived inside interpolated text, even when your own text supplies its closing character. See Substituted text is never re-scanned.
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 Recorded Stubs
addDecorateBehavior is copied onto the stubs a proxyOnce or proxyAlways recording saves, and addWaitBehavior: true gives each saved response a wait equal to the upstream’s observed latency, so both take effect when the recorded stubs are replayed. See Proxy Mode.
{
"proxy": {
"to": "https://api.example.com",
"addDecorateBehavior": "function(request, response) { response.headers['X-Proxied'] = 'true'; return response; }"
}
}
Injection Responses
Generate dynamic responses using JavaScript. An inject response requires the server to be started with --allowInjection; without it the imposter is refused.
{
"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" } - one value per name, the first sent
request.body // Request body as a string; call JSON.parse yourself for JSON
Rift also accepts Mountebank’s current function (config) { ... } form: config.request, config.state and config.logger hold the same objects, and the request fields are copied onto config itself, which is why the legacy function (request, state, logger) form keeps working.
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
EJS tags in a config file are expanded once, when the file loads, and never see a request. To put request values into a response, use the ${request.…} tokens in Request Interpolation, or the opt-in _rift.templated {{ }} grammar and the date tokens described in Response Templates.
{
"is": {
"statusCode": 200,
"headers": { "Content-Type": "application/json" },
"body": "{ \"path\": \"${request.path}\", \"method\": \"${request.method}\" }"
}
}
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
}]
}
Flat response (no is wrapper)
Recorded and migrated mocks often put statusCode, headers, body and _mode directly on the response. Rift serves that exactly like the same fields inside is; statusCode defaults to 200.
{
"responses": [{
"statusCode": 404,
"headers": { "Content-Type": "application/json" },
"body": { "error": "not found" }
}]
}
is wins when both are present. GET /imposters returns the response in the is form.
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