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 twoX-Testheaders,firstandsecond) 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=secondand?key=first&key=second&key=third, but not?key=first. deepEqualscompares the values as a multiset.{"deepEquals": {"query": {"key": ["second", "first"]}}}matches?key=first&key=secondin either order, but not with a third value, and["first", "first"]matches only a key sent twice asfirst. A scalar neverdeepEqualsa 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. exceptand case folding apply to each value and to each expected element.- A comma does not split a value.
?key=first,secondis 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:
notsees every value.{"not": {"equals": {"headers": {"X-Test": "first"}}}}does not match the request above. Before Rift evaluated every value,firstwas shadowed bysecondand the negation succeeded.- Scripts see every value; single-valued contexts take the first.
injectfunctions,decorate,shellTransform'sMB_REQUEST,_rift.scriptandpredicateGeneratorsinjectsee 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, thecopy/lookupbehaviors 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:
Match headers (case-insensitive by default):
Match JSON body:
deepEquals¶
Like equals but requires exact object structure (no extra fields):
Works on all request fields - useful for strict matching:
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,endsWithandmatchestreat 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}]}.containscompares each element on its own, so{"tags": "a\",\"b"}does not match across["a", "b"], andnotof 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. deepEqualscompares 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.nullequals onlynullunderequals(not"", not"null", not a missing field), and equals"null"underdeepEquals, 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.0is1and1e2is100, under every operator.contains/startsWith/endsWithread a number or boolean body value as that text (see Differences).
contains¶
Partial match - checks if value is contained:
Match substring in query parameter values:
This matches requests like ?lenderIds=TestUser or ?lenderIds=MyTestValue.
startsWith¶
Match beginning of string:
endsWith¶
Match end of string:
matches¶
Regular expression match:
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:
true- Field must exist and not be emptyfalse- 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.
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:
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,endsWithandmatcheswith a single expectedbodyhold when one of the selected values matches it.caseSensitiveandexceptapply to each value.- An expected
bodythat is an array holds when every element of it matches some selected value. existswithtrueholds when some selected value is non-empty; withfalse, when some is empty or nothing is selected.deepEqualscompares the sorted list of selected values with the sorted expected array, so{"deepEquals": {"body": ["b", "a"]}}matches a selection ofaandb, 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:
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 ( , ©) or an & that starts no reference
is kept as literal text (x 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, soexists: trueholds for an empty element<a/>; /: the XML declaration's data (version="1.0"), a leading comment or processing instruction, elseundefined.
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&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:
not¶
Predicate must not match:
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):
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.
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:
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¶
Match Path with ID¶
Match JSON Content-Type¶
{
"and": [
{ "equals": { "method": "POST" } },
{ "contains": { "headers": { "Content-Type": "application/json" } } }
]
}
Match Authenticated Requests¶
Match Query Parameters¶
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 } }]
}
]
}