Most of these changes need an action only if you use the specific feature or client they name. But five of them can affect a plain HTTP mock with no special configuration: a statusCode outside 100–999 is now rejected (no opt-out), an unexpected control-plane failure now answers 500 instead of 400, an over-count verification failure's message wording changed, a repeated id in an initialisation file now keeps the last entry instead of the first, and maxLoggedMatchFailures now caps did-not-match log entries at 50 by default. Check the table below against what you use.

MockServer 9.0.0 is a major release. Most of its breaking changes are opt-out, feature-gated, or fix a matcher that used to be silently ignored, but not all — see the callout above for the ones that can affect a plain HTTP mock. This page lists every BREAKING entry from the changelog, plus the Go client's module-path move (tracked separately, not yet a changelog entry at the time of writing), grouped by who it affects, with the exact property or code change to make.

 

Who needs to act

Change Who's affected What to do
Failed template answers 500Anyone using a response or forward templateHandle a 500 instead of a 404/502, or set templateFailureAsServerError=false
maxLoggedMatchFailures defaults to 50Anyone with >50 expectations who reads did-not-match log entries at INFOSet maxLoggedMatchFailures=-1 to keep logging every comparison
Init files: repeated id keeps the last entryAnyone with a repeated id across expectations in an initialisation fileRemove the repeated ids, or reorder so the one you want is last
Binary expectations now match while proxyingAnyone who proxies binary (non-HTTP) traffic and has binary expectations registered on the same MockServerSet matchBinaryExpectationsWhenProxying=false to keep forwarding everything
Binary forwarder removed; one upstream connection per clientAnyone who proxies a binary (non-HTTP) protocolRemove forwardBinaryRequestsWithoutWaitingForResponse; verify your upstream proxy allows CONNECT to non-443 ports; drop any calls to the removed Java APIs
Response statusCode must be 100–999Anyone with an expectation or file whose statusCode is 0 or otherwise out of rangeFix the status code; invalid entries in files are skipped with a WARN, not fatal
HTTP/3 needs an opt-in jar/image and now refuses to start without itAnyone who sets http3PortUse the jar-with-dependencies-http3 jar or the -http3 Docker tag / Helm image.variant=http3
DNS mocking refuses to start if its port can't be usedAnyone who sets dnsEnabled=true with a fixed dnsPortFree the port, or leave dnsPort=0 (default) so the OS picks one
Unexpected control-plane failures answer 500, not 400Code that treats any control-plane 400 as "my input was wrong", or catches the Java client's bare IllegalArgumentExceptionTreat 500 as an internal MockServer fault (correlation id in the body and log); 400 still means your request was rejected
Control-plane mTLS trusts only the configured CA chainAnyone using controlPlaneTLSMutualAuthenticationCAChain with certificates signed by MockServer's own CA; also anyone with control-plane mTLS required, no chain set, and the bundled default CA — the server now refuses to startAdd MockServer's CA certificate to the chain (server and Java client each need this separately), or set certificateAuthorityCertificate/certificateAuthorityPrivateKey to give MockServer its own CA
Over-count verification failures reword "not found" to "found"Tests that assert the exact text of a never()/atMost/exactly/between failure messageUpdate the assertion to the new wording
Response-body verification now applies the matcherResponse verifications using a regex/JSON path/JSON Schema/XPath/XML Schema/form/multipart/fuzzy/GraphQL/JSON-RPC/WASM/allOf matcher, or matchType/notFix the matcher or the system under test — a verification that only passed because the matcher was ignored now fails
OpenAPI request verification now counts only matching operationsVerifications using verify(openAPI(...), ...)Fix the matcher or the system under test if a verification now fails
AsyncAPI verify rejects unknown/top-level count fieldsCallers of PUT /mockserver/asyncapi/verify putting atLeast/atMost/exactly at the top levelMove them inside a count object
LOG_ENTRIES bodies written once, not three timesCode reading a body from an entry's message or argumentsRead the body from the entry's httpRequest/httpResponse instead
GenAI trace spans renamed for Gemini and text completionsTraces, dashboards or alerts matching gen_ai.operation.name == "chat"Match generate_content (Gemini) or text_completion (legacy/FIM/Ollama/Bedrock prompt-string) too
Node client: mockserver-client/llm typings drop their default exportTypeScript users importing mockserver-client/llmUse the named export instead of the default
Node client/launcher gain an exports mapAnyone deep-importing a path the map doesn't name (a .d.ts file, test/, tasks/mockServer.js)Import the package itself or a listed path instead
Node client: recorded-request methods now typed as requests, not responsesTypeScript code typing results as HttpResponse[] or reading .headers/.body/.cookies directlyNarrow each result to HttpRequest first
Rust client enums are now #[non_exhaustive]Rust code with an exhaustive match on a client enumAdd a wildcard _ => arm
Go module path moves to /v9Go users of mockserver-client-go or mockserver-testcontainers's Go moduleUpdate the import path and run go get .../v9
 

Server configuration

Three defaults change behaviour for an unmodified configuration, each with an opt-out property (templateFailureAsServerError, maxLoggedMatchFailures, matchBinaryExpectationsWhenProxying), plus stricter statusCode validation, which has no opt-out.

 

A response or forward template that fails now answers 500, not 404

templateFailureAsServerError is now true by default. A response or forward template that throws, fails to parse, times out, or renders output that isn't a valid response or request now answers 500 with an x-mockserver-template-error: true header and the template error as the body, instead of the 404 (or 502 for a forward template) it answered in 8.0.0. The error is still logged as before.

To keep the 8.0.0 behaviour:

-Dmockserver.templateFailureAsServerError=false

or

MOCKSERVER_TEMPLATE_FAILURE_AS_SERVER_ERROR=false

See Configuration – Answer a failed template with a 500.

 

maxLoggedMatchFailures now defaults to 50

At INFO, a request compared against more than 50 expectations now logs only the first 50 "request didn't match expectation" entries, with the closest-match entry saying how many were left out. Every expectation is still compared, so which expectation serves the request, the closest-match entry, the 404 hint and explain output are all unchanged — only how many of these log entries retrieveLogMessages, LOG_ENTRIES output and the dashboard log show.

If your tests or tooling count these entries, or expect one per expectation:

-Dmockserver.maxLoggedMatchFailures=-1

or

MOCKSERVER_MAX_LOGGED_MATCH_FAILURES=-1

-1 restores the 8.0.0 behaviour of logging every comparison. See Configuration – Max Logged Match Failures.

 

Binary expectations now match messages on a proxied connection by default

In 8.0.0, binary expectations were never consulted on a connection MockServer proxies to an upstream (port forwarding, a PROXY protocol header, transparent proxying). With matchBinaryExpectationsWhenProxying now true by default, a message on such a connection whose bytes match a binary expectation is answered by that expectation and is not forwarded; every other message is forwarded as before. This only affects you if you proxy binary (non-HTTP) traffic and have binary expectations registered on the same MockServer.

To keep the 8.0.0 behaviour and always forward:

ConfigurationProperties.matchBinaryExpectationsWhenProxying(false)

or

-Dmockserver.matchBinaryExpectationsWhenProxying=false

or

MOCKSERVER_MATCH_BINARY_EXPECTATIONS_WHEN_PROXYING=false

or at runtime:

PUT /mockserver/configuration
{
  "matchBinaryExpectationsWhenProxying": false
}
 

A response statusCode outside 100–999 is now rejected

A blank status code in the 8.0.0 dashboard became 0 and registered; the server then answered HTTP/1.1 0, which HTTP clients reject. PUT /mockserver/expectation (and every client library) now refuses an expectation whose statusCode is outside 100–999 with a 400 naming the field; any three-digit code, such as 999, is still accepted.

Expectation files are more forgiving: an invalid expectation in an initializationJsonPath file, or in persisted expectations restored from a cloud blob store, is skipped with a WARN naming it (position, id and path) while the rest of the file loads — before, one invalid expectation skipped the whole file. With failOnInitializationError=true a skipped expectation still fails startup. A persistedExpectationsPath file that held one is copied to <file>.invalid-entries-<timestamp>.bak before MockServer first saves over it.

To migrate: search your expectation files for "statusCode" : 0 and other out-of-range values, and set the status you meant.

 

HTTP/3 needs an opt-in jar or image, and refuses to start without it

The QUIC native binaries moved to a jar-with-dependencies-http3 classifier, shrinking the default standalone jar from 104 MB to 93 MB. If you set http3Port without the native library present, MockServer now refuses to start and prints the exact fix, instead of logging a warning and silently ignoring the port (which is what every published 8.0.0 Docker image did, since none of them could load the native library). It also now refuses to start when the HTTP/3 port itself can't be bound (for example another process already holds that UDP port), where 8.0.0 logged a warning and served only HTTP/1.1 and HTTP/2.

If you use HTTP/3:

  • Standalone jar: run mockserver-netty-<version>-jar-with-dependencies-http3.jar
  • Maven/Gradle: org.mock-server:mockserver-netty already brings the right native dependency; mockserver-netty-no-dependencies cannot load it
  • Docker, Docker Compose or Kubernetes: use the mockserver/mockserver:<version>-http3 tag (Helm: image.variant=http3)

If you don't set http3Port — the default — nothing changes except a smaller standalone jar.

 

DNS mocking refuses to start if its port can't be used

With dnsEnabled=true, MockServer now refuses to start when it cannot start its DNS server — nearly always a fixed dnsPort that's already in use — instead of logging a warning and running without DNS. With dnsPort left at its default of 0, the operating system picks a free port, so a refusal is unlikely. A common way to hit this without noticing: running more than one MockServer with the same fixed dnsPort (parallel test forks sharing one properties file, for example) — in 8.0.0 the first instance served DNS and the rest silently didn't; now every instance but the first fails to start.

To fix: free the port, choose a different dnsPort, leave dnsPort=0 (default), or set dnsEnabled=false. If you don't enable DNS mocking — the default — nothing changes.

 

An unexpected control-plane failure now answers 500, not 400

Before, a fault inside MockServer while it handled a control-plane request (for example PUT /mockserver/expectation) was answered 400 Bad Request with the exception's bare message as the body — reading as a problem with your request. It now answers 500 Internal Server Error with unexpected error processing request, see the MockServer log for correlation id: <id>, and the log holds the stack trace under that id. A request MockServer genuinely cannot accept — unreadable JSON, a request matcher that fails validation, an invalid parameter, an unsupported operation — is still answered 400 with a message saying what's wrong.

If your code treats any control-plane 400 as "my request was bad", or catches the Java client's IllegalArgumentException as a catch-all: distinguish a genuine 500 (an internal MockServer fault) from a 400 (your request was rejected).

 

Control-plane mTLS trusts only the configured CA chain

With control-plane mTLS and a controlPlaneTLSMutualAuthenticationCAChain set, both sides used to also trust MockServer's own CA certificate (certificateAuthorityCertificate, by default the bundled CA whose private key is published) alongside the configured chain. They now trust only the chain:

  • Server: MockServer now accepts only client certificates signed by the chain. If your control-plane client certificates are signed by MockServer's own CA, add that CA to the chain (or, if it's the bundled default CA, sign your client certificates with your own CA instead). With no chain set and control-plane mTLS required, MockServer now refuses to start if it would fall back to the bundled default CA — set the chain, or give MockServer its own CA with certificateAuthorityCertificate and certificateAuthorityPrivateKey.
  • Java client (MockServerClient, the JUnit 4 rule, JUnit 5 extension, Spring test listener): it now trusts only the chain for MockServer's certificate. If MockServer's certificate is signed by MockServer's own default CA, add that CA certificate to the chain the client uses, or the TLS handshake fails.

Without control-plane mTLS, nothing changes. See Control Plane Authorisation and HTTPS & TLS.

 

Docker & Helm

The only Docker/Helm-specific change is the HTTP/3 opt-in image above: if you set http3Port, switch to the mockserver/mockserver:<version>-http3 tag (Helm: image.variant=http3). No other Docker image or Helm chart behaviour in this list is breaking on its own — the config and binary-proxying changes below apply identically whether MockServer runs in a container or not.

 

Binary (non-HTTP) proxying

 

One upstream connection per client connection; forwardBinaryRequestsWithoutWaitingForResponse removed

Proxying a binary (non-HTTP) protocol through MockServer used to open a new upstream connection for every message. A client's connection now gets one upstream connection for its whole life: every message the client sends is written to it, whatever the upstream sends back is relayed as it arrives, and when the client closes its connection the upstream connection is closed too. A client whose upstream never answers is no longer cut off after maxFutureTimeout.

Through an upstream proxy, the one connection is now a tunnel: SOCKS5 through forwardSocksProxy, or CONNECT through forwardHttpsProxy or forwardHttpProxy (with forwardProxyAuthenticationUsername/forwardProxyAuthenticationPassword when set). 8.0.0 sent the raw bytes to forwardHttpProxy as if it were the server itself. Many proxies refuse CONNECT to ports other than 443 — if yours does, list the server in noProxyHosts. A refused CONNECT closes the client's connection with one warning naming the proxy, the setting and the status it answered.

A client that starts TLS part way through a session (such as PostgreSQL with sslmode=require) now has its upstream connection upgraded to TLS on the same connection, so a database like this can be proxied end-to-end (SCRAM channel binding needs channelBinding=disable in the client, or MockServer must be given the server's own certificate).

To migrate: remove forwardBinaryRequestsWithoutWaitingForResponse from your configuration — a value still set is ignored, with one warning at start-up, and nothing brings per-message forwarding back. In Java code, remove calls to the (now removed) forwardBinaryRequestsWithoutWaitingForResponse methods on ConfigurationProperties and Configuration, both NettyHttpClient.sendRequest(BinaryMessage, ...) overloads, and any reference to MockServerBinaryClientCodec or its encoder/decoder.

If you don't proxy binary protocols, nothing changes. See Binary expectations now match messages on a proxied connection above, which interacts with this change.

 

Expectation initialisation files

 

A repeated id now keeps the last entry, matching PUT /mockserver/expectation

Loading an initializationJsonPath file — at start-up or on a --watch reload — used to keep the first expectation for each repeated id in a file and ignore later ones. PUT /mockserver/expectation has always kept the last, because it upserts each entry in turn. File loading now matches that: the kept expectation serves from the first entry's place (as an in-place update would), and a file loaded later still replaces an earlier file's expectation with the same id.

mockserver lint and the WARN logged when a file loads now report each replaced expectation as DUPLICATE_ID, naming whether a later entry in the same file or a later file replaced it.

To migrate: if a file relies on its first entry for a repeated id being served, remove the repeated ids or reorder so the entry you want served is last.

See also statusCode validation above, which changes how an invalid entry in the same files is handled (skipped with a WARN, not fatal for the whole file).

 

Verification & log output

 

Over-count verification failures now say "found ... but should have been found ..."

When a verification's upper bound is exceeded (never(), exactly(n), atMost(n), between(m, n), or verifyZeroInteractions()), the failure message now reads, for example:

Request found 1 time but should have been found exactly 0 times, expected:<...> but was:<...>

where it used to read:

Request not found exactly 0 times, expected:<...> but was:<...>

("Response found ..." likewise for response verification.) Failures for too few matches are unchanged and still start with "Request not found" / "Response not found".

To migrate: update any test assertion on the exact text of an over-count failure to the new wording, or assert on "found" plus the times instead.

 

Response-body verification now applies the matcher instead of ignoring it

Verifying a response by body (httpResponse in PUT /mockserver/verify, httpResponses in /mockserver/verifySequence, or verify(..., response()..., ...)) with a regex, JSON path, JSON Schema, XPath, XML Schema, form, multipart, fuzzy, GraphQL, JSON-RPC, WASM or allOf matcher, or with matchType or not, is now actually applied. Before, it was ignored and the verification passed whenever any recorded response had a JSON object body — whatever it contained.

If a response verification starts failing after you upgrade: it passed only because the matcher was ignored. Fix the matcher or the system under test. In Java, use response().withBodyMatching(...) for these matcher types.

 

OpenAPI request verification now counts only matching operations

PUT /mockserver/verify (or verify(openAPI(...), ...)) with a specUrlOrPayload and operationId used to ignore the spec entirely and count every recorded request, so such a verification could pass when it should have failed. It now counts only requests for that operation.

If a verification starts failing after you upgrade: fix the matcher or the system under test; verifySequence was not affected by this bug.

 

AsyncAPI verify rejects count constraints at the top level

PUT /mockserver/asyncapi/verify (or the Java client's verifyAsyncMessage) now answers 400 if atLeast, atMost or exactly is sent at the top level, or if any field the endpoint doesn't know is sent — where it used to silently check "at least 1 message" instead.

To migrate: put count constraints inside count:

{ "channel": "orders", "count": { "atMost": 0 } }

instead of:

{ "channel": "orders", "atMost": 0 }
 

LOG_ENTRIES writes each body once; message/arguments show a short form

A log entry retrieved with format=LOG_ENTRIES used to write its request three times: in httpRequest, again as an element of arguments, and again inside the rendered message. The full request and response are now written only in httpRequest/httpRequests and httpResponse; where arguments and message refer to that same request or response, they now show a short form — its method and path (for example "POST /orders"), or its status code (for example "201") — the same way compactLogFormat already shows them on the console. This also applies to the MCP retrieve_logs and raw_retrieve tools. An argument that is a different request or response (for example the one a request was compared with) is still written in full.

To migrate: read a request or response body from the entry's httpRequest or httpResponse field instead of from message or arguments.

 

GenAI trace spans: Gemini and text completions renamed

Following the OpenTelemetry GenAI semantic conventions, spans for calls that aren't chat completions are no longer all named chat:

  • Gemini calls are now generate_content
  • Legacy, fill-in-the-middle and Ollama /api/generate completions, and forwarded AWS Bedrock InvokeModel calls to prompt-string models, are now text_completion

To migrate: if a trace query, dashboard or alert finds completions by gen_ai.operation.name == "chat" (or a chat span-name prefix), also match the new operation names. See AI Traffic Inspection.

 

Java client

The Java client (including the JUnit 4 rule, JUnit 5 extension and Spring test listener) is affected by several changes covered elsewhere on this page:

 

Node / TypeScript client

 

mockserver-client/llm typings drop their default export

For TypeScript users of the Node client's deep import mockserver-client/llm: its typings no longer declare a default export. Everything imported from mockserver-client itself type-checks exactly as it did in 8.0.0.

 

mockserver-client and mockserver-node now have an exports map

Both packages now declare an exports map. Importing or requiring a path it doesn't name — a .d.ts file, anything under test/, or the launcher's tasks/mockServer.js — now fails with ERR_PACKAGE_PATH_NOT_EXPORTED, where previously any file inside either package could be required. In exchange, an ES module can now import a deep path (for example mockserver-client/llm) without its .js extension, which failed with ERR_MODULE_NOT_FOUND in 8.0.0.

The map names the package itself, and each client module with and without .js: index, llm, setupMockServer, mockServerClient, mcpMockBuilder, a2aMockBuilder, sendRequest, webSocketClient, plus the types-only llmTypes and mockServer; the launcher's map names index, downloadJar, downloadBinary; and both name package.json.

To migrate: import the package itself or one of the listed paths. If you need a file by its on-disk location, resolve <package>/package.json and work from there.

 

retrieveRecordedRequests / retrieveRecordedRequestsById now typed as requests

These methods were typed Promise<HttpResponse[]> in 8.0.0 although they return the recorded requests, so reading a result's path didn't compile without a cast. They're now typed Promise<RequestDefinition[]> (a DNS or binary filter can return DNS queries or binary messages, which have no HTTP fields).

To migrate: code that reads .headers, .body or .cookies on a result, or declares the results as HttpResponse[], no longer compiles. Narrow each result to HttpRequest first:

if ('path' in request) {
  console.log(request.headers);
}
 

Other clients

 

Rust client: every public enum is now #[non_exhaustive]

A match on any public enum in the Rust crate that lists every variant no longer compiles. This includes LoadThresholdMetric, which also gains a CheckFailureRate variant (what MockServer sends for a CHECK_FAILURE_RATE threshold, which the enum couldn't represent before).

The enums MockServer sends — ResponseMode, BinaryUpstream, CrossProtocolTrigger and the Load* enums — also gain an Unknown variant: a value this client version doesn't know, sent by a newer MockServer, now reads as Unknown instead of failing the whole response. Unknown cannot be sent back: serialising it, or sending an expectation or load scenario that holds it, returns an error and nothing reaches the server. To re-submit server JSON unchanged, use upsert_raw.

To migrate: add a wildcard arm to any exhaustive match:

match metric {
    LoadThresholdMetric::CheckFailureRate => ...,
    _ => ...,
}

If you don't use the Rust client, nothing changes.

 

Go module path moves to /v9

Following Go's module-versioning rules for a major version, the Go client's module path and the Testcontainers-Go module's path for 9.0.0 gain a /v9 suffix.

To migrate: update the import path in your code and fetch the new major version:

go get github.com/mock-server/mockserver-monorepo/mockserver-client-go/v9

and update the corresponding import statements from the old (pre-9.0.0) path to the /v9 path. Do the same for the Testcontainers-Go module if you use it.

 

See also