Predicates

Predicates define rules for matching incoming requests. When all predicates in a stub match, the stub’s response is returned.


Request Fields

Predicates can match on these request fields:

Field Description Example
method HTTP method GET, POST, PUT, DELETE
path Request path /api/users
query Query parameters { "page": "1" }
headers Request headers { "Authorization": "Bearer..." }
body Request body String or JSON object
form Form fields of an application/x-www-form-urlencoded body { "name": "alice" }
requestFrom Client address and port 127.0.0.1:53412
ip Client IP address 127.0.0.1

Repeated request headers

A client may send the same header name more than once. Such a header matches if any of its values satisfies the predicate — so a request carrying

X-Test: first
X-Test: second

matches {"equals": {"headers": {"X-Test": "first"}}} and {"equals": {"headers": {"X-Test": "second"}}}.

Two consequences worth knowing:

  • not sees every value. {"not": {"equals": {"headers": {"X-Test": "first"}}}} does not match the request above. Before Rift evaluated every value, first was shadowed by second and the negation succeeded.
  • Single-valued contexts take the first value. inject functions, _rift.script, predicateGenerators, the debug endpoint, the copy / lookup / decorate / shellTransform behaviors and ${request.headers.*} template substitution all receive one value per header name — the first one sent.

Only a value that is not valid UTF-8 is affected — the check is UTF-8 validity, not ASCII (#1048). A header carrying non-ASCII UTF-8, such as X-User-Name: José or Content-Disposition: attachment; filename="résumé.pdf", is matched, forwarded and recorded byte-exact.

A header value that is not valid UTF-8 is dropped, everywhere: predicate matching, proxy forwarding, savedRequests, the behaviors and template substitution. It is never presented as an empty string, so {"equals": {"headers": {"X-Bin": ""}}} does not match a request that sent raw bytes, a name whose only value was undecodable does not satisfy exists, and a script sees no such key rather than a key holding "". Each request carrying one logs a single warning naming the affected headers.

Every one of those surfaces reads the same collected view of the request (#1040), so they cannot disagree about what the client sent. Before that, the behaviors and template substitution made their own second pass with a different rule — last value rather than first, and "" rather than dropped — which meant a single request could answer one way to a predicate and another way to a copy reading the same header.


Predicate Types

equals

Exact match on request fields:

{
  "equals": {
    "method": "GET",
    "path": "/users",
    "query": { "active": "true" }
  }
}

Match headers (case-insensitive by default):

{
  "equals": {
    "headers": { "Content-Type": "application/json" }
  }
}

Match JSON body:

{
  "equals": {
    "body": { "username": "admin", "password": "secret" }
  }
}

deepEquals

Like equals but requires exact object structure (no extra fields):

{
  "deepEquals": {
    "body": { "id": 1, "name": "Test" }
  }
}

Works on all request fields - useful for strict matching:

{
  "deepEquals": {
    "method": "GET",
    "path": "/api/users",
    "body": ""
  }
}

contains

Partial match - checks if value is contained:

{
  "contains": {
    "path": "/api",
    "body": { "action": "create" }
  }
}

Match substring in query parameter values:

{
  "contains": {
    "query": { "lenderIds": "Test" }
  }
}

This matches requests like ?lenderIds=TestUser or ?lenderIds=MyTestValue.

startsWith

Match beginning of string:

{
  "startsWith": {
    "path": "/api/v1"
  }
}

endsWith

Match end of string:

{
  "endsWith": {
    "path": ".json"
  }
}

matches

Regular expression match:

{
  "matches": {
    "path": "/users/\\d+",
    "headers": { "Authorization": "Bearer [A-Za-z0-9]+" }
  }
}

Unlike the other operators, matches folds case per Unicode, not ASCII — see caseSensitive.

exists

Check field existence:

{
  "exists": {
    "headers": { "X-Api-Key": true },
    "query": { "debug": false }
  }
}
  • true - Field must exist
  • false - Field must not exist

inject

Match with a JavaScript function that returns true or false. It requires the server to be started with --allowInjection; without it the imposter is refused.

{
  "inject": "function (config) { return config.request.path.indexOf('/admin') === 0; }"
}

The function receives Mountebank’s config object (config.request, config.state, config.logger); the legacy function (request, logger) form also works, because the request fields are copied onto config too.


JSONPath Predicates

Match specific values in JSON bodies using JSONPath. The jsonpath selector is combined with a predicate operation:

{
  "jsonpath": { "selector": "$.user.name" },
  "equals": { "body": "admin" }
}

A leading $ is optional. A selector that does not start with $ is treated as root-relative: user.name is normalized to $.user.name, and [0] to $[0]. This applies wherever a jsonpath selector is used — predicates and the copy behavior’s jsonpath extraction alike.

JSONPath Operators

// Equals - match exact value
{ "jsonpath": { "selector": "$.count" }, "equals": { "body": 10 } }

// Contains - partial match
{ "jsonpath": { "selector": "$.tags" }, "contains": { "body": "important" } }

// Matches - regex pattern
{ "jsonpath": { "selector": "$.email" }, "matches": { "body": ".*@example\\.com" } }

// Exists - check field presence
{ "jsonpath": { "selector": "$.optional" }, "exists": { "body": true } }

JSONPath Examples

// Match array element
{ "jsonpath": { "selector": "$.items[0].name" }, "equals": { "body": "First Item" } }

// Match nested value
{ "jsonpath": { "selector": "$.user.address.city" }, "equals": { "body": "NYC" } }

// Match with filter
{ "jsonpath": { "selector": "$.items[?(@.price > 100)]" }, "exists": { "body": true } }

XPath Predicates

Match values in XML bodies using XPath:

The xpath selector sits next to the operator, the same way jsonpath does:

{
  "xpath": { "selector": "//user/name" },
  "equals": { "body": "admin" }
}

XPath with Namespaces

{
  "xpath": {
    "selector": "//ns:item/ns:price",
    "ns": { "ns": "http://example.com/schema" }
  },
  "equals": { "body": "99.99" }
}

Logical Operators

and

All predicates must match:

{
  "and": [
    { "equals": { "method": "POST" } },
    { "equals": { "path": "/api/users" } },
    { "contains": { "body": { "role": "admin" } } }
  ]
}

or

At least one predicate must match:

{
  "or": [
    { "equals": { "path": "/api/v1/users" } },
    { "equals": { "path": "/api/v2/users" } }
  ]
}

not

Predicate must not match:

{
  "not": {
    "equals": { "method": "DELETE" }
  }
}

Complex Combinations

{
  "and": [
    { "equals": { "method": "POST" } },
    {
      "or": [
        { "contains": { "body": { "type": "A" } } },
        { "contains": { "body": { "type": "B" } } }
      ]
    },
    {
      "not": {
        "exists": { "headers": { "X-Test-Skip": true } }
      }
    }
  ]
}

Predicate Options

caseSensitive

Enable case-sensitive matching (default: false):

{
  "equals": { "path": "/API/Users" },
  "caseSensitive": true
}

Case-insensitive matching (the default) folds ASCII letters only (AZaz) across equals, deepEquals, contains, startsWith, and endsWith. Non-ASCII characters (e.g. É vs é) are compared exactly.

matches is the exception: its case-insensitive mode is the regex engine’s, which applies full Unicode case folding. So {"matches": {"path": "^/josé$"}} matches a request for /JOSÉ, while {"equals": {"path": "/josé"}} does not.

Migrating from Mountebank: Mountebank folds Unicode for all of these operators, so it has no such split. Rift’s string operators deviate deliberately (see below); matches is the one that behaves identically in both. If your paths, headers or bodies are all ASCII — as most are — nothing changes. If they are not, the string operators are stricter here than you may expect.

The split is deliberate, not an oversight. The string operators fold ASCII so that a comparison allocates nothing per request, and so the stub index’s prefix/substring pruning stays sound — Unicode folding is length-changing and context-sensitive (startsWith "/ΟΣ" against a request for /ΟΣΑ folds to a final sigma), which would make the index prune a stub that in fact still matches. matches keeps the regex engine’s own folding because narrowing it would mean disabling Unicode mode entirely, which would also change \w, \d, \s, \b and ..

If you need one rule across both, set caseSensitive: true and normalise case yourself. Note that an inline (?i) in a matches pattern overrides the flag for that pattern, and still folds per Unicode.

keyCaseSensitive

Controls whether the names in query, headers and form are compared case-sensitively. It defaults to the value of caseSensitive.

{
  "exists": { "query": { "Debug": true } },
  "keyCaseSensitive": true
}

except

A regular expression whose matches are removed from each value before it is compared:

{
  "equals": { "path": "/orders/" },
  "except": "\\d+"
}

This matches /orders/123 and /orders/456, because the digits are stripped first.


Common Patterns

Match Any GET Request

{ "equals": { "method": "GET" } }

Match Path with ID

{ "matches": { "path": "/users/[0-9a-f-]+" } }

Match JSON Content-Type

{
  "and": [
    { "equals": { "method": "POST" } },
    { "contains": { "headers": { "Content-Type": "application/json" } } }
  ]
}

Match Authenticated Requests

{ "exists": { "headers": { "Authorization": true } } }

Match Query Parameters

{
  "equals": {
    "query": { "page": "1", "limit": "10" }
  }
}

Multiple Predicates (Implicit AND)

When you specify multiple predicates in a stub’s predicates array, they are combined with implicit AND - all must match:

{
  "predicates": [
    { "endsWith": { "path": "/lender-details" } },
    { "contains": { "query": { "lenderIds": "ALL" } } },
    { "deepEquals": { "method": "GET" } }
  ],
  "responses": [{ "is": { "statusCode": 200 } }]
}

This matches GET requests to paths ending in /lender-details with query parameter lenderIds containing “ALL”.


Stub Ordering

Stubs are evaluated in order. Place more specific predicates first:

{
  "stubs": [
    {
      "predicates": [{ "equals": { "path": "/users/admin" } }],
      "responses": [{ "is": { "body": "Admin user" } }]
    },
    {
      "predicates": [{ "matches": { "path": "/users/.*" } }],
      "responses": [{ "is": { "body": "Regular user" } }]
    },
    {
      "predicates": [],
      "responses": [{ "is": { "statusCode": 404 } }]
    }
  ]
}