Scripting
Rift supports multiple scripting engines for dynamic behavior.
Available Engines
| Engine | Format | Use Case |
|---|---|---|
| JavaScript | inject response | Mountebank-compatible injection responses |
| Rhai | _rift.script | Lightweight fault logic with flow state |
| Lua | _rift.script | High-performance scripting with flow state |
JavaScript (Mountebank Inject)
JavaScript uses the standard Mountebank inject response format for compatibility.
Injection Responses
{
"responses": [{
"inject": "function(config) { return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: config.request.path, timestamp: Date.now() }) }; }"
}]
}
Request Object
config.request.method // "GET", "POST", etc.
config.request.path // "/api/users/123"
config.request.query // { page: "1", limit: "10" }
config.request.headers // { "content-type": "application/json" }
config.request.body // Request body (string or parsed object)
State Object
Persist data across requests within the same imposter:
function(config, state) {
// Initialize or increment counter
state.counter = (state.counter || 0) + 1;
// Store user-specific data
var userId = config.request.headers['X-User-Id'];
state.users = state.users || {};
state.users[userId] = { lastSeen: Date.now() };
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requestNumber: state.counter })
};
}
Rhai (_rift.script)
Rhai is a lightweight embedded scripting language optimized for Rust. Scripts must define a should_inject(request, flow_store) function.
Basic Script
{
"port": 4545,
"protocol": "http",
"_rift": {
"flowState": {"backend": "inmemory", "ttlSeconds": 600}
},
"stubs": [{
"responses": [{
"_rift": {
"script": {
"engine": "rhai",
"code": "fn should_inject(request, flow_store) { let count = flow_store.get(\"demo\", \"counter\"); if count == () { count = 0; }; count += 1; flow_store.set(\"demo\", \"counter\", count); #{inject: true, fault: \"error\", status: 200, body: `{\"count\":${count}}`, headers: #{\"Content-Type\": \"application/json\"}} }"
}
}
}]
}]
}
Available Variables
// Request information
request.method // String: "GET", "POST", etc.
request.path // String: "/api/users"
request.headers // Map: access via request.headers["header-name"]
request.query // Map: access via request.query["param"]
request.pathParams // Map: access via request.pathParams["id"]
request.body // Parsed JSON body
// Helper functions
timestamp_header() // RFC 1123 formatted timestamp for HTTP Date header
Flow Store
Flow store provides persistent state across requests. All methods require a flow_id parameter to namespace state.
// Get value (returns () if not set)
let value = flow_store.get("flow-id", "key");
let count = flow_store.get("flow-id", "counter");
if count == () { count = 0; };
// Set value
flow_store.set("flow-id", "key", "value");
flow_store.set("flow-id", "counter", count + 1);
// Increment counter (returns new value)
let attempts = flow_store.increment("flow-id", "attempts");
// Check existence
if flow_store.exists("flow-id", "key") {
// key exists
}
// Delete value
flow_store.delete("flow-id", "key");
// Set TTL for entire flow (seconds)
flow_store.set_ttl("flow-id", 300);
Return Values
Scripts must return a map with an inject flag:
// No injection (pass through to next response or upstream)
#{ inject: false }
// Inject error response
#{
inject: true,
fault: "error",
status: 503,
body: "{\"error\": \"Service unavailable\"}",
headers: #{
"Content-Type": "application/json",
"Retry-After": "30"
}
}
// Inject latency
#{
inject: true,
fault: "latency",
duration_ms: 500
}
Lua (_rift.script)
Lua provides high-performance scripting. Scripts must define a should_inject(request, flow_store) function.
Basic Script
{
"port": 4545,
"protocol": "http",
"_rift": {
"flowState": {"backend": "inmemory", "ttlSeconds": 600}
},
"stubs": [{
"responses": [{
"_rift": {
"script": {
"engine": "lua",
"code": "function should_inject(request, flow_store)\n local fid = 'lua'\n local count = flow_store:get(fid, 'count') or 0\n count = count + 1\n flow_store:set(fid, 'count', count)\n return {\n inject = true,\n fault = 'error',\n status = 200,\n body = '{\"count\":' .. count .. '}',\n headers = {['Content-Type'] = 'application/json'}\n }\nend"
}
}
}]
}]
}
Available Variables
-- Request information (passed as first argument)
request.method -- String
request.path -- String
request.headers -- Table: request.headers["header-name"]
request.query -- Table: request.query["param"]
request.pathParams -- Table: request.pathParams["id"]
request.body -- Parsed body (table or string)
-- Standard Lua functions
math.random() -- Float 0.0 to 1.0
math.random(n) -- Integer 1 to n
math.random(m, n) -- Integer m to n
os.time() -- Unix timestamp
os.date("*t") -- Date table
Flow Store
Lua uses colon syntax for method calls:
-- Get value (returns nil if not set)
local value = flow_store:get("flow-id", "key")
local count = flow_store:get("flow-id", "counter") or 0
-- Set value
flow_store:set("flow-id", "key", "value")
flow_store:set("flow-id", "counter", count + 1)
-- Increment counter (returns new value)
local attempts = flow_store:increment("flow-id", "attempts")
-- Check existence
if flow_store:exists("flow-id", "key") then
-- key exists
end
-- Delete value
flow_store:delete("flow-id", "key")
-- Set TTL for entire flow (seconds)
flow_store:set_ttl("flow-id", 300)
Return Values
-- No injection
return { inject = false }
-- Inject error response
return {
inject = true,
fault = "error",
status = 503,
body = '{"error": "Service unavailable"}',
headers = {
["Content-Type"] = "application/json",
["Retry-After"] = "30"
}
}
-- Inject latency
return {
inject = true,
fault = "latency",
duration_ms = 500
}
Script Examples
Rate Limiting
{
"_rift": {
"flowState": {"backend": "inmemory", "ttlSeconds": 60}
},
"stubs": [{
"responses": [{
"_rift": {
"script": {
"engine": "rhai",
"code": "fn should_inject(request, flow_store) { let fid = \"ratelimit\"; let count = flow_store.get(fid, \"requests\"); if count == () { count = 0; }; count += 1; flow_store.set(fid, \"requests\", count); if count > 100 { #{inject: true, fault: \"error\", status: 429, body: `{\"error\":\"Rate limit exceeded\",\"count\":${count}}`, headers: #{\"Content-Type\": \"application/json\", \"Retry-After\": \"60\"}} } else { #{inject: false} } }"
}
}
}]
}]
}
Retry Simulation (Fail First N Requests)
{
"_rift": {
"flowState": {"backend": "inmemory", "ttlSeconds": 300}
},
"stubs": [{
"responses": [{
"_rift": {
"script": {
"engine": "rhai",
"code": "fn should_inject(request, flow_store) { let flow_id = request.headers.get(\"x-flow-id\"); if flow_id == () { flow_id = \"default\"; }; let attempts = flow_store.get(flow_id, \"attempts\"); if attempts == () { attempts = 0; }; attempts += 1; flow_store.set(flow_id, \"attempts\", attempts); if attempts <= 2 { #{inject: true, fault: \"error\", status: 503, body: `{\"error\":\"Temporary failure\",\"attempt\":${attempts}}`, headers: #{\"Content-Type\": \"application/json\"}} } else { #{inject: false} } }"
}
}
}]
}]
}
Counter with Multiple Endpoints
{
"_rift": {
"flowState": {"backend": "inmemory", "ttlSeconds": 600}
},
"stubs": [
{
"predicates": [{"equals": {"method": "POST", "path": "/api/counter/increment"}}],
"responses": [{
"_rift": {
"script": {
"engine": "rhai",
"code": "fn should_inject(request, flow_store) { let fid = \"demo\"; let counter = flow_store.get(fid, \"counter\"); if counter == () { counter = 0; }; counter += 1; flow_store.set(fid, \"counter\", counter); #{inject: true, fault: \"error\", status: 200, body: `{\"counter\":${counter}}`, headers: #{\"Content-Type\": \"application/json\"}} }"
}
}
}]
},
{
"predicates": [{"equals": {"method": "GET", "path": "/api/counter"}}],
"responses": [{
"_rift": {
"script": {
"engine": "rhai",
"code": "fn should_inject(request, flow_store) { let fid = \"demo\"; let counter = flow_store.get(fid, \"counter\"); if counter == () { counter = 0; }; #{inject: true, fault: \"error\", status: 200, body: `{\"counter\":${counter}}`, headers: #{\"Content-Type\": \"application/json\"}} }"
}
}
}]
},
{
"predicates": [{"equals": {"method": "DELETE", "path": "/api/counter"}}],
"responses": [{
"_rift": {
"script": {
"engine": "rhai",
"code": "fn should_inject(request, flow_store) { let fid = \"demo\"; flow_store.delete(fid, \"counter\"); #{inject: true, fault: \"error\", status: 200, body: \"{\\\"message\\\":\\\"Counter reset\\\"}\", headers: #{\"Content-Type\": \"application/json\"}} }"
}
}
}]
}
]
}
Engine Comparison
| Feature | JavaScript | Rhai | Lua |
|---|---|---|---|
| Format | inject response | _rift.script | _rift.script |
| State access | state.key | flow_store.get(id, key) | flow_store:get(id, key) |
| Flow isolation | Per imposter | Per flow_id | Per flow_id |
| Function wrapper | None needed | should_inject(request, flow_store) | should_inject(request, flow_store) |
| Performance | Good | Excellent | Excellent |
| Mountebank compatible | Yes | No | No |
Performance Tips
- Use Rhai/Lua for high-throughput - Both are compiled and cached for efficient reuse
- Minimize flow store access - Each get/set has overhead; batch operations when possible
- Keep scripts simple - Complex logic is harder to debug and maintain
- Use flow_id wisely - Namespace state by request ID, user ID, or session to avoid collisions
- Set appropriate TTLs - Prevent unbounded state growth with
ttlSecondsconfig