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

  1. Use Rhai/Lua for high-throughput - Both are compiled and cached for efficient reuse
  2. Minimize flow store access - Each get/set has overhead; batch operations when possible
  3. Keep scripts simple - Complex logic is harder to debug and maintain
  4. Use flow_id wisely - Namespace state by request ID, user ID, or session to avoid collisions
  5. Set appropriate TTLs - Prevent unbounded state growth with ttlSeconds config