# How to control which stub wins when mock rules overlap

> Make stub precedence explicit in WireMock, MSW and nock, and fail a test when a stub never answers, so a catch-all cannot hide the specific case you just added.

Source: https://voxgig.com/howto/control-which-stub-wins-when-rules-overlap

- Audience: api-consumer
- Level: advanced
- Verified: 2026-10-15
- Published: 2026-10-15

## Short answer

Make precedence explicit, then test it. WireMock takes a priority field, where 1 beats the default of 5. MSW answers with the first matching handler in array order, and server.use() prepends. nock consumes one-shot interceptors in declaration order. Assert the specific and the general request in one test, and fail it when a stub never matched: listHandlers() in MSW, pendingMocks() in nock.

---
## You will need

Node 22.12 or later, which msw 3 declares as its floor, and a suite that already stubs HTTP.
Verified 2026-10-15 against Node 22.22.0, msw 3.0.3 and nock 15.0.1. WireMock 3.13.2 ran in
this container under Java 21, from the jar the `wiremock` npm package bundles, and is not re-run
by the page's checks. If nock is new to you, start with
[stub outbound HTTP calls in Node tests with nock](/howto/stub-outbound-node-http-calls-with-nock).

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [MSW](https://mswjs.io/guides/best-practices/network-behavior-overrides) | Handlers shared between browser tests, Node tests and local development | Array order is the precedence, so a merge that reorders two handlers changes behavior without a conflict | Stubs live as files edited by people who never read the code |
| [nock](https://github.com/nock/nock#read-this---about-interceptors) | Node suites where each test declares its own one-shot stubs | Declaration order decides, and one `.persist()` on a broad interceptor shadows everything after it | Stubs are shared across many tests or services |
| [WireMock](https://wiremock.org/docs/stubbing/) | Stubs kept as JSON files, served to several services or languages | A Java process to run, and mappings nobody reviews next to the code they serve | Every caller is one Node process you control |

WireMock is the only one of the three that states precedence in the stub itself, so two files
can arrive in any order and still resolve the same way. The cost is a separate server and a
second language in the test setup. MSW and nock are in-process and need no server, but both
take precedence from position, which nobody reads when resolving a merge.

## See which stub answers

Run each ordering against the same client and print who answered. The general MSW handler
matches `/v1/invoices/:id`, and the specific one matches `/v1/invoices/in_missing`.

```bash
node msw-demo.mjs 2>&1 | head -12
```

```text output
general handler first in the array
  getInvoice(in_missing)     {"id":"in_missing","status":"paid"}
  getInvoice(in_1)           {"id":"in_1","status":"paid"}
  never matched: GET https://api.example.com/v1/invoices/in_missing
specific handler first in the array
  getInvoice(in_missing)     null (the 404 stub)
  getInvoice(in_1)           {"id":"in_1","status":"paid"}
  never matched: (none)
general in setupServer, specific via server.use()
  getInvoice(in_missing)     null (the 404 stub)
  getInvoice(in_1)           {"id":"in_1","status":"paid"}
  never matched: (none)
```

MSW checks handlers top to bottom and the first match answers. `server.use()` puts its handlers
in front of the list,[^1] which is why an override inside one test wins over the shared setup. A
resolver that returns nothing lets the
[next matching handler](https://mswjs.io/docs/http/handling-requests) answer instead.

nock reads its interceptors in declaration order and removes each one as it is used:

```bash
node nock-demo.mjs | head -8
```

```text output
general interceptor declared first, both one-shot
  1st getInvoice(in_missing) {"status":"paid"}
  2nd getInvoice(in_missing) null (the 404 stub)
  pending: []
general interceptor declared first with .persist()
  1st getInvoice(in_missing) {"status":"paid"}
  2nd getInvoice(in_missing) {"status":"paid"}
  pending: ["GET https://api.example.com:443/v1/invoices/in_missing"]
```

The one-shot case is the dangerous one. Both interceptors were used, so `pendingMocks()` is empty,
yet the first request got the wrong answer. The order of calls decided the result, not the
specificity of the stubs.

## Give WireMock an explicit priority

WireMock picks the stub added last when priorities are equal. A `priority` field
overrides that, and the lowest number wins. Put the specific case at 1 and the catch-all
well below the default.

```json title="wiremock/mappings/invoice-missing.json"
{
  "name": "in_missing does not exist",
  "priority": 1,
  "request": {
    "method": "GET",
    "urlPath": "/v1/invoices/in_missing",
    "headers": {
      "x-api-version": { "equalTo": "2024-06-01" }
    }
  },
  "response": {
    "status": 404,
    "jsonBody": { "error": "no such invoice" }
  }
}
```

```json title="wiremock/mappings/invoice-any.json"
{
  "name": "any invoice is a paid invoice",
  "priority": 10,
  "request": {
    "method": "GET",
    "urlPathPattern": "/v1/invoices/[^/]+"
  },
  "response": {
    "status": 200,
    "jsonBody": { "id": "{{request.path.[2]}}", "status": "paid" },
    "transformers": ["response-template"]
  }
}
```

Start it with the mappings directory as its root:

```bash
java -jar wiremock-standalone-3.13.2.jar --port 8080 --root-dir wiremock
```

Run in this container, `in_missing` returned the 404 and `in_1` the paid invoice. Loading the
same two stubs without priorities through the `/__admin/mappings` endpoint, catch-all last,
made the catch-all answer both. The [priority scale](https://wiremock.org/docs/stubbing/) runs from 1 down, with 5
as the default.[^2]

## Fail the test when a stub never answers

A shadowed stub is a stub that never matched, so assert that every stub was used. MSW keeps a
flag on each handler, and nock lists what is still waiting.

```js title="stub-guard.mjs"
export function unusedHandlers(server) {
  return server.listHandlers().filter((h) => h.kind !== 'websocket').map((h) => {
    if (typeof h.isUsed !== 'boolean') {
      throw new Error(`cannot tell whether ${h.info?.header ?? 'a handler'} was used; check the msw version`)
    }
    return h
  }).filter((h) => !h.isUsed).map((h) => h.info.header)
}
```

`isUsed` and `info.header` are public properties of the handler object in msw 3.0.3, but the
[`listHandlers()` reference](https://mswjs.io/api/setup-server/list-handlers) does not describe
them. The guard throws rather than returning an empty list if they disappear, so an upgrade
cannot turn it into a check that always passes. It skips `ws.link()` handlers, which have no
`isUsed` flag, so a suite that stubs a WebSocket beside its HTTP calls still gets an answer
instead of the version error.

Then assert both requests in the same test. Testing only the general request passes under
either ordering, which is how the shadowing gets merged in the first place.

```js title="msw.test.mjs"
test('the specific and the general request, in one test', async () => {
  const { anyInvoice, missingInvoice } = invoiceHandlers()
  const server = start(missingInvoice, anyInvoice)
  try {
    assert.equal(await getInvoice('in_missing'), null)
    assert.deepEqual(await getInvoice('in_1'), { id: 'in_1', status: 'paid' })
    assert.deepEqual(unusedHandlers(server), [], 'every handler answered something')
  } finally {
    server.close()
  }
})
```

## Check it worked

Run both suites. Each file runs in its own process, which keeps msw and nock from intercepting
the same `fetch`.

```bash
node --test msw.test.mjs nock.test.mjs | tail -8
```

```text output
# tests 13
# suites 0
# pass 13
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 388.314206
```

`# fail 0` covers the deliberate failures too. One test builds the merged order, general first,
and asserts that the guard names `GET https://api.example.com/v1/invoices/in_missing`. Another
hands the guard a handler with no `isUsed` flag and expects it to throw, and a third adds a
WebSocket handler to the list and expects the same report as before.

## When it goes wrong

A stub stops matching because the request carries a query parameter or header it did not
declare. The three tools disagree on whether that is a miss at all. nock treats an undeclared
query string as a path mismatch, and its `no match` event names the reason:

```bash
node nock-demo.mjs | sed -n 9,19p
```

```text output
a query parameter the interceptor did not declare
  getInvoice(in_1, expand)   threw: GET /v1/invoices/in_1?expand=customer failed with 501
  response: 501 ERR_NOCK_NO_MATCH
  url in the message: https://api.example.com/v1/invoices/in_1
  closest: GET /v1/invoices/in_1
  reasons: ["Path mismatch: expected /v1/invoices/in_1, got /v1/invoices/in_1?expand=customer"]
a header that differs, reqheaders against matchHeader
  getInvoice(in_2)           threw: GET /v1/invoices/in_2 failed with 501
  reqheaders reasons:   ["Request headers don't match"]
  getInvoice(in_2)           threw: GET /v1/invoices/in_2 failed with 501
  matchHeader reasons:  ["Header mismatch: expected x-api-version to match 2023-10-16, got 2024-06-01"]
```

Three details in that output change how you debug. nock 15.0.1 answers a miss on a mocked host
with a 501 response, a change made in
[pull request 2868](https://github.com/nock/nock/pull/2868) for the 15 betas. nock 14.0.17, run
in this container for comparison, rejected the `fetch` instead. A client that retries 5xx now
retries a missing stub.[^3] The error message prints the URL without its query string, which is the part
that failed. The [`no match` event](https://github.com/nock/nock#global-no-match-event) keeps it,
so read the reasons there. Declare the parameter with
[`.query()`](https://github.com/nock/nock#specifying-request-query-string), and prefer
`matchHeader` over `reqheaders`, because only `matchHeader` names the header that failed.

MSW ignores the query string when matching, so the same request matches `/v1/invoices/:id`.
WireMock splits the difference: `urlPath` ignores the query, and `url` requires it, as its
[request matching](https://wiremock.org/docs/request-matching/) reference shows. In the local
WireMock run, a stub declared with `url` answered the query string with a 404. Its body was a
"Request was not matched" table, the closest stub on the left and `<<<<< URL does not match`
against the line.

A catch-all hides every near miss. In the same WireMock run, a request for `in_missing` with a
different `x-api-version` failed the specific stub, so the priority 10 catch-all answered 200
and WireMock had no miss to report. In MSW or nock the same shape leaves the specific stub
unused, which is the case the unused-stub guard exists to fail.

MSW 3 renamed `onUnhandledRequest` to `onUnhandledFrame`, and the
[migration guide](https://mswjs.io/docs/migrations/2.x-to-3.x) lists the change. Plain
JavaScript passing the old name gets the default, `warn`, and the request leaves the process:

```bash
node msw-demo.mjs 2>&1 | tail -8
```

```text output
unmatched request, MSW 2 option name onUnhandledRequest: error
[MSW] Warning: intercepted a request without a matching request handler:

  • GET http://127.0.0.1:45117/v1/invoice/in_1

If you still wish to intercept this unhandled request, please create a request handler for it.
Read more: https://mswjs.io/docs/http/intercepting-requests
  200 answered by the real server
```

## When not to do this

Do not reach for priorities to make a catch-all safe. A catch-all that answers 200 for anything
turns every typo into a passing test. Prefer a catch-all that answers 501 with the request in the
body, so a miss is loud.

Do not run the unused-stub guard over a shared handler list in every test. A suite-wide list has
handlers that one test never needs, and the guard reports them all. Run it over the handlers a
test added, or once at the end of the file.

Do not move stubs into WireMock to get a `priority` field. A Java server in the loop costs a
startup and a second language for every developer. For a single Node process, order plus the
guard does the same job.

## Related how-tos

- [Stub outbound HTTP calls in Node tests with nock](/howto/stub-outbound-node-http-calls-with-nock)

- [Serve your OpenAPI examples as mock responses](/howto/serve-openapi-examples-as-mock-responses)

- [Simulate slow and broken networks with Toxiproxy](/howto/simulate-slow-networks-with-toxiproxy)

## Last verified

Verified 2026-10-15 against Node 22.22.0, msw 3.0.3 and nock 15.0.1. Every output block is what
the command before it printed. WireMock 3.13.2 ran in this container under Java 21, and what it
returned is described in prose rather than shown as captured output.

[^1]: Mock Service Worker runs no service worker in Node, and `setupServer` starts no server.
Its [reference page](https://mswjs.io/api/setup-server/) says so: despite the word "server" in
the name, it does not establish any servers and operates in the thread of your process. The
name comes from the browser, where the library intercepts requests through the Service Worker
API.

[^2]: The lowest priority is Java's `Integer.MAX_VALUE`, 2,147,483,647, according to the same
[stubbing page](https://wiremock.org/docs/stubbing/). With the default set at 5, four levels
outrank the default and a little over two billion rank below it.

[^3]: [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.6.2) defines 501 as the
answer of a server that does not support what the request requires, which is an accurate
description of a stub nobody wrote. The same section makes a 501 heuristically cacheable unless
something says otherwise, a property a test double rarely has occasion to use.