Skip to content
Latest stable: v0.8.0.

Advanced matchers

Chaos Maker rules historically matched on urlPattern plus optional methods and graphqlOperation. Advanced matchers extend the targeting surface so a single rule can express “the customers API on the production host, GET only, when the bearer token is set, fetch traffic only” without hand-crafting URL substrings. A separate named matcher registry lets multiple rules share one definition.

Every network rule (failures, latencies, aborts, corruptions, cors) accepts these optional fields alongside the existing ones:

  • hostname - string | RegExp. String compares case-insensitively against new URL(url).hostname. RegExp uses .test(). The g and y flags are rejected at validation time, matching graphqlOperation.
  • queryParams - Record<string, string | RegExp | boolean>. Every entry must pass. true requires the key to be present (any value). false requires absence. A string matches the decoded value exactly. A RegExp tests the value.
  • requestHeaders - Record<string, string | RegExp | boolean>. Same value semantics as queryParams. Key comparison is case-insensitive. The field is named requestHeaders (not headers) so it does not collide with the response-synthesis headers field on failure rules.
  • resourceTypes - Array<'fetch' | 'xhr'>. Non-empty. Rule fires only when the originating interceptor is in the list. WebSocket and SSE rules already live in their own categories and are not addressable here.

The matchers are evaluated in order: urlPatternmethodsresourceTypeshostnamequeryParamsrequestHeadersgraphqlOperation. The first one that fails skips the rule.

await injectChaos(page, {
network: {
failures: [
{
urlPattern: '/api',
hostname: 'api.example.com',
methods: ['POST'],
queryParams: { tenant: 'acme', debug: false },
requestHeaders: { authorization: /^Bearer / },
resourceTypes: ['fetch'],
statusCode: 503,
probability: 1,
},
],
},
});

Repeating the same matcher block across multiple rules bloats the config and obscures intent. A named matcher is a reusable bundle stored on the top-level matchers field. Rules reference one by name via the matcher field instead of inlining the matcher fields.

await injectChaos(page, {
matchers: {
customers: {
urlPattern: '/api/customers',
hostname: 'api.example.com',
methods: ['GET'],
},
},
network: {
failures: [
{ matcher: 'customers', statusCode: 503, probability: 1 },
],
latencies: [
{ matcher: 'customers', delayMs: 500, probability: 1 },
],
},
});

At engine init, the resolver inlines the registered fields into every referencing rule and strips the matchers field from the resolved config. The matcher reference disappears too; downstream interceptors see flat rules identical to the inline form.

A rule MUST use either matcher: 'name' alone OR one or more inline matcher fields, never both. Mixing the two surfaces a matcher_inline_conflict validation issue:

// Throws ChaosConfigError with code: 'matcher_inline_conflict'
{
matcher: 'customers',
urlPattern: '/api/anything', // forbidden alongside `matcher`
statusCode: 503,
probability: 1,
}

This keeps the mental model simple: a rule is either a named-matcher reference or a self-contained inline rule. There is no “merge with overrides” precedence to reason about.

Resolution runs as the last step inside prepareChaosConfig:

  1. Zod pass 1 (schema validation).
  2. applyProfile resolves profile and profileOverrides.
  3. expandPresets expands the presets[] array.
  4. Zod pass 2 (post-merge re-validation).
  5. resolveNamedMatchers inlines matcher references against the per-instance MatcherRegistry.

Running last means rules brought in by presets or profiles can reference matchers defined at the top level. Named matchers themselves cannot reference other matchers; carrying a matcher field inside a registry entry surfaces a matcher_cycle validation issue. This code path is reserved now and will catch real cycles when matcher composition is added in a future release.

The ChaosConfigBuilder exposes .defineMatcher(name, matcher) to register entries fluently:

import { ChaosConfigBuilder } from '@chaos-maker/core';
const config = new ChaosConfigBuilder()
.defineMatcher('customers', { urlPattern: '/api/customers', methods: ['GET'] })
.build();

Rules that reference the matcher set the matcher field on the literal rule object; the builder rule helpers stay positional and unchanged in this release.

A few matcher definitions get rewritten project after project: the GraphQL endpoint, API traffic, authenticated requests. Chaos Maker ships these as built-in named matchers. A rule references one by name with no matchers entry of its own.

await injectChaos(page, {
network: {
latencies: [
{ matcher: 'graphql', delayMs: 1200, probability: 1 },
],
},
});
NameTargetsDefinition
graphqlGraphQL endpointsurlPattern: '/graphql'
apiRequestsAPI trafficurlPattern: '/api'
authRequestsRequests carrying an Authorization headerrequestHeaders: { authorization: true }

A built-in resolves through the same path as a user matcher: its fields inline into the rule, the matcher reference is stripped, and debug events carry detail.matcherName. There is no separate runtime and no new rule shape.

Declare a matchers entry (or call .defineMatcher) with the same name to replace a built-in for that config. The user entry wins and no collision is raised, so a project can keep the familiar name while pointing it at its own endpoint.

await injectChaos(page, {
matchers: {
graphql: { urlPattern: '/internal/graphql' },
},
network: {
failures: [{ matcher: 'graphql', statusCode: 503, probability: 1 }],
},
});

graphql and apiRequests target on urlPattern, so they apply to network, WebSocket, and SSE rules alike.

authRequests is meaningful on network rules only. Its single field, requestHeaders, is not evaluated on WebSocket or SSE rules, because those transports do not expose request headers. A WebSocket or SSE rule that references matcher: 'authRequests' therefore carries no transport-applicable targeting and matches every stream. For WebSocket and SSE, target with graphql, apiRequests, or an inline urlPattern / hostname / queryParams.

Failures surface through the existing ChaosConfigError aggregator. Codes specific to advanced matchers:

  • matcher_not_found - rule references a name that is not in matchers (after preset and profile expansion). Path: matchers.<name>.
  • matcher_collision - two entries collide after trim() normalization. Path: matchers.<name>.
  • matcher_inline_conflict - rule mixes matcher with inline matcher fields. Path: <rule-array>.[<index>].matcher.
  • matcher_cycle - registry entry carries its own matcher field. Reserved for future composition; observable today via untyped configs. Path: matchers.<name>.matcher.

Rules originating from a named matcher are attributed in debug events. Every type: 'debug' event from a matcher-resolved rule carries detail.matcherName: 'customers' (or whatever name fired). The rule-matched stage additionally carries detail.matchedBy: string[] listing which non-URL matchers fired ('hostname' | 'queryParams' | 'requestHeaders' | 'resourceTypes' | 'graphqlOperation'), and rule-skip-match carries detail.skippedAt: string naming the matcher field that failed.

{
type: 'debug',
detail: {
stage: 'rule-matched',
ruleId: 'failure#0',
matcherName: 'customers',
matchedBy: ['hostname', 'queryParams'],
url: '/api/customers?tenant=acme',
method: 'GET',
},
}

This makes the answer to “why did this rule fire?” and “why did this rule skip?” trivially readable in dashboards.

WebSocket and SSE rules accept the same named-matcher registry and a subset of the inline matcher fields. The supported inline fields on every WS and SSE rule are:

  • urlPattern
  • hostname
  • queryParams
  • matcher (named-matcher reference)

The four network-only fields (methods, requestHeaders, resourceTypes, graphqlOperation) are rejected if inlined onto a WS or SSE rule because they have no meaning on those transports: WebSocket opens via a fixed Upgrade handshake, SSE is GET-only, neither browser API exposes request headers, and there is no JSON-body operation to extract.

A NamedMatcher reused across transports may still declare any of its fields. The WS/SSE gate evaluates only urlPattern, hostname, and queryParams; non-applicable fields are silently ignored so one matcher can target network, WebSocket, and SSE without per-transport duplication.

await injectChaos(page, {
matchers: {
realtimeApi: { hostname: /realtime/ },
},
websocket: {
drops: [
{ matcher: 'realtimeApi', direction: 'inbound', probability: 0.2 },
],
},
sse: {
drops: [
{ matcher: 'realtimeApi', probability: 0.2 },
],
},
});

Inline forms work the same way:

websocket: {
drops: [
{
urlPattern: 'wss://realtime',
direction: 'outbound',
queryParams: { room: 'alpha' },
probability: 1,
},
],
}

The transport gate runs urlPattern → direction (WS) / eventType (SSE) → hostname → queryParams before counting and probability so a matcher mismatch never consumes counting state. Debug attribution mirrors the network surface: rule-matched carries detail.matchedBy, rule-skip-match carries detail.skippedAt, and matcher-resolved rules carry detail.matcherName.

The TypeScript shape TransportRuleMatchers is a discriminated union: a rule either declares one or more inline fields (urlPattern, hostname, queryParams) or declares matcher: 'name', never both. This mirrors the inline-versus-named split on network rules; urlPattern is optional as long as hostname or queryParams already targets the rule. At runtime, validation rejects rules that supply neither a matcher reference nor at least one inline field, so a rule always carries some targeting.

Matcher behavior is parity-tested across all four supported adapters. Playwright, Cypress, WebdriverIO, and Puppeteer each run an identical set of scenarios from a shared declarative catalog, so a matcher field that fires on one adapter fires on every adapter, with the same chaos log, the same matchedBy attribution, and the same observable outcome. The catalog covers hostname, query parameter, header, and resource-type matching on network rules, the three built-in matchers and their user-override behavior, and the WebSocket and SSE matcher subset (urlPattern, hostname, queryParams, matcher) plus debug matchedBy attribution. New matcher coverage gets added once to the shared catalog rather than copied into each adapter’s E2E suite.

  • Matcher composition (a NamedMatcher referencing another). The error code matcher_cycle reserves the surface for future work.
  • Response matchers (status code, response headers). Matchers operate on the request only.
  • Disjunctive (any-of) matchers. A rule’s matcher block is conjunctive: every field must pass.

See also: Scenario profiles, Presets, Validation.