The API container calls the shipping partner through WireMock and gets 404 with the body No response could be served as there are no stub mappings in this WireMock instance, or Request was not matched followed by a near-miss diff that shows the stub expected /v2/quotes and the client sent /v2/quotes/. Or everything works, but only the happy path is stubbed, so the code that handles the partner's 429 and timeout responses has never run anywhere except production. This page sets up WireMock in Compose with mappings that match reliably and cover the failure modes that matter, as part of mocking external APIs and third-party services.

WireMock is the right tool when you need exact control over responses: specific error codes, delays, sequences and assertions about what the application sent.

Diagnostic

Check what WireMock has loaded and what it received that matched nothing:

#!/usr/bin/env bash
set -euo pipefail
curl -fsS http://localhost:8089/__admin/mappings | jq '.meta.total'
docker compose exec -T shipping-mock ls -R /home/wiremock | head -8
curl -fsS http://localhost:8089/__admin/requests/unmatched | jq -r '.requests[] | "\(.method) \(.url)"' | sort | uniq -c
curl -fsS http://localhost:8089/__admin/requests/unmatched/near-misses | jq -r '.nearMisses[0].matchResult.distance // "none"'

Expected bad output:

0
/home/wiremock:
__files
mocks
      3 POST /v2/quotes/
none

WireMock loaded zero mappings because they were mounted into /home/wiremock/mocks instead of /home/wiremock/mappings, and the client sends a trailing slash the stubs would not have matched anyway.

Why WireMock Returned 404 Decision diagram mapping WireMock 404 causes to loading or matching problems. Why WireMock Returned 404 How many mappings are loaded? zero wrong mount path or bad JSON some, near miss shown URL or body mismatch some, no near miss method or host differs
Check whether mappings loaded before debugging why a request did not match them.

Root cause

WireMock loads stub definitions from a mappings directory and response bodies from __files under its root (/home/wiremock in the official image). Mounting a directory at any other path, or a JSON syntax error in one file, leaves it with zero mappings and every request returns the generic 404. Once mappings load, matching is exact by default: url matches the full path and query string exactly, so a trailing slash, an extra query parameter or different parameter order all miss. urlPath ignores the query, urlPathPattern accepts a regex, and body matchers such as equalToJson compare structure rather than bytes. Near-miss reports exist precisely because these small mismatches are the common failure; reading them is faster than guessing.

Response templating adds a second, quieter failure mode. With --global-response-templating enabled, any {{ ... }} sequence in a response body is interpreted as a Handlebars expression. A copied real response that happens to contain double braces — a templated email body, a Mustache fragment in a CMS payload — then renders as empty or throws a template error, and the stub appears to return the wrong data. Either disable templating globally and enable it per mapping with "transformers": ["response-template"], or escape the braces in those bodies.

The happy-path-only problem is a different kind of cause: stubs are usually written by copying one real response, so failure behaviour is never modelled. The partner's rate limiting, timeouts and validation errors then exist only in production.

Resolution

  1. Mount mappings at the path WireMock reads and give the service a healthcheck:
services:
  shipping-mock:
    image: wiremock/wiremock:3.9.1
    command: ["--global-response-templating", "--disable-banner", "--verbose"]
    ports:
      - "127.0.0.1:8089:8080"
    volumes:
      - ./mocks/shipping/mappings:/home/wiremock/mappings:ro
      - ./mocks/shipping/__files:/home/wiremock/__files:ro
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/__admin/health"]
      interval: 5s
      retries: 20
  1. Match on what matters, tolerantly. Use urlPathPattern for paths that may carry a trailing slash, and JSON-path matchers for bodies:
{
  "name": "quote success",
  "request": {
    "method": "POST",
    "urlPathPattern": "/v2/quotes/?",
    "headers": { "Authorization": { "matches": "Bearer .+" } },
    "bodyPatterns": [{ "matchesJsonPath": "$[?(@.destination.country == 'DE')]" }]
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "quote_id": "{{randomValue type='UUID'}}", "amount_cents": 1299, "currency": "EUR" }
  }
}
  1. Add failure scenarios as separate mappings, selected by a test-controlled input such as a magic country code:
{
  "name": "quote rate limited",
  "request": { "method": "POST", "urlPathPattern": "/v2/quotes/?", "bodyPatterns": [{ "matchesJsonPath": "$[?(@.destination.country == 'XR')]" }] },
  "response": { "status": 429, "headers": { "Retry-After": "2" }, "jsonBody": { "error": "rate_limited" } }
}
{
  "name": "quote timeout",
  "request": { "method": "POST", "urlPathPattern": "/v2/quotes/?", "bodyPatterns": [{ "matchesJsonPath": "$[?(@.destination.country == 'XT')]" }] },
  "response": { "status": 200, "fixedDelayMilliseconds": 15000, "jsonBody": { "quote_id": "late" } }
}
  1. Model sequences with scenarios when the partner's state changes between calls, such as a shipment moving from created to in_transit:
{
  "scenarioName": "shipment lifecycle",
  "requiredScenarioState": "Started",
  "newScenarioState": "in_transit",
  "request": { "method": "GET", "urlPathPattern": "/v2/shipments/[a-z0-9-]+" },
  "response": { "status": 200, "jsonBody": { "status": "created" } }
}

A second mapping with "requiredScenarioState": "in_transit" returns the next state. Tests reset scenarios with POST /__admin/scenarios/reset.

  1. Point the application at the mock through its base URL variable and assert on received requests in tests:
#!/usr/bin/env bash
set -euo pipefail
docker compose up -d --wait shipping-mock api
curl -fsS -X POST http://localhost:8089/__admin/requests/count \
  -H 'content-type: application/json' -d '{"method": "POST", "urlPathPattern": "/v2/quotes/?"}' | jq '.count'
A Scenario Set for One Endpoint Layers of stub mappings for the quote endpoint, from success to timeout. A Scenario Set for One Endpoint success 200 with quote validation error 422 unsupported country rate limited 429 with Retry-After server error 503 from partner timeout 15 s delay
Each mapping is selected by a test-controlled input, so every branch of client code can run locally.

Expected output

$ curl -fsS http://localhost:8089/__admin/mappings | jq '.meta.total'
6
$ curl -fsS -X POST http://localhost:8089/v2/quotes/ -H 'Authorization: Bearer test' -H 'content-type: application/json' -d '{"destination":{"country":"DE"}}'
{"quote_id":"5e0c7a1b-7a3e-4c61-9d8e-2f1b0e6c9a44","amount_cents":1299,"currency":"EUR"}
$ curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8089/v2/quotes -H 'Authorization: Bearer test' -H 'content-type: application/json' -d '{"destination":{"country":"XR"}}'
429

All mappings load, requests match with or without the trailing slash, and failure scenarios are reachable on demand — so the client's retry and timeout handling can be tested locally.

Run the application's own integration tests against the mock and then check the unmatched-requests endpoint once more. An empty list at the end of a full test run means every call the application makes is covered by a mapping; any entry is either a missing scenario or a client bug, and both are worth knowing about before a release.

Prevention

  1. Fail tests on unmatched requests. After the integration suite, query /__admin/requests/unmatched and fail if it is not empty; unmatched calls mean the client changed and the stubs did not.

  2. Validate mapping JSON in CI (jq empty mocks/**/mappings/*.json) so a syntax error does not silently leave WireMock with no mappings.

  3. Add a mapping for every production incident involving the partner, reproducing the behaviour that caused it.

Client Code Paths Exercised Locally Bar chart of how many client error-handling branches run locally before and after adding failure scenarios. Client Code Paths Exercised Locally happy path only 3 of 11 plus 4xx and 5xx 8 of 11 plus timeout, sequence 11 of 11
Counted from coverage of the shipping client; failure scenarios cover the branches that fail in production.

Platform caveats

Apple Silicon (ARM64): wiremock/wiremock images are multi-arch and run natively.

macOS (Docker Desktop): mapping files are read at startup and on POST /__admin/mappings/reset; bind-mounted edits do not reload automatically. Call the reset endpoint after editing stubs, or restart the container.

WSL2: keep the mocks/ directory in the Linux filesystem; mounting from /mnt/c works but CRLF line endings in JSON bodies can surprise exact-match body stubs.

Rollback

Point the base URL back at the partner's sandbox and remove the service; mapping files can stay for later use:

#!/usr/bin/env bash
set -euo pipefail
docker compose rm -sf shipping-mock
sed -i.bak 's|^SHIPPING_BASE_URL=.*|SHIPPING_BASE_URL=https://sandbox.shipfast.example|' .env

Frequently Asked Questions

Why does WireMock say there are no stub mappings?

Mappings were not loaded, usually because they are mounted at the wrong path or one file is invalid JSON. Mount them at /home/wiremock/mappings and check GET /__admin/mappings for the count.

How do I match a URL regardless of query parameters?

Use urlPath or urlPathPattern instead of url, and add queryParameters matchers only for parameters that matter. url requires an exact match of path and query string.

Can WireMock simulate slow or broken responses?

Yes. fixedDelayMilliseconds and delayDistribution add latency, and fault values such as CONNECTION_RESET_BY_PEER and MALFORMED_RESPONSE_CHUNK simulate network failures.

How do tests select a failure scenario?

Use a request attribute the test controls — a magic value in the body, a header such as X-Mock-Scenario, or a path segment — and give each failure mapping a matcher on it. Keep the magic values documented next to the mappings.