Skip to content

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 keys

A client may send the same query key, form key or header name more than once. The field then holds every value, in the order sent, and is matched value by value, as in Mountebank:

  • A scalar expectation holds when any value satisfies it. A request carrying ?key=first&key=second (or two X-Test headers, first and second) matches {"equals": {"query": {"key": "first"}}} and {"equals": {"query": {"key": "second"}}}, and {"startsWith": {"query": {"key": "sec"}}}.
  • An array expectation needs each of its elements among the values, in any order, with other values allowed. {"equals": {"query": {"key": ["second", "first"]}}} matches ?key=first&key=second and ?key=first&key=second&key=third, but not ?key=first.
  • deepEquals compares the values as a multiset. {"deepEquals": {"query": {"key": ["second", "first"]}}} matches ?key=first&key=second in either order, but not with a third value, and ["first", "first"] matches only a key sent twice as first. A scalar never deepEquals a repeated key.
  • A one-element array is not a key sent once. {"equals": {"query": {"key": ["first"]}}} does not match ?key=first, as in Mountebank, which is what lets its documentation's example tell "the entire array", "a subset" and "one field" apart.
  • except and case folding apply to each value and to each expected element.
  • A comma does not split a value. ?key=first,second is one value, first,second; it does not match ["first", "second"], and "first,second" does not match ?key=first&key=second. Releases before #1553 joined a repeated key into one comma-separated string, so a predicate written against that form ("first,second") needs the array instead.

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.
  • Scripts see every value; single-valued contexts take the first. inject functions, decorate, shellTransform's MB_REQUEST, _rift.script and predicateGenerators inject see a repeated header, query key or form key as an array, as Mountebank's do, and one sent once as a string. The debug endpoint, the copy / lookup behaviors and ${request.headers.*} template substitution receive the first value; ${request.query.*} renders a repeated key comma-joined.

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": ""
  }
}

Arrays in a JSON body

When a body expectation is an object or array, the body is read as JSON and compared field by field, by the rules of Mountebank's predicates.js:

  • equals, contains, startsWith, endsWith and matches treat an expected array as a subset, in any order: {"tags": ["a", "c"]} matches {"tags": ["a", "b", "c"]}, and [] matches any array. A superset (["a", "z"] against ["a"]) does not match.
  • A scalar or object expectation against an array matches any element: {"tags": "b"} matches {"tags": ["a", "b"]}, and {"items": {"id": 2}} matches {"items": [{"id": 1}, {"id": 2}]}. contains compares each element on its own, so {"tags": "a\",\"b"} does not match across ["a", "b"], and not of such a predicate does not match either.
  • An object expectation against an array of objects matches when one element satisfies the whole object: {"name": "x"} matches [{"name": "y"}, {"name": "x"}], but {"a": "1", "b": "2"} does not match [{"a": "1"}, {"b": "2"}].
  • A top-level expected array ("body": ["a"]) is compared by position after sorting the expected side, so ["a"] matches ["a", "b"] and ["b", "a"] matches ["a", "b"], but ["a", "b"] does not match ["b", "a"]. The same holds for a field whose value is a string holding a JSON array.
  • deepEquals compares arrays as multisets: {"items": [{"id": 1}, {"id": 2}]} matches the reordered {"items": [{"id": 2}, {"id": 1}]}, but ["a", "a", "b"] does not match ["a", "b", "b"], and the lengths must agree. A JSON string nested in the body stays a string.
  • null equals only null under equals (not "", not "null", not a missing field), and equals "null" under deepEquals, which compares every value as text. A field the body does not have reads as "", so {"equals": {"body": {"a": ""}}} matches {}.
  • Numbers compare as JavaScript prints them: 1.0 is 1 and 1e2 is 100, under every operator. contains/startsWith/endsWith read a number or boolean body value as that text (see Differences).

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.

The path field is the request path as the client sent it. Rift does not percent-decode it, so a request for /caf%C3%A9 is matched as /caf%C3%A9.

Query and form values are decoded as application/x-www-form-urlencoded, as in Mountebank: + is a space and %2B is a plus. The path is not decoded.

exists

Check field existence:

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

As in Mountebank, an empty value counts as absent: ?flag, ?q=, an empty header, an empty form field and a "" JSON value all fail true and satisfy false. Any other JSON value is present, including 0, false, null, {} and []. The expectation is read for JavaScript truthiness, so 0, "" and null mean "absent" and any other value means "present". except applies first: a value it strips to empty is absent.

Object expectations reach into JSON ("body": {"user": {"id": true}}). Each one needs a container: an empty body, a request without a form (form exists only for a non-empty urlencoded body) or a parent that is "", null, 0 or false fails it, whether it expects true or false. A body that is not JSON has no fields, so everything in it is absent.

A repeated header, query or form key, or a JSON array exists for true; for false it holds when one of the values is empty (a header value after an empty first one replaces it, as in Mountebank). A JSON array tested against an object expectation holds when some element satisfies it, which differs from Mountebank — see Differences from Mountebank. Body keys are compared ignoring case unless keyCaseSensitive is set.

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.

A predicate's selector matches key names without regard to case, as in Mountebank: $..TITLE selects {"title": …} and $.book.title selects {"Book": {"Title": …}}. When two keys differ only in case, the later one wins. With caseSensitive false (the default) the selector's string literals are folded too, so $[?(@.n=='A')] finds "n": "a" as well as "n": "A" — except under matches, which sees the values as written. keyCaseSensitive: true, or caseSensitive: true without keyCaseSensitive: false, keeps key names exact. The copy and lookup behaviors and proxy predicateGenerators select from the body as written, as Mountebank does.

Mountebank (jsonpath-plus) shorthands

Mountebank evaluates JSONPath with the jsonpath-plus library, which accepts two spellings the JSONPath standard (RFC 9535) refuses or reads differently. Rift reads them as Mountebank does, by rewriting the selector to the standard form that selects the same values:

Written Mountebank reads it as Rift evaluates
$.a.b.[0].c a descendant segment: [0] of b and of every array under b $.a.b..[0].c
$.a.b[:0] a slice end of 0 falls back to the array length: the whole array $.a.b[:]
$.a.b[::0] a step of 0 falls back to 1 $.a.b[::]

So $.x.y.[:0].z selects the z of every element of every array at or under y, not nothing. Quoted names are left alone. The rewrite only affects evaluation: GET /imposters and a saved imposter keep the selector as written. rift-lint reports these spellings as I001.

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 } }

Selectors that pick several values

A jsonpath or xpath selector can select more than one value, for example $.items[*].name or //user/name. As in Mountebank, the predicate holds when any selected value satisfies it:

  • equals, contains, startsWith, endsWith and matches with a single expected body hold when one of the selected values matches it. caseSensitive and except apply to each value.
  • An expected body that is an array holds when every element of it matches some selected value.
  • exists with true holds when some selected value is non-empty; with false, when some is empty or nothing is selected.
  • deepEquals compares the sorted list of selected values with the sorted expected array, so {"deepEquals": {"body": ["b", "a"]}} matches a selection of a and b, and a single expected value never matches a selection of several.
  • A JSONPath selector that selects one array, such as $.tags, stands for the array's elements, as in Mountebank: {"equals": {"body": "important"}} matches {"tags": ["a", "important"]}.

XPath values are taken in document order. A copy or lookup behavior still uses the first selected value.

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" }
}

The body is read the way Mountebank's XML parser (xmldom 0.9) reads it: a leading byte-order mark (unless an XML declaration follows it, as in Mountebank) and text after the root element are ignored, and an entity XML does not define (&nbsp;, &copy;) or an & that starts no reference is kept as literal text (x&nbsp;y). Text before the root element and one stray end tag naming the root after it are ignored too, a < that opens no tag (1 < 2) and a ]]> read as text, a character reference pair to a surrogate pair reads as the one character it encodes, and a comment or processing instruction inside a DOCTYPE's internal subset is skipped whole. An unclosed or mismatched tag, an undeclared namespace prefix, a second element or DOCTYPE, or a tag after the stray end tag still means no document. See Differences from Mountebank for the recoveries Rift does not copy.

An XPath selector that fails when it runs (an undeclared prefix, an unknown function or variable, a function given the wrong arguments) fails the request with 500 "malformed xpath predicate selector", as in Mountebank; it never reads as an empty value, so equals {body: ""}, exists {body: false} or not behind it cannot match. A prefix must be bound in ns, even when the document declares it. The selector only runs on a body that parses as XML: for any other body it selects nothing, and the request is answered normally. See Differences from Mountebank.

Unless the predicate sets "caseSensitive": true, an XPath predicate ignores case, as in Mountebank: the body, the selector (string literals included) and the ns map are compared in lower case, so //b selects <B>, //a/@id selects Id="7", and //*[local-name()='Envelope'] selects <soap:Envelope>. matches selects from the body as written, as in Mountebank. See Differences from Mountebank for where this differs.

What a selected node reads as

A selected node's value is the one Mountebank reads:

  • a text node or an attribute: its text;
  • an element: its first child node. Text or CDATA gives that text, a comment its text, a processing instruction its data. An element whose first child is an element, or that has no children, reads as the string undefined, so exists: true holds for an empty element <a/>;
  • /: the XML declaration's data (version="1.0"), a leading comment or processing instruction, else undefined.

So //a on <a>x<b/>y</a> reads x, and on a pretty-printed <order>\n <id>… it reads the leading whitespace. A run of text and references (x&amp;y) is one text node, as in Mountebank, and text() selects it once. To match an element's whole text, use string(//a). That works in Rift only; Mountebank fails the request (see Differences from Mountebank).

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 (A–Z ↔ a–z) 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": {"headers": {"X-Name": "^josé$"}}} matches a request that sends X-Name: JOSÉ, while {"equals": {"headers": {"X-Name": "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. Mountebank reads it differently; see Differences from Mountebank.

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

except

A regular expression whose matches are removed from each value before it is compared — from the request's value and from the predicate's own, as in Mountebank:

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

This matches /orders/123 and /orders/456: both sides become /orders/. Under matches only the request's value is stripped; the predicate's value is a pattern. The expression ignores case unless the predicate sets "caseSensitive": true — except under matches, where it always compares case, as in Mountebank.

except applies to exists too: a value it strips to empty counts as absent, so {"exists": {"query": {"q": true}}, "except": "^x+$"} does not match ?q=xxx. In a JSON body the values are stripped after the body is parsed; Mountebank also strips the raw body text before parsing it — see Differences from Mountebank.

With an xpath or jsonpath selector, except applies to each selected value, not to the whole body as in Mountebank — see Differences from Mountebank.


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 } }]
    }
  ]
}