Upgrading to MockServer 9.0.0
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 500 | Anyone using a response or forward template | Handle a 500 instead of a 404/502, or set templateFailureAsServerError=false |
| maxLoggedMatchFailures defaults to 50 | Anyone with >50 expectations who reads did-not-match log entries at INFO | Set maxLoggedMatchFailures=-1 to keep logging every comparison |
| Init files: repeated id keeps the last entry | Anyone with a repeated id across expectations in an initialisation file | Remove the repeated ids, or reorder so the one you want is last |
| Binary expectations now match while proxying | Anyone who proxies binary (non-HTTP) traffic and has binary expectations registered on the same MockServer | Set matchBinaryExpectationsWhenProxying=false to keep forwarding everything |
| Binary forwarder removed; one upstream connection per client | Anyone who proxies a binary (non-HTTP) protocol | Remove forwardBinaryRequestsWithoutWaitingForResponse; verify your upstream proxy allows CONNECT to non-443 ports; drop any calls to the removed Java APIs |
| Response statusCode must be 100–999 | Anyone with an expectation or file whose statusCode is 0 or otherwise out of range | Fix 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 it | Anyone who sets http3Port | Use 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 used | Anyone who sets dnsEnabled=true with a fixed dnsPort | Free the port, or leave dnsPort=0 (default) so the OS picks one |
| Unexpected control-plane failures answer 500, not 400 | Code that treats any control-plane 400 as "my input was wrong", or catches the Java client's bare IllegalArgumentException | Treat 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 chain | Anyone 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 start | Add 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 message | Update the assertion to the new wording |
| Response-body verification now applies the matcher | Response verifications using a regex/JSON path/JSON Schema/XPath/XML Schema/form/multipart/fuzzy/GraphQL/JSON-RPC/WASM/allOf matcher, or matchType/not | Fix 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 operations | Verifications using verify(openAPI(...), ...) | Fix the matcher or the system under test if a verification now fails |
| AsyncAPI verify rejects unknown/top-level count fields | Callers of PUT /mockserver/asyncapi/verify putting atLeast/atMost/exactly at the top level | Move them inside a count object |
| LOG_ENTRIES bodies written once, not three times | Code reading a body from an entry's message or arguments | Read the body from the entry's httpRequest/httpResponse instead |
| GenAI trace spans renamed for Gemini and text completions | Traces, 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 export | TypeScript users importing mockserver-client/llm | Use the named export instead of the default |
| Node client/launcher gain an exports map | Anyone 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 responses | TypeScript code typing results as HttpResponse[] or reading .headers/.body/.cookies directly | Narrow each result to HttpRequest first |
| Rust client enums are now #[non_exhaustive] | Rust code with an exhaustive match on a client enum | Add a wildcard _ => arm |
| Go module path moves to /v9 | Go users of mockserver-client-go or mockserver-testcontainers's Go module | Update 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:
- Control-plane mTLS now trusts only the configured CA chain — the client, not only the server.
- The removed binary-forwarding APIs: ConfigurationProperties/Configuration.forwardBinaryRequestsWithoutWaitingForResponse, both NettyHttpClient.sendRequest(BinaryMessage, ...) overloads, and the MockServerBinaryClientCodec classes.
- An unexpected control-plane failure now answers 500, not 400 — if your code catches the client's IllegalArgumentException as a catch-all for control-plane failures, it will no longer see that exception for an internal MockServer fault, only for a request MockServer genuinely rejects.
- AsyncAPI verify rejects unknown/top-level count fields — verifyAsyncMessage now answers 400 for a top-level atLeast/atMost/exactly or an unknown field, instead of silently checking "at least 1 message".
- The over-count verification wording change — any test asserting the exact text of a never()/atMost/exactly/between failure needs updating.
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
- Full changelog — every change in 9.0.0, not only the breaking ones
- Configuration Properties
- Getting Started
- Logging & Debugging