In MockServer, why does `withHeader("X-Sweep-Tenant", "acme.co")` also match a request sending `acmeXco`?
answer
- exact comparison first, pattern second
- the expectation's text is the pattern
- a dot is any character
- the pattern must match in full
- escape it, or quote it
basics
~20 sMockServer compares a header value literally first, then retries it as a regular expression. The dot in acme.co is therefore a wildcard that any character satisfies, so acmeXco matches as well. Escape the metacharacter to pin the value.
solid answer
~40 sMockServer does not treat a header, cookie or query-string value as a plain string. Its string matcher tries an exact comparison first and, when that fails, compiles **the text you wrote in the expectation** as a regular expression and applies it to the value the request actually sent. So MockServer's `withHeader("X-Sweep-Tenant", "acme.co")` does match the literal tenant `acme.co` — the exact comparison wins — but it also matches `acmeXco` and `acme7co`, because `.` is the any-character metacharacter. The fix is to escape what you meant literally, `"acme\\.co"`, or to wrap a runtime value in `Pattern.quote(...)`. Two details save time later: MockServer applies the pattern to the whole value, so `acme` on its own does not match `acme.co`, and the identical rule governs MockServer's `withQueryStringParameter` and `withCookie` on the same stub.
code
java · 23 linesimport static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import java.util.regex.Pattern;
import org.mockserver.client.MockServerClient;
MockServerClient client = new MockServerClient("localhost", 1080);
// Escaped dot: only the tenant acme.co matches, not acmeXco.
client.when(
request()
.withMethod("GET")
.withPath("/v1/sweeps")
.withHeader("X-Sweep-Tenant", "acme\\.co")
.withQueryStringParameter("borough", "camden"))
.respond(response().withStatusCode(200).withBody("{\"sweeps\":[]}"));
// Same guarantee for a tenant value that arrives from a fixture.
client.when(
request()
.withPath("/v1/sweeps")
.withHeader("X-Sweep-Tenant", Pattern.quote(tenantFromFixture)))
.respond(response().withStatusCode(200));go deeper
Know that everything on a MockServer expectation's request side is a predicate over the incoming call, and that the header, cookie and query values you write there are compared against what the client sent.
Be ready to explain MockServer's two-step comparison: an exact match first, then the expectation's own text compiled as a regular expression and applied across the whole value the request carried.
Show how you would find this in a suite that is green but wrong. Reproduce with a tenant you never seeded, then escape or quote every value that reached the MockServer expectation from configuration or a fixture.
Own the convention. Decide whether values in stub definitions are literal by default with patterns opt-in, and say how that is enforced when definitions are generated, shared between teams or reviewed at scale.
MockServer's request side is a set of predicates, and the part that surprises people is that a value you type as a plain string is not compared as one. ## MockServer's two-step comparison A MockServer expectation is a pair: a `request()` object saying which incoming calls it applies to, and a `response()` object saying what to send back. Everything on MockServer's `request()` side — `withQueryStringParameter`, `withHeader`, `withCookie` — is a **predicate**. MockServer holds those names and values as `NottableString` values and runs them through one shared string matcher, which does two things in order: 1. Compare the expectation's text and the request's text for **exact equality**. If they are equal the predicate is satisfied and nothing further happens. 2. If they are not equal, compile **the expectation's own text** as a regular expression and apply it to the value the request sent, requiring it to match across the **whole** value. Step two is the whole of the answer. In MockServer the pattern is the string you wrote and the request's value is the subject it is applied to; getting that direction backwards is the commonest way this behaviour is mis-explained in a code review. ## Why the tenant header over-matches The chimney-sweep scheduling stub answers `GET /v1/sweeps` per tenant, and the tenant arrives in the `X-Sweep-Tenant` header. Written as MockServer's `withHeader("X-Sweep-Tenant", "acme.co")`, the expectation reads to a human as "the tenant acme.co". To the matcher it reads as "acme, then any single character, then co", because `.` is the regular-expression metacharacter for any character. Calls from `acmeXco`, `acme7co` and `acme-co` all satisfy it, and each of them is handed the tenant-scoped booking list that belongs to somebody else. Nothing complains, which is what makes it expensive: the match produces the ordinary 200 the test expects, so the suite stays green while the predicate quietly answers calls it was never written for. Three consequences follow from the same rule: - **A value with no metacharacters behaves exactly as written.** MockServer's `withQueryStringParameter("trade", "sweep")` gives the regular-expression engine nothing to reinterpret, so the fallback and the literal comparison agree. - **A fragment does not match a longer value.** In MockServer the pattern must cover the whole value, so `withHeader("X-Sweep-Tenant", "acme")` does *not* match a call sending `acme.co`. People who have only just met the fallback usually guess the opposite. - **Metacharacters you were not thinking about count too.** `+`, `?`, `(`, `)`, `[`, `|`, `*` and `$` turn up in real header and query values — API keys, versioned media types, base64 padding, date ranges — and MockServer honours every one of them. ## Pinning a value you meant literally - **Escape the metacharacter where you write it.** MockServer reads `withHeader("X-Sweep-Tenant", "acme\\.co")` as the pattern `acme\.co` — one backslash in the regular expression, doubled in Java source. - **Quote anything that arrives at runtime.** Handing MockServer `Pattern.quote(tenantFromFixture)` wraps the whole string so nothing inside it is interpreted. Make that the default wherever a value comes from a fixture, an environment variable or a generated identifier, rather than auditing values one at a time. - **Use MockServer's fallback on purpose where it earns its keep.** `withQueryStringParameter("from", "2026-02-\\d{2}")` accepts any February booking date, which is far better than pinning a date the test cannot control. - **Do not anchor by hand.** MockServer already requires the pattern to cover the whole value, so `^` and `$` add nothing; putting `.*` at both ends is how you deliberately turn the predicate into a substring test, which is rarely what you meant. ## The rule is uniform across MockServer's request side | MockServer predicate | example on the sweep scheduling stub | how the value is compared | |---|---|---| | MockServer `withHeader` | `withHeader("X-Sweep-Tenant", "acme\\.co")` | exact, then regular expression | | MockServer `withQueryStringParameter` | `withQueryStringParameter("borough", "camden")` | exact, then regular expression | | MockServer `withCookie` | `withCookie("sweepSession", "[0-9a-f]{32}")` | exact, then regular expression | The cookie row shows the fallback doing useful work: a session identifier the test cannot predict is matched by shape rather than by value, which is exactly the case MockServer's regular-expression path exists for. ## What the neighbouring stub servers do In WireMock the choice is made explicit by the matcher you pick, since WireMock's `withHeader("X-Sweep-Tenant", equalTo("acme.co"))` is a literal comparison and WireMock's `equalToIgnoreCase(...)` is its case-blind form, so there is no silent fallback to be caught by. Mountebank differs again. Mountebank's predicates are case-insensitive unless its `caseSensitive` field says otherwise, and Mountebank's `except` field takes a regular expression whose matches are stripped from both sides before the comparison happens — which is how you ignore a prefix or a wrapper without loosening the predicate itself. ## Working the diagnosis 1. Reproduce with a value you did **not** intend to match — send `acmeXco` and watch the MockServer expectation answer it. 2. Read every value in the MockServer expectation as a regular expression rather than as a string, and mark the metacharacters. 3. Escape the ones that were meant literally, or wrap the value in `Pattern.quote`. 4. Re-run the negative case: it should now go unmatched, while the positive case still answers. The habit worth taking away is that on MockServer's request side "literal" is a decision you make, not a default you get.
- The tenant identifier is generated per test run. How do you keep the MockServer header predicate literal?Quote it before it reaches the matcher, with MockServer's `withHeader("X-Sweep-Tenant", Pattern.quote(tenant))`. MockServer then sees a pattern in which every character is literal, because `Pattern.quote` wraps the string in `\Q` and `\E`. Doing that where the fixture value enters the expectation is far safer than auditing values one by one, and it costs nothing when the value contains no metacharacters at all.
- Does the same exact-then-regex rule apply to a MockServer cookie predicate?Yes. In MockServer `withCookie(...)`, `withHeader(...)` and `withQueryStringParameter(...)` all funnel into the same string matcher, so a cookie value is compared literally first and as a regular expression afterwards. That makes MockServer's `withCookie("sweepSession", "[0-9a-f]{32}")` a deliberate and useful pattern — and a session value containing a `+` or a `?` an accidental one.
A dot in a regular expression is the blank square in a crossword: any single letter fits it. Writing a dotted tenant name straight into a matcher pencils a blank into the grid, and then a word you never intended fits.
saying these in an interview costs you the question
- Says MockServer compares header values only as plain literal strings
- Thinks the request's value is the pattern and the expectation's the subject
- Adds a wildcard to loosen matching without noticing one is already there
- Believes a short fragment such as acme matches the longer value acme.co
- Blames the path predicate and rewrites it instead of the header value