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:
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.- Single-valued contexts take the first value.
injectfunctions,_rift.script,predicateGenerators, the debug endpoint, thecopy/lookup/decorate/shellTransformbehaviors 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 existfalse- 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 (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": {"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 } }]
}
]
}