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, including the Go client's module-path move, grouped by who it affects, with the exact property or code change to make. A further section below, Non-breaking changes that can still affect an unmodified setup, covers changes the changelog does not mark BREAKING but that alter the behaviour of a server or client that sets nothing new.

 

Who needs to act

Change Who's affected What to do
Only trusted web pages can use MockServer's own API from a browser (later 9.x)Anyone whose browser page on another host, or dashboard pointed at another host or behind a Host-rewriting proxy, calls MockServer's API; anyone who set corsAllowOrigin so a page could call itSet controlPlaneCorsAllowOrigin to that page's origin
forwardProxyBlockPrivateNetworks can only be set at startup (later 9.x)Anyone who turns forwardProxyBlockPrivateNetworks on or off with PUT /mockserver/configuration or a client's updateConfigurationSet it before the server starts (system property, environment variable, properties file or Configuration)
File paths MockServer writes to can only be set at startup (later 9.x)Anyone who changes auditLogFile, persistedExpectationsPath, persistedRecordedExpectationsPath, persistedRecordedRequestsPath, memoryUsageCsvDirectory, directoryToSaveDynamicSSLCertificate or grpcProtocPath with PUT /mockserver/configuration or a client's updateConfigurationSet them before the server starts (system property, environment variable, properties file or Configuration)
Control-plane authentication, authorization, auditing and the dashboard analytics endpoint can only be set at startup (later 9.x)Anyone who changes the control-plane mTLS, JWT or OIDC settings, controlPlaneAuthorizationEnabled, controlPlaneScopeMapping, controlPlaneAuditEnabled or dashboardAnalyticsEndpoint with PUT /mockserver/configuration, a client's updateConfiguration or the dashboard's Audit Trail switchSet them before the server starts (system property, environment variable, properties file or Configuration)
noProxyHosts can only be set at startup (later 9.x)Anyone who changes noProxyHosts with PUT /mockserver/configuration or a client's updateConfigurationSet it before the server starts (system property, environment variable, properties file or Configuration)
Changing llmBaseUrl at runtime clears llmApiKey (later 9.x)Anyone who changes llmBaseUrl (or llmProvider with no llmBaseUrl) with PUT /mockserver/configuration and relies on the key set earlierSend llmApiKey in the same request as the new endpoint
Cluster fan-in peers and token can only be set at startup (later 9.x)Anyone who changes clusterVerifyFanInPeers or clusterFanInPeerAuthToken with PUT /mockserver/configuration or a client's updateConfigurationSet them before the server starts (system property, environment variable, properties file or Configuration)
A WAR enforces tlsMutualAuthenticationRequired on mocked requests (later 9.x)Anyone who deploys mockserver-war or mockserver-proxy-war with tlsMutualAuthenticationRequired=true (for example from a shared mockserver.properties)Serve over HTTPS with the servlet container verifying client certificates, or unset tlsMutualAuthenticationRequired
Kafka SASL login and bootstrap servers can only be set at startup (later 9.x)Anyone who sends kafkaSecurity.saslJaasConfig to PUT /mockserver/asyncapi, sends a SASL_PLAINTEXT/SASL_SSL request naming brokers other than asyncKafkaBootstrapServers, or changes asyncKafkaBootstrapServers with PUT /mockserver/configurationSet asyncKafkaSaslJaasConfig and asyncKafkaBootstrapServers before the server starts, and leave kafkaBootstrapServers out of SASL requests
retrieve with forwardUnmatchedTo needs a role that may change state (later 9.x)Anyone with controlPlaneAuthorizationEnabled whose read-only principal calls retrieve with forwardUnmatchedToGrant that principal the MUTATE role, or arm recording with a principal that has it
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; the standard Docker image includes the native library (from the release that fixed #2869; before that, 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
mTLS never trusts the bundled default CA (later 9.x)Anyone with tlsMutualAuthenticationRequired (or tlsMutualAuthenticationCertificateChain) while MockServer uses its default CA — the server now refuses to start, or stops accepting client certificates signed by the default CAGive MockServer its own CA, set dynamicallyCreateCertificateAuthorityCertificate=true, or set tlsMutualAuthenticationCertificateChain to the CA that signs your client certificates; for a throwaway local test only, tlsMutualAuthenticationAllowBundledCa=true
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 includes its own copy from the release that fixed #2869 (earlier versions of it cannot load the native)
  • Docker, Docker Compose or Kubernetes: from the release that fixed #2869 the standard mockserver/mockserver:<version> image serves HTTP/3; the -http3 tags (Helm: image.variant=http3), which earlier 9.x releases needed, are deprecated aliases of it, kept for one more release

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).

Each client surfaces this differently:

  • Java: the client raises ClientException for a 500. retrieveLogsByCorrelationId and retrieveActiveExpectations(requestDefinition, format) now throw instead of returning the error body as their result for any response status ≥400: 400 still raises IllegalArgumentException, 401 raises AuthenticationException, and every other status ≥400 (including 500) raises ClientException. Wrap calls to these two methods accordingly — see Troubleshooting Matching – Retrieving Logs by Correlation ID.
  • PHP: the client raises MockServerException (was InvalidRequestException).
  • Rust: the client raises Error::UnexpectedStatus (was Error::InvalidRequest).
  • Node, Python, Ruby, Go, .NET: unchanged.
 

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. Do not do this where MockServer reads the same chain (the JUnit rule and extension, the Spring test listener, or ClientAndServer in one JVM): MockServer refuses to start if its control-plane chain contains the bundled default CA, because its private key is published. Give MockServer its own CA with certificateAuthorityCertificate and certificateAuthorityPrivateKey and put that CA in the chain instead.

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

 

mTLS never trusts the bundled default CA

When mTLS is on (tlsMutualAuthenticationRequired=true, or a tlsMutualAuthenticationCertificateChain is set), MockServer used to accept client certificates signed by its own CA. By default that is the CA bundled with MockServer, and its private key is published in the MockServer jar, so anyone could create a client certificate MockServer accepted. MockServer now never trusts the bundled CA for client certificates:

  • mTLS on, no chain, default CA: MockServer refuses to start, and PUT /mockserver/configuration answers 400. The message names the fix.
  • A chain that contains the bundled CA: the same.
  • A chain without the bundled CA, default CA: MockServer starts and trusts only the chain. A client certificate signed by MockServer's default CA, such as the one MockServerClient presents by default, is no longer accepted.

To fix it, do one of these:

  • give MockServer its own CA with certificateAuthorityCertificate and certificateAuthorityPrivateKey, and sign your client certificates with it;
  • set dynamicallyCreateCertificateAuthorityCertificate=true, so MockServer creates a CA of its own (a client in the same JVM, such as ClientAndServer or the JUnit rule, then presents a certificate signed by it);
  • set tlsMutualAuthenticationCertificateChain to the CA that signs your client certificates.

For a throwaway local test only, tlsMutualAuthenticationAllowBundledCa=true restores the old behaviour. Because anyone can then create a client certificate MockServer accepts, a warning is logged whenever the bundled CA becomes trusted (at startup, when a configuration change makes it trusted, and each time the TLS context is rebuilt to trust it). It can only be set at startup. See HTTPS & TLS.

 

Docker & Helm

The only Docker/Helm-specific change is the HTTP/3 opt-in image above: if you set http3Port, use the standard mockserver/mockserver:<version> image from the release that fixed #2869 (earlier 9.x releases need the -http3 tag, Helm image.variant=http3, now a deprecated alias). 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

Separately, embeddings and rerank spans are now named embeddings <model> / rerank <model> instead of chat <model>. Token/cost metrics are unaffected by any of these renames, since they're labelled by provider/model, not operation.

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 generate_content and text_completion, and update any query/alert selecting embeddings or rerank spans by name or operation. See Observability – GenAI Spans.

 

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 now describe the module as a plain object of named exports, matching what the JavaScript has always done. Everything imported from mockserver-client itself type-checks exactly as it did in 8.0.0.

To migrate:

  • A default import without esModuleInterop/allowSyntheticDefaultImports no longer compiles. Use import llm = require('mockserver-client/llm'), import * as llm from 'mockserver-client/llm', import { llm } from 'mockserver-client', or turn on esModuleInterop.
  • Reading the module's .default property is now a compile error — drop .default from the access.
  • Named imports, import * as, and require in type-checked JavaScript now all type-check.
  • Declaring your own value with the same name as an imported builder type (Provider, Role, or one of the ten builder names) is now a compile error (TS2440) instead of silently shadowing the type import — use the imported value, or rename your import (import { Provider as ProviderName }).
  • A module augmentation of mockserver-client/llm still compiles but reaches less: members added to a builder interface or to Llm are no longer visible on the factories/builders or on the object imported from that path. Augment mockserver-client or the new mockserver-client/llmTypes instead — builder types now live there. TurnBuilder is now declared alongside the other builder constructors.
 

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.

 

Non-breaking changes that can still affect an unmodified setup

None of these are in the BREAKING list above, but each changes the behaviour of a server or client that sets nothing new — worth checking even if the table at the top says you're unaffected.

 

Docker image: jar split

If you override the standard Docker images' ENTRYPOINT or copy the jar out of them: /mockserver-netty-jar-with-dependencies.jar is now split into /mockserver.jar and /mockserver-deps.jar. Use -cp /mockserver.jar:/mockserver-deps.jar:/libs/* — java -jar /mockserver.jar still runs, but without the image's AppCDS start-up archive, so it starts more slowly. See Running the Docker Container.

 

forwardProxyBlockPrivateNetworks: service-name Host headers now get 502

With forwardProxyBlockPrivateNetworks on, a request that matches no expectation and whose Host header names MockServer itself by a name it doesn't recognise as its own (a Docker Compose or Kubernetes service name, for example) now gets 502 Bad Gateway instead of 404 Not Found, because that name resolves to a private address. Add the name to noProxyHosts, or set attemptToProxyIfNoMatchingExpectation=false if MockServer isn't used as a proxy, to get the 404 back.

 

localBoundIP now also binds the DNS and HTTP/3 ports

If you set localBoundIP, it now also keeps the DNS and HTTP/3 UDP ports on that address, where they previously listened on every address of the host even when it was set. A DNS query or HTTP/3 request sent to another address of the host is no longer answered — send them to the localBoundIP address, or leave localBoundIP unset.

 

HTTP/1.1 request line and header size limits are now on by default

maxInitialLineLength (64 KB) and maxHeaderSize (256 KB) were previously unlimited. A request line longer than the limit now gets 414, oversized headers get 431, and any other request MockServer cannot read (an invalid header, a chunked body with an invalid chunk size) gets 400, with the connection then closed — where before such a request was logged as an ERROR and matched/answered from whatever had been read. Set either property to 2147483647 to remove the limit.

 

maxEventLogSizeInBytes is now on by default

The event log is now bounded by size as well as by count: maxEventLogSizeInBytes defaults to a twentieth of heap at WARN and below, a twelfth at INFO and above (releases after 9.0 use a twentieth at INFO too; see maxEventLogSizeInBytes). Set it to 0 to restore count-only bounding (maxLogEntries) — though an explicitly-set value you already relied on may now retain fewer entries than before, since it is now enforced rather than ignored.

 

Idle-connection and stalled-write timeouts

Idle client connections now close after 5 minutes by default (inboundConnectionIdleTimeoutMillis; set 0 to restore unlimited). A client that stops reading a response now has that response ended as incomplete after 1 minute of nothing being read (responseWriteStallTimeoutMillis; set 0 to disable): an HTTP/1.1 connection or tunnel is closed, and an HTTP/2 or HTTP/3 stream is reset while other streams on the same connection continue.

 

Docker images: JVM heap cap drops from 75% to 45% of the container memory limit

Because ZGC's working-set-under-load plus off-heap footprint could otherwise exceed the container limit and get OOM-killed, the heap is now three-fifths of what the 75% cap in 8.0.0 gave at every container size (for example 232 MiB instead of 384 MiB at a 512 MiB limit, 922 MiB instead of 1,536 MiB at 2 GiB). 512 MiB is now the smallest supported container memory limit (768 MiB for -clustered); 256 MiB is no longer supported unless you also set a small explicit heap.

Because maxLogEntries defaults off the heap ceiling when not set explicitly (below its 250,000 cap it scales with the heap), the smaller default heap changes its computed default too:

Container memory limit8.0.0 default maxLogEntries9.0.0 default maxLogEntries
512 MiBat most ~45,000 (less whatever heap was in use at start-up)27,136
1 GiBat most ~92,500 (less whatever heap was in use at start-up)56,576
2 GiB100,000 (flat cap)115,456
4 GiB100,000 (flat cap)233,472

So the new default is lower on small containers and higher on large ones. To keep a larger heap (and maxLogEntries default), set -Xmx explicitly via JAVA_TOOL_OPTIONS (keeping -XX:+UseZGC) and size the container --memory limit for it.

 

Docker images now use ZGC by default

Images now use ZGC by default instead of G1 (the -aot image keeps G1, for its cold-start profile, and the -webhook test-tool image is unchanged; the -clustered image needs generational ZGC explicitly — its JDK 21 base defaults ZGC to single-generational, so the image sets JAVA_TOOL_OPTIONS="-XX:+UseZGC -XX:+ZGenerational" in its environment; -XX:+ZGenerational is obsolete, and absent, from JDK 23 on, so the standard/local/-aot/-graaljs images don't set it). Override with JAVA_TOOL_OPTIONS=-XX:+UseG1GC — note that setting JAVA_TOOL_OPTIONS yourself for any reason replaces the image's default entirely, so include -XX:+UseZGC (and, on -clustered, -XX:+ZGenerational too) unless you want G1 back. Switching to G1 also loses the AppCDS start-up archive, which is trained under ZGC, so G1 start-up is slower on the standard/local images. On Helm, set app.jvmOptions to change JAVA_TOOL_OPTIONS.

 

JUnit rule/extension now default to "dev mode"

The JUnit 4 rule and JUnit 5 extension now default to maxLogEntries=1000 and maxExpectations=1000 instead of deriving them from heap, cutting memory across many in-JVM instances. A suite relying on deeper log history should raise the caps explicitly or set mockserver.devMode=false.

 

Rust client: Delay/HttpTemplate struct literals need newly-added fields

Struct-literal callers of Delay/HttpTemplate (rather than the provided constructors/builders) need to set newly-added fields: distribution and template/template_type on Delay; delay, primary, response_override and response_modifier on HttpTemplate (not WebSocket/SSE settings — HttpTemplate has none) — or the struct literal no longer compiles. If you don't use the Rust client, or always build these with the provided constructors, nothing changes.

 

Python client: http_request/http_response are now deprecated aliases

http_request/http_response on HttpOverrideForwardedRequest now work only as deprecated aliases of request_override/response_override (which previously raised TypeError if used at all) — prefer the non-deprecated names in new code. If you don't use the Python client's forward-override builder, nothing changes.

 

Other things worth checking

  • Maven plugin: the run/runForked goals now fail the build when MockServer doesn't start, instead of logging an error and carrying on without a running server.
  • Import ids: HAR and Postman import (not OpenAPI, which never used this scheme) now derive each imported expectation's id from the request it matches, instead of from the entry's position in the file (har-0, har-1, …). Re-importing the same file already updated its own expectations in place under the old scheme too; what's fixed is a different file importing entries at the same positions, which no longer collides with, and silently overwrites, the first file's expectations. If you re-import a file (or an older export of one) that you already imported under 8.0.0, clear the existing expectations once first, or you may end up with both the old position-keyed and new request-keyed copies.
  • Internal x-http2-* headers (x-http2-scheme/x-http2-stream-id on a request, x-http2-stream-weight on a tunnelled response) are no longer leaked into recorded/matched/forwarded requests or responses. If an expectation or verification names one of these headers, remove it — it will no longer be present to match against.
  • Log-level changes that can affect alerting: an HTTP/1.1 or HTTP/2 upload its client gives up on part way through the body now logs one INFO entry naming the stream/request, not an ERROR with a stack trace — an alert keyed on ERROR entries, or on that message, stops firing for this case, and nothing is logged for it at WARN/ERROR levels. Separately, some connection-reset warnings that Netty used to print outside MockServer's own log at WARN (an upstream resetting an idle pooled HTTP/2 connection, for example) now appear inside MockServer's log at DEBUG, so they no longer show at the default INFO level; set logLevel=DEBUG to see them.
  • Java API: NottableString.withStyle/withSchemaType and Parameter.withStyle now return a new instance instead of mutating and returning this — code relying on the mutation (rather than using the return value) no longer sees the change.
  • Event log timing: a log entry can now take up to 10 ms longer to reach the console, disk capture, or the dashboard, and disk capture now batches writes — a hard kill (not a graceful stop()) can lose up to 8 KB of not-yet-flushed disk-captured entries.
  • Netty upgraded from 4.2.17.Final to 4.2.19.Final (bundled dependency), two visible changes: an HTTP/2 client whose SETTINGS_INITIAL_WINDOW_SIZE change would overflow a stream's flow-control window now has its whole connection closed with FLOW_CONTROL_ERROR (RFC 9113), where before only that one stream was reset; and in a multipart/form-data request, a file part whose declared Content-Type is not a plausible media type (for example text) is now read as application/octet-stream — a partContentTypes matcher for such a part must now name application/octet-stream instead of the part's declared type.
  • Windows binary launchers (each client's launcher that runs the downloaded mockserver.bat, not a JVM started directly): hardened against characters cmd.exe would otherwise read as commands rather than text, in the launcher path or in extra launch arguments.
    • Node: no longer starts MockServer through a shell at all (on Windows this previously ran mockserver.bat under cmd.exe, so %NAME% in an argument, or in the cache directory path, was expanded as an environment variable, and a line break could end the command early). A spawnOptions.shell option is now ignored. MOCKSERVER_JAVA_OPTS is now split on whitespace with quoted text kept together on every OS.
    • Python: start() now quotes every argument itself instead of running with shell=True, and raises ValueError if the launcher path or an extra_args entry contains a double quote, a line break or NUL, or if together they contain more than one %.
    • Ruby: builds the cmd.exe command line itself instead of relying on Ruby's own quoting, and raises MockServer::Error for the same character set (", <, >, |, &, a line break, NUL, or more than one %) in the launcher path or extra_args. Needs a native Ruby (mswin or mingw, such as RubyInstaller) on Windows.
    • PHP: builds the cmd.exe command line itself and throws BinaryInstallException for the same cases in the launcher path or an extra argument.
    • Go, Rust and .NET: each now quotes the launcher path itself before passing it to cmd.exe, and refuses a launcher path containing a double quote, a line break or NUL, or more than one %.
 

Changes in later 9.x releases

 

Only trusted web pages can use MockServer's own API from a browser

From 7.0.0 to 9.0.0, MockServer's own API (every /mockserver/... path), the dashboard's live log and the callback WebSocket answered every web page, so any site open in your browser could read recorded traffic from, or change, a MockServer running on your machine. A new property, controlPlaneCorsAllowOrigin, now decides which pages they answer. Left blank (the default), they only answer:

  • pages on localhost, 127.0.0.1 or [::1], on any port, over http or https, and
  • pages MockServer serves itself, such as the dashboard, at the same host and port, when you reach MockServer by localhost, an IP address, a single-word name such as a Docker or Compose service name (mockserver), a Kubernetes service name ending in .svc or .svc.cluster.local, or a name ending in .internal, such as host.docker.internal or an AWS ec2.internal name.

The second rule is limited to those names to stop DNS rebinding by a web site, where a malicious site points its own name at MockServer so that its page looks like one MockServer served: a page from http://evil.example.com:1080 sent with Host: evil.example.com:1080 is refused. A page MockServer serves at any other name, for example https://mockserver.example.com, is trusted only if you list its origin in controlPlaneCorsAllowOrigin, or its name ends in a suffix you list in the new controlPlaneTrustedHostSuffixes property (for example corp.example, for a Kubernetes cluster with a custom domain). This is so even when controlPlaneCorsAllowOrigin is set: listing some origins does not make other names trusted. Either one trusts that name, so only list names public DNS cannot resolve, or turn on control-plane authentication. Someone who controls your machine's name lookups (a hostile Wi-Fi network, a spoofing machine on the local network, your cluster's DNS) can still rebind any of these names except an IP address or localhost; turn on control-plane authentication if that is a risk for you. controlPlaneTrustedHostSuffixes can also only be set at startup.

A request MockServer's own API serves — any /mockserver/... path, a legacy path such as /reset or /expectation with a method the API serves there, the liveness path, or a dashboard or callback WebSocket — that comes from any other page is refused with 403, whatever its method, before it is handled and before any expectation can answer it. Origin: null (a sandboxed iframe, or a page opened from a file) is refused too. The browser's preflight for such a request is answered without CORS headers. Clients that are not browsers, such as curl and the client libraries, send no Origin and are not affected.

controlPlaneCorsAllowOrigin can only be set at startup (system property, environment variable, properties file or Configuration); PUT /mockserver/configuration ignores it and logs a warning.

corsAllowOrigin no longer affects MockServer's own API. It, and enableCORSForAllResponses, still apply to mocked and proxied responses as before, with these differences:

  • a preflight for a request MockServer's own API serves (a legacy path such as /reset or /graphql counts only with a method the API serves there, PUT for most) is answered by MockServer under controlPlaneCorsAllowOrigin, not by an OPTIONS expectation or corsAllowOrigin;
  • a respondBeforeBody expectation no longer answers a request MockServer's own API serves;
  • a browser request from a page that controlPlaneCorsAllowOrigin does not allow is refused with 403 even where a mock, not the control plane, would have answered it, for these requests: GET or DELETE on /loadScenario/start or /loadScenario/stop; PUT /pact; GET or DELETE on any deeper path under /loadScenario/<name>/ other than GET /loadScenario/<name>/report; GET, PUT or DELETE under /chaosExperiment/profiles/<name>/; POST under /chaosExperiment/apply/<name>/; and any method on a /mockserver or /_mockserver_ path that is not a route;
  • an unmatched preflight for another method on a legacy path, for example POST /graphql to a mocked GraphQL API, allows only the requested method in Access-Control-Allow-Methods.

Preflights for mocked paths also no longer carry the stray deprecated header.

Who needs to act:

  • anyone whose browser page on another host calls MockServer's API, for example a front-end on https://app.example.com or a browser test served from a CI host name;
  • anyone who points the dashboard at a MockServer on another host, or opens it through a reverse proxy or ingress that changes the Host header;
  • anyone who opens the dashboard at a name with a dot in it that is not a .svc, .svc.cluster.local or .internal name, for example https://mockserver.example.com: list that address in controlPlaneCorsAllowOrigin (even if it already lists other origins), or add a private suffix such as corp.example to controlPlaneTrustedHostSuffixes;
  • anyone who set corsAllowOrigin so that a page could call MockServer's API;
  • anyone running a Java client older than this release behind a proxy that changes the Host header: older Java clients send an Origin header on their callback and breakpoint WebSockets, built from the address they connect to. Upgrade the client, or add that address (for example http://mockserver-proxy:1080) to controlPlaneCorsAllowOrigin.

Set controlPlaneCorsAllowOrigin on that MockServer to the page's origin, or a comma-separated list of origins:

-Dmockserver.controlPlaneCorsAllowOrigin=https://app.example.com
# or, in Docker
-e MOCKSERVER_CONTROL_PLANE_CORS_ALLOW_ORIGIN=https://app.example.com

The same release makes the callback WebSocket require control-plane authentication when it is turned on, as the dashboard's WebSocket already did. The Java, Node, Python, Go, Rust and .NET clients of this release send their bearer token on it, and with mTLS authentication the Java, Node, Python, Go, Rust and .NET clients present their client certificate on it, so upgrade them with the server; a browser cannot send a token, so the Node client's browser build and the dashboard need mTLS for callbacks and breakpoints when JWT or OIDC authentication is on. A second connection that registers with a client id already in use takes over at once from the same authenticated caller, and otherwise only from a connection that no longer answers a ping within 2 seconds; while the old connection answers, it is refused with 403 (a different authenticated caller) or 409. See CORS.

 

forwardProxyBlockPrivateNetworks can only be set at startup

forwardProxyBlockPrivateNetworks stops forward actions, the proxy, replays and webhooks reaching loopback, private-network and cloud-metadata addresses. Up to 9.1.0, PUT /mockserver/configuration could switch it off on a running server, so anyone who could reach the control plane could turn that protection off. It is now fixed at startup, like forwardProxyAllowedHosts and forwardProxyDeniedHosts: a PUT that sets a different value is still answered, but the value is ignored and a warning is logged. A PUT that sends the value already in force, such as one that sends back the whole configuration, logs nothing.

To fix: if you turned it on or off at runtime, set it before the server starts instead:

-Dmockserver.forwardProxyBlockPrivateNetworks=true
# or, in Docker
-e MOCKSERVER_FORWARD_PROXY_BLOCK_PRIVATE_NETWORKS=true
 

File paths MockServer writes to can only be set at startup

auditLogFile, persistedExpectationsPath, persistedRecordedExpectationsPath, persistedRecordedRequestsPath, memoryUsageCsvDirectory and directoryToSaveDynamicSSLCertificate decide where MockServer writes files. Up to 9.1.0, PUT /mockserver/configuration could change them on a running server, so anyone who could reach the control plane could, for example, send the audit log, or the generated certificates and their private keys, to another directory. persistedRecordedRequestsPath also sets which file a format=recording import from disk reads, so a PUT could point that import at another file. grpcProtocPath, which names the protoc program MockServer runs, is now fixed at startup too. They are now fixed at startup, like recordedBodyFilesDirectory: a PUT that sets a different value is still answered, but the value is ignored and a warning is logged. A PUT that sends the value already in force, such as one that sends back the whole configuration, logs nothing. Most of these paths were already only used when the server started, so for them the change only stops GET /mockserver/configuration reporting a path the server was not using.

To fix: if you set them at runtime, set them before the server starts instead:

-Dmockserver.auditLogFile=/var/log/mockserver/audit.ndjson
# or, in Docker
-e MOCKSERVER_AUDIT_LOG_FILE=/var/log/mockserver/audit.ndjson
 

Control-plane authentication, authorization, auditing and the dashboard analytics endpoint can only be set at startup

Up to 9.1.0, PUT /mockserver/configuration could change these settings on a running server, so anyone allowed to change MockServer's configuration could weaken its security: switch control-plane authentication off, trust their own keys or identity provider, give themselves a role, switch the audit log off before making changes, or make every dashboard viewer's browser send events to their own server. They are now fixed at startup, like the controlPlaneBasicAuthentication* settings:

  • control-plane mTLS: controlPlaneTLSMutualAuthenticationRequired and controlPlaneTLSMutualAuthenticationCAChain;
  • control-plane JWT: controlPlaneJWTAuthenticationRequired, controlPlaneJWTAuthenticationJWKSource, controlPlaneJWTAuthenticationExpectedAudience, controlPlaneJWTAuthenticationMatchingClaims and controlPlaneJWTAuthenticationRequiredClaims;
  • control-plane OIDC: controlPlaneOidcAuthenticationRequired, controlPlaneOidcIssuer, controlPlaneOidcJwksUri, controlPlaneOidcAudience, controlPlaneOidcRequiredScopes and controlPlaneOidcScopeClaim;
  • authorization: controlPlaneAuthorizationEnabled and controlPlaneScopeMapping;
  • controlPlaneAuditEnabled and dashboardAnalyticsEndpoint.

A PUT that sets a different value is still answered, but the value is ignored, a warning is logged and GET /mockserver/configuration reports the value in force. A PUT that sends the value already in force, such as one that sends back the whole configuration, logs nothing. The dashboard's Audit Trail view no longer has a switch for the audit log; it shows whether it is on. controlPlaneAuditReads and controlPlaneAuditMaxEntries can still be changed at runtime.

To fix: if you set them at runtime, set them before the server starts instead:

-Dmockserver.controlPlaneAuditEnabled=true
# or, in Docker
-e MOCKSERVER_CONTROL_PLANE_AUDIT_ENABLED=true
 

noProxyHosts can only be set at startup

noProxyHosts lists the hosts MockServer connects to directly instead of through forwardHttpProxy, forwardHttpsProxy or forwardSocksProxy. MockServer reads those upstream proxies only when it starts, but up to 9.1.0 PUT /mockserver/configuration could change noProxyHosts on a running server, so anyone allowed to change MockServer's configuration could route forwarded traffic around an upstream proxy you rely on, for example for logging or data-loss checks. It is now fixed at startup: a PUT that sets a different value is still answered, but the value is ignored, a warning is logged and GET /mockserver/configuration reports the value in force. A PUT that sends the value already in force, such as one that sends back the whole configuration, logs nothing.

To fix: if you set it at runtime, set it before the server starts instead:

-Dmockserver.noProxyHosts="*.internal.corp,localhost"
# or, in Docker
-e MOCKSERVER_NO_PROXY_HOSTS="*.internal.corp,localhost"
 

Changing llmBaseUrl at runtime clears llmApiKey

llmBaseUrl can still be changed with PUT /mockserver/configuration, but up to 9.1.0 the key set earlier was then sent to the new endpoint, so anyone allowed to change MockServer's configuration could collect the LLM API key by pointing llmBaseUrl at their own server. Now a PUT that changes where the default LLM backend sends requests — a different llmBaseUrl, or a different llmProvider while llmBaseUrl is empty — without also supplying llmApiKey clears the key and logs a warning. The ***REDACTED*** value a GET returns does not count as supplying it. A PUT that supplies both applies both. Likewise, a JSON llmBackendsConfig value in which a backend's baseUrl or provider changes while its key is left masked is not applied, and a warning is logged.

To fix: send the key in the same request as the new endpoint:

curl -X PUT http://localhost:1080/mockserver/configuration \
  -d '{"llmBaseUrl": "https://gateway.example.com", "llmApiKey": "sk-..."}'
 

Cluster fan-in peers and token can only be set at startup

With cluster verify/retrieve fan-in, each node sends clusterFanInPeerAuthToken to every node listed in clusterVerifyFanInPeers. Up to 9.1.0, PUT /mockserver/configuration could change that list on a running server, so anyone who could reach the control plane could add a host of its own and collect the token, which grants write access to every node. Both settings are now fixed at startup: a PUT that sets a different value is still answered, but the value is ignored and a warning is logged (the warning never shows the token). A PUT that sends the value already in force logs nothing. The token was already only read when the server started, so for it the change only stops a PUT appearing to work.

To fix: if you set them at runtime, set them before the server starts instead:

-Dmockserver.clusterVerifyFanInPeers=http://mockserver-1:1080,http://mockserver-2:1080
# or, in Docker
-e MOCKSERVER_CLUSTER_VERIFY_FAN_IN_PEERS=http://mockserver-1:1080,http://mockserver-2:1080
 

Kafka SASL login and bootstrap servers can only be set at startup

Up to 9.1.0, a PUT /mockserver/asyncapi request could send its own Kafka SASL login in brokerConfig.kafkaSecurity.saslJaasConfig. A JAAS configuration names a Java class for the Kafka client to load, so anyone who could reach MockServer's API could choose code to run inside MockServer. That field, and any other kafkaSecurity field MockServer does not know, is now rejected with a 400. The login is set once, when MockServer starts, with asyncKafkaSaslJaasConfig; PUT /mockserver/configuration ignores it with a warning.

Because the login is now held by MockServer (in that property, or in the JVM's own JAAS file), it must only go to brokers you chose. A request whose securityProtocol is SASL_PLAINTEXT or SASL_SSL must leave out kafkaBootstrapServers or name the same brokers as asyncKafkaBootstrapServers (in any order). Otherwise, or if asyncKafkaBootstrapServers is not set, it gets a 400 and nothing is sent. asyncKafkaBootstrapServers can now only be set at startup: a PUT /mockserver/configuration that sets a different value is ignored and a warning is logged. PLAINTEXT and SSL requests can still name any brokers.

To fix: set both before the server starts, and keep securityProtocol and saslMechanism in the request:

-e MOCKSERVER_ASYNC_KAFKA_BOOTSTRAP_SERVERS=broker:9093 \
-e MOCKSERVER_ASYNC_KAFKA_SASL_JAAS_CONFIG='org.apache.kafka.common.security.plain.PlainLoginModule required username="user" password="pass";'
 

retrieve with forwardUnmatchedTo needs a role that may change state

retrieve?type=RECORDED_EXPECTATIONS&forwardUnmatchedTo=<upstream> changes where the server forwards unmatched requests, but control-plane authorization treated it as a read. With controlPlaneAuthorizationEnabled, a principal with only the READ role is now refused with 403 when it sends forwardUnmatchedTo; it needs the MUTATE role, as extractBodiesLargerThan already did. A retrieve without that parameter is still a read. Without control-plane authorization, nothing changes. See Control Plane Authorisation.

 

A WAR enforces tlsMutualAuthenticationRequired on mocked requests

A WAR deployment (mockserver-war or mockserver-proxy-war) used to ignore tlsMutualAuthenticationRequired and served mocks over plain HTTP, and over HTTPS without a client certificate. With it set, a mocked request over plain HTTP now gets 426 Upgrade Required, and one over HTTPS without a client certificate verified by the servlet container gets 401. The servlet container's trust store decides which client certificates are accepted. To keep serving mocks, configure the container for HTTPS with client-certificate verification, or unset tlsMutualAuthenticationRequired.

 

See also