Inspecting the WireMock Request Journal
A test fails with a 404 from your mock and the stub looks correct. The journal is where that argument ends: it records every request WireMock received, verbatim, and reports which stub came closest to matching and on exactly which attribute it diverged. This page covers reading it, querying it, and converting what you find into an assertion.
Context: the mock is not lying to you
When a request misses every stub, WireMock returns a 404 with a body explaining that no mapping matched. The instinct is to re-read the mapping, decide it looks right, and start changing things at random. The journal removes that step by showing what the server actually received rather than what the code appears to send.
The gap between those two is almost always something the client added on its own: an Accept header the HTTP library sets by default, a charset appended to the content type, a query parameter appended by an interceptor, a trailing slash added by a URL builder. None of these are visible at the call site, and all of them break an exact-match stub.
Solution
1. List what was received
# Everything the mock has seen, newest first
curl -s http://localhost:8080/__admin/requests \
| jq '.requests[] | {method: .request.method, url: .request.url, matched: .wasMatched}'
# Just the misses — usually the whole story
curl -s http://localhost:8080/__admin/requests \
| jq '[.requests[] | select(.wasMatched == false) | .request | {method, url, headers, body}]'
The wasMatched flag is the fastest triage. A journal full of true with one false localises the problem to a single call; a journal that is entirely false usually means the base URL is wrong and nothing is reaching the intended path at all.
2. Ask which stub came closest
The near-miss endpoint is the part most people never discover, and it is the one that actually answers the question:
curl -s http://localhost:8080/__admin/requests/unmatched/near-misses \
| jq '.nearMisses[0] | {
stub: .stubMapping.request.urlPath,
diff: .matchResult.distance,
received: .request.url
}'
WireMock scores every stub against the unmatched request and returns them ranked by distance. The top entry is the stub you meant to hit, and comparing its request block against the recorded one shows the divergence directly. A distance of 0.06 means one attribute out of many differed — look for the single field that is not identical. A distance near 1 means nothing about the two resembles each other, which points at a wrong path or method rather than a detail.
For a human-readable version, WireMock also logs the same comparison when started with --verbose:
Request was not matched
=======================
| Closest stub
-----------------------------------------------------
GET | GET
/api/orders/?locale=en-GB | /api/orders <<<<< URL does not match
The <<<<< marker names the failing attribute. That single line replaces the entire guessing phase.
3. Turn the diagnosis into an assertion
Having found it, stop it recurring. WireMock’s verification endpoint counts matching requests, so a test can assert the call was made with the shape you now know it has:
curl -s -X POST http://localhost:8080/__admin/requests/count \
-H 'Content-Type: application/json' \
-d '{
"method": "GET",
"urlPathPattern": "/api/orders/?",
"queryParameters": { "locale": { "equalTo": "en-GB" } },
"headers": { "Accept": { "contains": "application/json" } }
}' | jq '.count'
Note the matchers: urlPathPattern with an optional trailing slash, contains rather than equalTo on Accept. Matching loosely on attributes the client controls and strictly on the ones your code sets is what makes a stub survive an HTTP-library upgrade.
Wired into a test, that becomes an ordinary expectation:
// tests/orders.integration.test.ts
import { expect, it } from 'vitest';
const ADMIN = process.env.WIREMOCK_ADMIN ?? 'http://localhost:8080/__admin';
async function countRequests(criteria: unknown): Promise<number> {
const res = await fetch(`${ADMIN}/requests/count`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(criteria),
});
const { count } = (await res.json()) as { count: number };
return count;
}
it('requests orders exactly once, with the locale the user selected', async () => {
await loadOrdersPage({ locale: 'en-GB' });
const n = await countRequests({
method: 'GET',
urlPathPattern: '/api/orders/?',
queryParameters: { locale: { equalTo: 'en-GB' } },
});
expect(n).toBe(1); // not 0 (never called) and not 2 (duplicate fetch)
});
Asserting toBe(1) rather than toBeGreaterThan(0) catches the duplicate-fetch bug that a React effect with a missing dependency array produces — a defect no response-level assertion will ever see.
Verification
# The journal is recording at all
curl -s http://localhost:8080/__admin/requests | jq '.requests | length'
# No unmatched requests remain after the fix
curl -s http://localhost:8080/__admin/requests \
| jq '[.requests[] | select(.wasMatched == false)] | length' # expect 0
The second command is worth running as the last step of any integration job. A run that passes with unmatched requests in the journal is passing by luck — some call fell through to a default and the assertion happened not to notice.
Capture it as an artefact when the job fails, so the record survives the container:
- name: Capture the mock journal on failure
if: failure()
run: curl -s http://localhost:8080/__admin/requests > wiremock-journal.json
- uses: actions/upload-artifact@v4
if: failure()
with:
name: wiremock-journal
path: wiremock-journal.json
Gotchas and edge cases
-
The journal is capped and silently drops the oldest entries. WireMock keeps a bounded number of requests (
--max-request-journal-entries). On a long run the request you are hunting may already have been evicted. Raise the cap for the debugging run, or reset the journal immediately before the failing action so it contains only that. -
Request bodies are recorded, including anything sensitive. Auth headers, tokens and personal data in payloads all end up in the journal, and uploading it as a CI artefact publishes them to anyone with repository access. Scrub before uploading, or restrict the artefact — the same care described in mock data privacy and anonymisation.
-
Counting requests across parallel jobs gives nonsense. One WireMock shared by several concurrent test jobs has one journal, so a count assertion sees every job’s traffic. Either give each job its own instance, or add a per-job header to every request and include it in the count criteria so the assertion only sees its own.
Reading the journal as a narrative
The journal is usually consulted as a lookup — find the failing request, read it, move on. Reading it in order, as a sequence, answers a different and often more useful question: what did the application actually do?
A page that was expected to make three calls and made seven has a request waterfall problem visible nowhere else. Calls arriving in an unexpected order reveal a race. The same endpoint appearing twice within milliseconds is a double-fetch. An endpoint that never appears at all means a code path did not run, which is frequently the real bug rather than anything about the response.
None of these are visible from a single entry, and none of them are visible from the rendered output. Ten seconds spent reading the journal top to bottom, before filtering it, is one of the higher-yield debugging habits available in this whole area.
Related
- Hot-Reloading Mock Definitions Without Restart — reloading stubs while keeping the journal you are reading
- Managing Mock Server Lifecycles in Docker — where the journal is archived on teardown
- Running WireMock in Docker Compose — exposing the admin port the journal lives behind
← Back to Mock Lifecycle Management