When an expectation does not match a request, MockServer returns a 404 response. This page explains how to diagnose and fix matching issues.

 

Debugging Checklist

Follow these steps in order when a request is not matching:

1. Is the expectation active?

Expectations expire after their configured times limit or timeToLive. Retrieve active expectations to confirm yours is still present:

Expectation[] active = mockServerClient.retrieveActiveExpectations(null);
curl -X PUT "http://localhost:1080/mockserver/retrieve?type=ACTIVE_EXPECTATIONS"

2. Did the request arrive at MockServer?

Verify that MockServer received the request:

HttpRequest[] requests = mockServerClient.retrieveRecordedRequests(null);
curl -X PUT "http://localhost:1080/mockserver/retrieve?type=REQUESTS"

3. Check the match failure reason

MockServer logs an EXPECTATION_NOT_MATCHED event for every expectation that was compared. Each event includes a because section explaining which fields matched and which did not:

curl -X PUT "http://localhost:1080/mockserver/retrieve?type=LOGS"

In the dashboard UI, look for log entries with the pink/rose colour. Click the ... to expand the "because" section, which shows each field result colour-coded green (matched) or red (didn't match).

4. Enable full match diagnostics

By default, MockServer stops comparing fields at the first mismatch (matchersFailFast=true). To see all mismatching fields at once, disable fail-fast:

ConfigurationProperties.matchersFailFast(false);
# System property
-Dmockserver.matchersFailFast=false

# Environment variable
MOCKSERVER_MATCHERS_FAIL_FAST=false

This shows every field that does not match in a single log entry, making it much easier to identify all issues at once rather than fixing them one at a time.

5. Look for the closest match

When no expectation matches, MockServer logs a closest match summary showing which expectation was most similar to the request and how many fields matched (e.g., "matched 8/12 fields"). This immediately tells you which expectation to investigate.

 

Common Pitfalls

These are the most frequent reasons expectations fail to match:

Trailing slash in path

A path of /api/users does not match /api/users/. Ensure your expectation path matches exactly, or use a regex:

// Exact match - will NOT match "/api/users/"
.withPath("/api/users")

// Match with or without trailing slash
.withPath("/api/users/?")

MockServer will show a HINT in the match failure when it detects a trailing slash mismatch.

Content-Type charset mismatch

Many HTTP clients send Content-Type: application/json; charset=utf-8 but expectations often specify just application/json. This is a header value mismatch.

Solutions:

  • Include the charset in your expectation: .withHeader("Content-Type", "application/json; charset=utf-8")
  • Use a substring header matcher
  • Omit the Content-Type header from your expectation (unspecified headers match anything)

MockServer will show a HINT when it detects this pattern.

JSON body matching semantics

By default, JSON body matching uses ONLY_MATCHING_FIELDS mode:

  • Extra fields in the request body are ignored (the expectation is a subset match)
  • Missing fields (in the request but expected in the matcher) cause a failure
  • Array order does not matter
  • Extra array items are ignored

If you need an exact match, use STRICT mode:

// Subset match (default) - extra fields OK
.withBody(json("{\"name\": \"foo\"}", MatchType.ONLY_MATCHING_FIELDS))

// Strict match - no extra fields allowed
.withBody(json("{\"name\": \"foo\"}", MatchType.STRICT))

Regex metacharacters in paths

Paths are matched as regular expressions. Characters like ., *, +, ?, (, ), [, { have special meaning in regex.

For example, /api/v1.0/users will also match /api/v1X0/users because . matches any character. Use \. for a literal dot:

// This matches "/api/v1X0/users" too
.withPath("/api/v1.0/users")

// This only matches "/api/v1.0/users"
.withPath("/api/v1\\.0/users")

MockServer will show a HINT when it detects unescaped dots in paths.

Header name case sensitivity

HTTP header names are matched case-insensitively (per HTTP spec), but header values are case-sensitive. For example:

// Header name "content-type" matches "Content-Type" - OK
// Header value "Application/JSON" does NOT match "application/json"
.withHeader("Content-Type", "application/json")

Query parameter encoding

Query parameters are decoded before matching. ?name=hello%20world is matched against the decoded value hello world:

// Matches ?name=hello%20world and ?name=hello+world
.withQueryStringParameter("name", "hello world")

Body type confusion

Using the wrong body matcher type is a common mistake:

// This is an EXACT string match - entire body must match this text exactly
.withBody("{\"name\": \"foo\"}")

// This is a JSON subset match - only specified fields must be present
.withBody(json("{\"name\": \"foo\"}"))

// This is a regex match
.withBody(regex(".*foo.*"))

If your JSON body isn't matching, check that you are using json() and not a plain string body matcher.

 

Reading the "because" Output

When a request does not match an expectation, the log entry includes a "because" section. Here is an annotated example:

method matched                        ← GET matched GET
path didn't match:                    ← path comparison failed

  string or regex match failed        ← the matcher type used
  expected: /api/users                ← what the expectation specified
  found: /api/user                    ← what was in the request
HINT: trailing slash mismatch ...     ← actionable suggestion (when applicable)
body matched                          ← body was not checked (no body matcher)
headers matched                       ← all expected headers were found
cookies matched                       ← all expected cookies were found

Fields are listed in matching order. When matchersFailFast=true (default), only fields up to and including the first failure are shown. Set matchersFailFast=false to see all fields.

 

Debugging Configuration Properties

Property Default Description
matchersFailFast true When true, matching stops at the first non-matching field. Set to false to see all mismatching fields in a single log entry.
detailedMatchFailures true When true, match failure log entries include detailed explanations of why each field did not match (expected vs actual values).
logLevel INFO At INFO (default), all match results are logged with full detail. Set to TRACE for internal matcher-level diagnostics.

For full configuration details, see the Configuration page.

 

Programmatic Mismatch Debugging

MockServer provides a dedicated PUT /mockserver/debugMismatch endpoint that analyzes why a request does not match any active expectations. It returns structured JSON showing per-expectation, per-field match results — including the closest match.

REST API

curl -X PUT "http://localhost:1080/mockserver/debugMismatch" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "GET",
    "path": "/api/users",
    "headers": {
      "Content-Type": ["application/json"]
    }
  }'

Java Client

String result = mockServerClient.debugMismatch(
    request()
        .withMethod("GET")
        .withPath("/api/users")
        .withHeader("Content-Type", "application/json")
);
System.out.println(result);

Response Format

The response is a JSON object with:

  • totalExpectations — the number of active expectations compared
  • closestMatch — which expectation had the fewest differences
  • results — per-expectation match analysis with field-level differences
{
  "totalExpectations": 2,
  "closestMatch": {
    "expectationId": "abc-123",
    "matchedFields": 10,
    "totalFields": 12
  },
  "results": [
    {
      "expectationId": "abc-123",
      "expectationPath": "/api/users",
      "expectationMethod": "POST",
      "matches": false,
      "matchedFieldCount": 10,
      "totalFieldCount": 12,
      "differences": {
        "method": [
          "string or regex match failed expected: POST found: GET"
        ]
      }
    }
  ]
}

Worked Example: "My test is getting 404"

When your test receives a 404, use the debug endpoint to find out exactly why:

// 1. Set up your expectation
mockServerClient.when(
    request().withMethod("POST").withPath("/api/users")
        .withBody(json("{\"name\": \"foo\"}"))
).respond(response().withStatusCode(201));

// 2. Your test sends a request that gets 404 - why?
// 3. Debug the mismatch
String analysis = mockServerClient.debugMismatch(
    request()
        .withMethod("GET")           // oops - wrong method!
        .withPath("/api/users")
        .withBody("{\"name\": \"foo\"}")
);

// The response will show:
// - method didn't match: expected POST, found GET
// - matchedFieldCount: 11/12 (all fields matched except method)

This endpoint is also available via the MCP debug_request_mismatch tool for AI-assisted debugging.

 

Retrieving Logs by Correlation ID

Every incoming HTTP request is assigned a unique correlation ID. All log entries for that request's lifecycle (received, match attempts, response) share this ID. This is invaluable when debugging a specific request among many — you can isolate exactly what happened.

Finding the Correlation ID

The correlation ID appears in:

  • Dashboard UI — as a small chip on grouped log entries (click to copy)
  • Log text — in the prefix of each log message
  • Typed log entries — as the correlationId field when using format=LOG_ENTRIES

Retrieving by Correlation ID

Once you have a correlation ID, retrieve all log entries for that request:

curl -X PUT "http://localhost:1080/mockserver/retrieve?type=LOGS&correlationId=abc-123-def-456"
String logs = mockServerClient.retrieveLogsByCorrelationId("abc-123-def-456");

The correlation ID is visible in the dashboard UI as a small chip on grouped log entries. Click it to copy the ID.

 

Typed Log Entry Retrieval

Instead of retrieving logs as plain text, you can retrieve structured LogEntry objects. This is useful for programmatic analysis — filtering by type, inspecting expectations, or building custom reports:

LogEntry[] entries = mockServerClient.retrieveLogEntries(null);

LogEntry[] matchEntries = mockServerClient.retrieveLogEntries(
    request().withPath("/api/users")
);

// Deserialized entries include: type, logLevel, correlationId, epochTime,
// timestamp, port, expectationId, messageFormat, arguments, because.
// Note: httpRequest, httpResponse, expectation, throwable are in the JSON
// but not deserialized (returned as null on the client).
for (LogEntry entry : entries) {
    System.out.println(entry.getType() + " at " + entry.getTimestamp());
}
LogEntry[] entries = mockServerClient.retrieveLogEntriesByCorrelationId("abc-123-def-456");
# REST API - use format=LOG_ENTRIES to get structured JSON
curl -X PUT "http://localhost:1080/mockserver/retrieve?type=LOGS&format=LOG_ENTRIES"

# Combined with correlation ID filter
curl -X PUT "http://localhost:1080/mockserver/retrieve?type=LOGS&format=LOG_ENTRIES&correlationId=abc-123-def-456"
 

Time-Filtered Retrieval

When debugging intermittent issues, you can narrow log entries to a specific time window:

long now = System.currentTimeMillis();
LogEntry[] recentEntries = mockServerClient.retrieveLogEntries(
    null, now - 30_000, now
);