Repository navigation
[9.0.0] - 2026-10-15
This release delivers a sustained performance and memory programme alongside data-integrity fixes under load, plus a large set of LLM-provider correctness fixes, dashboard improvements, and client-library parity work. Headline numbers from the single-load-generator benchmark on the same six-core rig (builds 420 and 464): the healthy ceiling rises from 39,033 to 57,149 req/s and the peak from 43,671 to 59,905 req/s, with p95 at 32,000 req/s falling from 56.6 ms to 0.44 ms on the new ZGC default in the Docker images; event-log retained heap at 20,000 entries falls from 429 MB to 61 MB; Docker image download shrinks ~23%; and instance shutdown drops from ~107 ms to near zero. 22 BREAKING changes affect HTTP/3, DNS mocking, binary (non-HTTP) proxying and binary expectation matching while proxying, template-failure response codes, maxLoggedMatchFailures's default, initialisation-file id conflicts, the Node client's TypeScript typings and exports map, LOG_ENTRIES log output, control-plane error handling and mTLS, the Rust client's enums, verification-failure wording, response statusCode validation, AsyncAPI and response/OpenAPI verification, GenAI trace span names, the Node client's recorded-request typings, and the Go client/Testcontainers module's /v9 module paths — see Upgrading from 8.0.0 and the BREAKING entries under Changed. For a per-change migration list, see the upgrade guide at https://www.mock-server.com/mock_server/upgrading_to_9.html.
| Metric | Before | After |
|---|---|---|
| Throughput, healthy ceiling / peak (req/s, 6 cores) | 39,033 / 43,671 | 57,149 / 59,905 |
| p95 latency at 32,000 / 48,000 req/s offered | 56.6 ms / 82.4 ms | 0.44 ms / 12.0 ms |
| Event-log heap (20,000 entries) | 429 MB | 61 MB |
| Docker image download (linux/arm64, standard image) | ~167 MB | ~129 MB |
| Instance shutdown time | ~107 ms | ~0 ms |
"Before" is perf build 420 (commit d568695, 2026-09-24, JDK 17.0.20, G1 collector), measured nine days after the 8.0.0 tag and already carrying some of this release's own work, since 8.0.0 itself was never measured on this rig. "After" is perf build 464 (commit efdc522, 2026-09-27, JDK 25, ZGC), the performance page's single-load-generator figures; the page's headline is now a different, multi-generator measurement (see Changed). Both are comparable with each other but not with any benchmark published after this release.
Upgrading from 8.0.0
- Go client and Go Testcontainers module users: both modules move to
/v9module paths (the Go module proxy refused their 8.0.0 tags) — rungo get github.com/mock-server/mockserver-monorepo/mockserver-client-go/v9(and the Testcontainers module's/v9path if you use it) and change your
imports from the/v7/unsuffixed paths to the/v9ones; see Changed. - HTTP/3 users (
http3Portset): the QUIC native library now ships separately — use the-jar-with-dependencies-http3jar or themockserver/mockserver:<version>-http3Docker image (latest-http3,snapshot-http3; Helm:image.variant=http3), and a server that cannot serve HTTP/3 (missing
native, or the port unusable) now refuses to start instead of silently falling back. - DNS mocking users (
dnsEnabled=true): MockServer now refuses to start if itsdnsPortcannot be bound, instead of logging a warning and running without DNS. LeavednsPortat its default of0to let the OS pick a free port, or setdnsEnabled=false. - TypeScript users of
mockserver-client/llm: the deep-import typings no longer declare a default export; import frommockserver-clientitself, useimport llm = require(...)/import * as llm from ..., or turn onesModuleInterop(see Changed). - Binary (non-HTTP) proxy users: a client connection now always keeps one upstream connection for its whole life — forwarding one message per upstream connection is removed, along with its
forwardBinaryRequestsWithoutWaitingForResponsesetting (a value still set is ignored, with a warning); remove it from your configuration. - Anyone reading a request or response body from
message/argumentsofLOG_ENTRIESoutput (including the MCPretrieve_logs/raw_retrievetools): read it from the entry'shttpRequest/httpResponseinstead —message/argumentsnow refer to that request/response by a short form
such as"POST /orders"or"201". - Anyone treating a control-plane
400as any failure, or catching the Java client'sIllegalArgumentExceptionfor non-input failures: an unexpected server-side fault is now a500with a generic message and correlation id;400is now reserved for requests MockServer genuinely rejects. The
Java client'sretrieveLogsByCorrelationIdandretrieveActiveExpectations(requestDefinition, format)now throw instead of returning the error's text. - Anyone importing a path inside
mockserver-client/mockserver-nodeother than the package or a documented module: both packages now have anexportsmap, so an unlisted path (.d.tsfiles,tasks/mockServer.js, etc.) now fails to resolve — resolve<package>/package.jsonand join a file's
path to that directory if you need one by location. - Control-plane mTLS, Java client: the client now trusts only
controlPlaneTLSMutualAuthenticationCAChainfor MockServer's certificate; add MockServer's own CA to that chain if it signs the server certificate. - Control-plane mTLS, server: MockServer now accepts only client certificates signed by
controlPlaneTLSMutualAuthenticationCAChain; add MockServer's own CA to the chain if it signs your client certificates (or use your own CA if it's the bundled default), and MockServer now refuses to start with
mTLS required, no chain set, and the bundled default CA — set the chain or configure your own CA withcertificateAuthorityCertificate/certificateAuthorityPrivateKey. - Rust client users who
matchon its enums: every public enum is now#[non_exhaustive](add a wildcard arm), and server-sent enums (ResponseMode,BinaryUpstream,CrossProtocolTrigger, theLoad*enums) gain anUnknownvariant that cannot be sent back;LoadThresholdMetricalso gains
CheckFailureRate. - Anyone asserting the exact text of an over-count verification failure (
never(),exactly,atMost,between,verifyZeroInteractions): the message now reads "found … but should have been found …" instead of "not found". - Anyone with a response
statusCodeof0or outside 100–999 (most likely from a blank 8.0.0 dashboard status field): it is now rejected at creation with a400, or skipped with aWARN(naming it) when loaded from an initialization/persisted-expectations file, with the file backed up first. - AsyncAPI verification users (
PUT /mockserver/asyncapi/verify, or the Java client'sverifyAsyncMessage): putatLeast/atMost/exactlyinsidecount, not at the top level — the endpoint now answers400for unknown fields instead of ignoring them. - Anyone verifying a response body with a regex, JSON path, JSON Schema, XPath, XML Schema, form, multipart, fuzzy, GraphQL, JSON-RPC, WASM or allOf matcher: that matcher is now actually applied, where it was previously ignored (verification passed whenever any recorded response had a JSON object
body). - Anyone verifying requests with an OpenAPI matcher: only requests for the matched
operationIdnow count, instead of every recorded request (verifySequencewas unaffected). - Anyone selecting LLM completions by
gen_ai.operation.name == "chat": Gemini calls are nowgenerate_content, and prompt-string completions (legacy/v1/completions, Mistral FIM, Ollama/api/generate, prompt-based Bedrock InvokeModel models) are nowtext_completion— update to match
chat,generate_contentandtext_completiontogether. - TypeScript users of the Node client's
retrieveRecordedRequests/retrieveRecordedRequestsById: now typedPromise<RequestDefinition[]>instead ofPromise<HttpResponse[]>; narrow each result toHttpRequestbefore reading HTTP-specific fields. - Anyone proxying binary (non-HTTP) traffic while binary expectations are registered on the same MockServer: a message matching a binary expectation is now answered by it and not forwarded, by default (
matchBinaryExpectationsWhenProxying); set it tofalseto always forward as in 8.0.0. - Anyone relying on every "didn't match" comparison being logged: the new
maxLoggedMatchFailuresproperty now defaults to50, so atINFOonly the first 50 "didn't match" entries per request are logged; setmaxLoggedMatchFailures=-1to keep logging every comparison. - Anyone with expectations in an
initializationJsonPathfile whoseids repeat: the file now keeps the last expectation for each repeatedid, not the first; remove the repeatedids, or put the one you want last. - Anyone relying on a failed template answering
404/502: a response/forward template that fails, or whose output isn't valid, now answers500by default (templateFailureAsServerError); set it tofalseto keep the 8.0.0 answers.
Also worth checking, though none is a breaking change: 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 CDS start-up archive, so it starts more slowly); 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; and if you set localBoundIP, it now also keeps the DNS and HTTP/3 ports on that address — a DNS query or HTTP/3 request sent to another address of the host is no longer answered, so send them to the localBoundIP address, or leave localBoundIP unset.
Security
- Velocity templates now have a time limit (
mockserver.velocityTemplateExecutionTimeout, default 5000 ms, same as JavaScript templates). A runaway#foreach, recursive macro or chained#defineblock could previously hold a template thread until restart; it now fails with a timeout error at
its next loop iteration, macro call or#parse/#evaluate. A single slow Java call inside a template is not interrupted, and the limit does not cap memory. Set the property to0to remove it. - Netty upgraded from
4.2.17.Finalto4.2.19.Final, and BouncyCastle from1.85to1.86, a security release whose advisories in the parts of Netty MockServer uses cover HTTP/1.1 request/response smuggling (including improper CRLF neutralization) and an origin validation error, unbounded
resource consumption in the HTTP/1.1, HTTP/2, HTTP/3 and DNS codecs and the base codec, input misinterpretation in the SOCKS codec, and an SNI routing bypass and an improper access control in the handler module. Two visible changes: an HTTP/2 client whoseSETTINGS_INITIAL_WINDOW_SIZEchange would
overflow a stream's flow-control window now has its connection closed withFLOW_CONTROL_ERROR, as RFC 9113 requires, instead of having that one stream reset; and in amultipart/form-datarequest, a file part whoseContent-Typeis not a plausible media type (for exampletext) is now read as
application/octet-stream, so apartContentTypesmatcher for such a part must nameapplication/octet-stream. Netty 4.2.18 also tightened its HTTP/2 frame decoder to validate header values as well as names; unvalidated inbound frames (a leading space, an embedded DEL0x7F, other control
characters in a header value) are a shape MockServer is used to record, so inbound frame validation is now disabled on the multiplex codec, keeping that leniency for HTTP/2 requests on the multiplex path (this also disables inbound header-name validation there, since Netty folds both under one
flag); outbound names and HTTP/2-forbidden connection-specific headers (Connection,Transfer-Encoding) remain validated. - The binary launchers (Node, Python, Ruby, Go, Rust, .NET, PHP) no longer start MockServer through a shell, or no longer let a shell misread their arguments, on Windows; the Node launcher never uses a shell at all.
- The Node launcher no longer starts MockServer through a shell. On Windows,
runBinaryand themockservercommand ran the bundle'smockserver.batundercmd.exe, so%NAME%in an argument or in the cache directory path was replaced with an environment variable's value, and a line break cut
the command short. On every platform they now run the bundle's own Java runtime directly, with the JVM options the launcher script applies, and pass each argument separately with no shell in between, so no shell can read%,&,$or;in an argument or path as a variable or a command.
MOCKSERVER_JAVA_OPTSis split on whitespace with quoted text kept together, so-Dx="a b"setsxtoa bon Linux and macOS too. Ashelloption passed inspawnOptionsis now ignored. - The Python client's binary launcher no longer starts MockServer through a shell on Windows.
start()ran themockserver.batlauncher withshell=True, socmd.execould treat characters in the launcher path or inextra_argsas commands rather than text. It now runscmd.exeitself with
every argument quoted, so characters such as&,|and^reach MockServer unchanged.start()raisesValueErrorif the launcher path or anextra_argsentry contains a double quote, a line break or NUL, or if together they contain more than one%, whichcmd.exewould expand as an
environment variable. Linux and macOS are unchanged and still use no shell. - The Ruby client's binary launcher now quotes every argument it passes to
cmd.exeon Windows.BinaryLauncher.startlet Ruby quote the arguments forcmd.exe, which follows different rules, so characters in the launcher path or inextra_argscould be read as commands. It now builds the
cmd.execommand line itself and raisesMockServer::Errorif the launcher path or anextra_argsentry contains",<,>,|,&, a line break or NUL, or if together they contain more than one%. On Windows it needs a native Ruby (mswin or mingw, such as RubyInstaller). On Linux and
macOS it now never uses a shell, even with no arguments. - The Go, Rust and .NET clients' binary launchers now quote the launcher path they pass to
cmd.exeon Windows. They ranmockserver.batthroughcmd.exewith the standard argument quoting, whichcmd.exedoes not follow, so characters in the launcher path (fromMOCKSERVER_BINARY_CACHEor the
user's cache folder) could be read as commands. Each now builds thecmd.execommand line itself, quoting every part, and refuses a launcher path that contains a double quote, a line break or NUL, or more than one%. - The PHP client's binary launcher now quotes every argument it passes to
cmd.exeon Windows.BinaryLauncher::start()let PHP quote the launcher path and$extraArgsforcmd.exe, which follows different rules, so characters in them could be read as commands. It now builds thecmd.exe
command line itself and throwsBinaryInstallExceptionif the launcher path or an extra argument contains a double quote, a line break or NUL, or if together they contain more than one%. - A long
MOCKSERVER_BINARY_BASE_URLmade of repeated/no longer takes the Node launcher seconds to process.
- The Node launcher no longer starts MockServer through a shell. On Windows,
- Dependency security upgrades: Jackson 2.22.2 → 2.22.3 (three Snyk-reported vulnerabilities); dashboard dependency KaTeX 0.16.47 → 0.18 (low-severity prototype-pollution bypass of its trust restrictions); dashboard dependency
dompurifyresolves to 3.4.16 (DOM XSS viaIN_PLACEsanitize with a
node-removing hook, Dependabot alert 593); dashboard npmoverridespinslodash-esto 4.18.1 (_.template/_.unset/_.omitadvisories; the affected functions are tree-shaken from the built dashboard anyway); the Node Testcontainers module's own development lockfile resolves newerundici,
brace-expansionand@grpc/grpc-js(Dependabot alerts 581, 582, 584, 586, 591, 592 — the published package is unaffected, since it ships onlydist). - A
zstdrequest or streamed compressed upstream response can no longer exhaust MockServer's memory. Azstdframe declares its decompressed size, and MockServer previously allocated that much up front (17 bytes claiming 1.5 GB threwOutOfMemoryErroron a 256 MB heap) — now decompressed in
64 KB pieces, withmaxRequestBodySize/maxResponseBodySizeenforced as for any other coding.- A streamed response (SSE and similar; on by default) was never body-size-limited at all, so a single network read's worth of compressed bytes could decompress to gigabytes (a 256 MB
zstdstream, 8 KB on the wire, exhausted a 256 MB heap); such a stream is now capped at
maxResponseBodySize(50 MB by default) of data waiting to be written to the client, ending incomplete past that point rather than looking finished. - A legitimate slow client is unaffected: MockServer reads more of the upstream only once the client has taken nearly all of what is waiting, so a slow client holds back the upstream rather than filling memory, and only a response that decompresses to nearly the limit or more from a single
network read is stopped; the wait doesn't count towardsstreamIdleTimeoutSeconds(which now measures only an upstream that sends nothing while MockServer is reading it — a client that pauses keeps its stream, with at most the limit waiting for it, until it reads on, disconnects or takes
nothing forresponseWriteStallTimeoutMillis). - A stream that times out, or whose upstream closes, fails or sends invalid chunk framing part-way through, now ends incomplete, where before it ended with a normal final chunk and looked complete; a stream whose upstream simply ends by closing its connection still completes normally. When the
client goes away, MockServer now also closes the upstream, where before it kept reading an endless stream forever. - A stream through a
CONNECTtunnel is limited the same way, but bymaxRequestBodySize(10 MB by default) — the limit that tunnel already applied to a response it collected whole, notmaxResponseBodySize; through such a tunnel this backpressure is new for a streamed response, which before
was queued in memory as fast as it arrived.
- A streamed response (SSE and similar; on by default) was never body-size-limited at all, so a single network read's worth of compressed bytes could decompress to gigabytes (a 256 MB
- A chunked HTTP/1.1 request can no longer make MockServer buffer an endless chunk-size line. Each chunk of a chunked request body starts with a line giving its size, which may carry extra parameters (chunk extensions), and the body may end with trailer headers. MockServer limited that line
only bymaxInitialLineLengthand the trailers only bymaxHeaderSize, which were both unlimited by default, so MockServer kept buffering a line its client never ended, up to 2 GB or until its network memory ran out. MockServer now answers400and closes the connection once more than 8 KB of
one chunk-size line, or of the final0line and its trailers counted together, is waiting to be read, on requests sent to it directly and through aCONNECTor SOCKS tunnel. The limit is fixed: real uploads use about 100 bytes per line, and 8 KB is what Tomcat allows. Anything within the
limit, the request line, headers and body sizes are unaffected, and a lowermaxInitialLineLengthormaxHeaderSizestill applies. HTTP/2 and HTTP/3 have no chunk-size lines. - The request line and headers of an HTTP/1.1 request are now limited by default, and a request MockServer cannot read is refused instead of answered.
maxInitialLineLength(64 KB) andmaxHeaderSize(256 KB) were previously unlimited; set either to2147483647to remove the limit. A longer
request line now gets414, larger headers431, and any other unreadable request (an invalid header, a chunked body with an invalid chunk size)400, with the connection then closed — where before such a request was logged as anERRORand matched/answered from whatever had been read (a request
line over the limit, for example, asGET /bad-request). This also applies throughCONNECT/SOCKS tunnels. A request is also now unreadable, and gets400, when the request before it on the same connection left bytes behind — for example a client that writes aGETbody with neither
Content-Lengthnor chunked encoding, as Node'shttp.requestdoes — where it used to get404forGET /bad-request. HTTP/2 and HTTP/3 now take their header limit frommaxHeaderSizetoo (see Changed); the limits apply only to requests MockServer receives — a mocked response with larger
headers is still sent intact. - A client can no longer make the server hold far more memory than the request it is sending. Over HTTP/2, small DATA frames mixed into a large read could pin up to ~6.7 GB across a connection's 100 streams at the default body-size limit; similarly for small HTTP/1.1 chunk-extension-padded chunks.
Held network-read buffers are now copied once a request's retained pieces use less than half of the read they arrived in, cutting held memory 6× to over 2,000× in the cases measured. - Control-plane mTLS now trusts and accepts only the configured CA chain, not MockServer's own CA as well (BREAKING; see Changed and Upgrading from 8.0.0). The Java client's callback and breakpoint WebSockets, which previously trusted any server certificate and presented no client certificate
over TLS, now use the same trust and client-certificate settings as the client's HTTP requests. forwardProxyBlockPrivateNetworksnow also blocks carrier-grade NAT addresses (100.64.0.0/10). This shared address range (RFC 6598) is used by carrier-grade NAT and by overlay networks such as Tailscale to reach machines that are not on the public internet, so with the setting on a forward
or proxied request could still reach them. A target in that range, including written as an IPv4-mapped IPv6 address such as::ffff:100.64.0.1, is now refused like a private-network address (with a502for HTTP); if you turn the setting on and forward to a Tailscale or other
100.64.0.0/10address on purpose, leave the setting off to keep those forwards working.forwardProxyBlockPrivateNetworksnow also applies to the MCP tools that send requests to a URL you give them. With the setting enabled, 8.0.0 letrun_contract_test,run_resiliency_testandrun_mcp_contract_testsend requests to loopback, private-network and cloud-metadata addresses
unchecked. Each tool now checks the URL's host before sending anything: a refused target is sent nothing, the tool returns an error naming the blocked address, and one warning is logged. Each request is checked again as it is sent, and with the setting enabled these tools no longer follow
redirects, which could lead to an address that was never checked. A spec path that does not start with/is now always sent to the base URL's host.forwardProxyBlockPrivateNetworksnow checks the address MockServer actually connects to, and OpenAPI spec fetches. With the setting on, a forward, a proxied request, a webhook, a load scenario request, a drift alert or a WebSocket relay that MockServer connects to directly now goes to the
exact address that passed the check, so a host name whose DNS answer changes between the check and the connection can no longer reach a blocked address. Every fetch of an OpenAPI spec from a URL (until now checked only for traffic validation), each remote$refin a spec, and each redirect
followed while fetching them are now checked too; a refused fetch makes the spec fail to load with a message naming the blocked address. Each MockServer applies its own setting to every spec it loads, so a server with the setting on in its own configuration checks them even when the global
property is off; a spec loaded by a server with the setting off is fetched again, with the check, by a server that has it on. To see those fetches MockServer wraps the JVM's defaultProxySelectorthe first time it loads a spec with the setting on; the wrapper stays for the life of the
JVM, and aProxySelectoryour code sets at the same moment can be lost, so set your own before MockServer loads a spec. A forward refused because the address looked up to connect to is blocked is neither retried nor counted by the forward circuit breaker.forwardProxyBlockPrivateNetworksnow also applies to requests MockServer sends itself. With the setting enabled, 8.0.0 still sent these to loopback, private-network and cloud-metadata addresses unchecked: before-action, after-action and step webhooks, load scenario requests, and drift alert
webhooks. Each is now checked as a forward is, and a refused one is not sent and is logged once as a warning naming the blocked address. A refused blocking webhook with theFAIL_FASTfailure policy makes the response a502 Bad Gateway(withBEST_EFFORTthe response goes ahead), a refused
load scenario request is counted as a failed request of kindblocked(one warning for the run).proxyRemoteHostis checked although it is set in configuration, as it is for proxied requests; only the upstream proxies MockServer sends through (forwardHttpProxy,forwardHttpsProxy,
forwardSocksProxy) are not checked.forwardProxyBlockPrivateNetworksnow applies to every forwarded and proxied request. With the setting enabled, 8.0.0 checked onlyhttpForwardactions. Overridden forwarded requests (forwardOverriddenRequest), class and object forward callbacks, binary forwarding, and requests that
matched no expectation and were proxied on did not apply the check, so an expectation, or an unmatched proxied request, could reach loopback, private-network and cloud-metadata addresses. All of them now apply it, as forward actions do: an HTTP request gets502 Bad Gateway, a binary
connection is closed, and a warning names the blocked address. A request sent throughforwardHttpProxyalso has its Host header checked, as that proxy is sent it as the address to go to. With the setting enabled, a request that matches no expectation and whose Host header names MockServer
itself by a name it does not recognise as its own (a Docker Compose or Kubernetes service name, for example) now gets502 Bad Gatewayand a warning instead of404 Not Found, because that name resolves to a private address — see Upgrading from 8.0.0.
- Configuration changes made through
PUT /mockserver/configurationnow take effect for every way of starting MockServer, and all at once. A server started without a configuration object (command line, Docker, Maven plugin) previously held two copies of its configuration, so aPUTthat reported
success (including enabling control-plane authentication, or loweringmaxLogEntries/maxEventLogSizeInBytes, or enablingredactSecretsInLog) left request handling on the old, insecure or unenforced values; settings given at startup, and servers started throughClientAndServer, the JUnit
integrations or Spring, were not affected. APUTthat changes authentication or TLS settings is now also applied atomically: control-plane authentication/authorization and the server TLS settings (tlsMutualAuthenticationRequiredand its certificate chain) switch from all-old to all-new with
nothing in between — closing a sub-millisecond window where aPUTswitching from mTLS to JWT/OIDC could briefly require no authentication at all, or a TLS connection opened mid-change could keep using the old setup. A rejectedPUT(an invalid certificate path, a negative
globalResponseDelayMillis) now answers400having changed nothing, where fields ahead of the invalid one used to stay applied. An MCP tool call is now also authorized with the settings it was authenticated with, rather than possibly a laterPUT's. Outbound TLS connections now cache their TLS
setup only under the exact settings it was built from, so aPUTchangingforwardProxyTLSX509CertificatesTrustManagerTypemid-build can no longer leave a stale trust-all setup cached under the old settings; turninghttp2Enabledoff at runtime now also stops outbound TLS connections offering
HTTP/2. redactSecretsInLognow masks credentials everywhere the event log is shown or retrieved, including the console/retrieved curl rendering of a proxied or forwarded request, JSON logmessage/arguments/expectationfields, the separate cookie list, HAR andformat=CURLexports, match-failure
explanations, error log entries, failed response verifications, and — newly — the bytes of a binary (non-HTTP) message in a "matched"/"didn't match" log entry (shown as***REDACTED***, where the "received binary request" message already masked them);redactSecretsInRecordedExpectationsnow also
masks cookie values. Masking now applies even to entries recorded before the setting was switched on, since redaction happens when an entry is shown or retrieved, not when it's written; what's not retroactively fixed is anything captured by MockServer before this release — treat logs, exports
and recordings captured pre-upgrade as still containing those values in full./mockserver/generateExpectationand LLM drift analysis now redact credentials from the prompt sent to an external LLM backend, unconditionally — sensitive headers, credential-shaped query parameters and JSON fields, OAuth codes, JWT-shaped tokens, and embedded URL credentials are replaced with
***REDACTED***before the prompt leaves MockServer, independent ofredactSecretsInLog. The same unconditional redaction covers the newnarrationfield onPUT /mockserver/explain/explainUnmatched(see Added): credential values are masked whether or notredactSecretsInLogis on. The
explain_request,explainUnmatchedand other MCP/REST diagnostic output, by contrast, is redacted the same way the event log is — i.e. only whenredactSecretsInLogis enabled — exceptdiagnose_config, which always masks credential values. Treat any credentials sent through
generateExpectation/drift analysis with a backend configured as previously exposed to that LLM service.
Added
- AI-assisted debugging: ask MockServer why a request matched or didn't, fix near-misses, and dry-run templates — via REST, the dashboard and new MCP tools.
- Ask why one request matched the expectation it did, and why a higher-priority one lost:
PUT /mockserver/explain?correlationId=<id>. Give it the correlation ID of a recorded request (fromPUT /mockserver/retrieve?type=LOGS&format=LOG_ENTRIESor the dashboard) and MockServer checks that
request again against your current expectations, in the order it evaluates them. The answer names the expectation that would serve the request now, gives a reason for every expectation checked before it (different fields; another namespace; used up or expired; waiting for another scenario
state; served only a percentage of the time; LLM conversation mismatch; anot(...)request matcher that excludes the request), lists lower-priority expectations that also match but are never reached, and reports how the request was handled when it arrived, including, when MockServer forwarded
the request and the forward failed with a 502, the expectation that forwarded it (or that it was proxied without one) and the reason the forward failed. At most 500 expectations are checked per call. The route has no bare/explainalias, so a mock of your own onPUT /explainkeeps working.
In the dashboard, every group of log messages for a request has an Explain button that shows the same answer, and the Java client has a typedexplain(correlationId)method returning aRequestExplanation. - AI assistants can now ask MockServer why a request matched the expectation it did, and which settings are in effect, through two new MCP tools.
explain_requesttakes the correlation ID of a request MockServer has received and returns the same answer asPUT /mockserver/explain. An unknown
or evicted correlation ID comes back as a tool error.diagnose_configlists the configuration settings changed from their default, with the value and where each came from (system property, properties file, environment variable or runtime setter), or looks up one setting by name in any form
(maxExpectations,mockserver.maxExpectations,MOCKSERVER_MAX_EXPECTATIONSormax-expectations), showing a setting at its default with the default's value, and suggests the setting you probably meant when nothing matches. It also lists eachmockserver.*/MOCKSERVER_*key you set
that MockServer does not recognise, with the setting it was probably meant to be (by name only, never its value). Both are read-only, so a principal with only the READ role can call them when control-plane authorization is on. Their output is redacted:diagnose_configalways masks
credentials, andexplain_requestmasks credentials quoted from your traffic whenredactSecretsInLogis enabled, as the event log does. - Every client can now ask why a request matched, or did not, and get a typed answer back. The JavaScript (Node), Python, Ruby, Go, Rust, .NET and PHP clients gain typed
explain,debugMismatchandexplainUnmatchedcalls (explain,debug_mismatchandexplain_unmatchedin Python, Ruby and
Rust;Explain,DebugMismatchandExplainUnmatchedin Go and .NET, which also hasAsyncversions), joining the Java client's typedexplain.explaintakes the
correlation ID of a recorded request and returns which expectation would serve it now, why each expectation checked before it was passed over, which lower-priority ones it hides, and how the request was handled when it arrived;debugMismatchcompares a request you describe with every active
expectation, field by field;explainUnmatchedlists the closest expectations for recent unmatched requests, optionally with a suggested JSON patch for each. An unknown or evicted correlation ID fails with the server's reason. - Fix an almost-matching expectation from the dashboard's Explain Unmatched view. Next to each close expectation, Preview Fix shows the smallest change that would make it match the unmatched request (for example the path, one header's value, or one field of a JSON body) and the request
matcher before and after; Apply Fix updates the expectation only when you select it, and refuses if anything it would change was edited in the meantime. Over REST, add"suggestFixes": truetoPUT /mockserver/explainUnmatchedto get each suggestion as a standard JSON Patch. Values the
log redacts, and bodies the log kept only part of, are never copied into a fix. - Find expectations that can never match before you debug a 404: expectation lint.
PUT /mockserver/lintchecks your active expectations, or the expectations you send in the body without storing them, andmockserver lint expectations.jsonchecks files with no server running. It reports an
expectation that another one evaluated earlier always matches first (shadowed),times,timeToLiveorpercentageof 0, a path without a leading/, a misspelt method or header name, and STRICT JSON matching of a body with arrays, among other checks. Creating expectations withPUT /mockserver/expectationnow runs the quick per-expectation checks too: the expectations are still stored, and any problems are counted in anx-mockserver-lint-warningsresponse header and logged as aWARN. Expectations loaded from aninitializationJsonPathfile are checked too, each
time the file loads. Lint is advisory and never rejects an expectation. In the dashboard, a Lint button on the Active Expectations panel lists the findings with their severity, rule and message. - AI assistants can now ask MockServer why a forward failed, how to fix a near-miss expectation, which expectations can never match, and what a template renders, through four new MCP tools:
explain_forward_failure,suggest_expectation_fix(the same guarded JSON Patch as
explainUnmatched's"suggestFixes": true),lint_expectations(returning at mostmaxIssuesissues, default 100), andtest_template. The first three are read-only, so a principal with only the READ role can call them when control-plane authorization is on;test_templateruns the
template code it is sent, so, like its REST endpoint, it needs the mutate role. Output is redacted as the event log is whenredactSecretsInLogis enabled, and a forward failure's reason is cut at 2,048 characters. - Try a response or forward template without creating an expectation:
PUT /mockserver/template/test. Send a Velocity, Mustache or JavaScript template and an optional sample request, and MockServer renders it with the same engines, security settings and time limits it uses when serving. It
returns the rendered response (or the request, for a forward template), or the reason it failed with the line and column in your template. Output that is not a valid response comes back with what is wrong with it and the raw text; serving it would answer with a 500 (a 404, or a 502 for a
forward template, withtemplateFailureAsServerError=false). Warnings point to each Velocity reference that did not resolve, to each Mustache variable that did not resolve and so rendered as empty text, and to eachscenario.set(...)call, which a dry run reports rather than applies; a dry
run never changes scenario state or appears in the event log. A JavaScript or
Velocity template is always stopped after its time limit, even when that limit is 0 (then after 5 seconds); a Mustache template, or a Velocity template busy inside one long Java call, cannot be stopped, so the dry run answers once the limit plus 5 seconds has passed; at most two dry runs run
at once, and a third gets a 429. When control-plane authorisation is on, a dry run needs the mutate role. The route has no bare/template/testalias, so a mock of your own on that path keeps working. - A failing JavaScript template now says where in your template it failed. GraalJS counts lines from the wrapper MockServer puts around your template, so a syntax error named a line two below the real one. The error in the log now ends with
at template line 2, column 29, counted from your
template's first line, and a hint when the cause is an undefined variable, a field read from a missing value, or an unclosed bracket or quote. Velocity and Mustache errors already gave the line, and are unchanged. PUT /mockserver/explainandPUT /mockserver/explainUnmatchedcan add a plain-language summary from your own LLM. When the newllmExplainNarrationEnabledproperty is on (defaultfalse), a runtime LLM backend is configured (llmProvider, or a provider API-key environment variable)
and the caller adds?narrate=true, the response gains anarrationfield, labelled as AI-generated, beside the unchanged deterministic explanation. A backend configured for other features is not enough on its own, and nothing is sent while a request is being served. Credential values from
the request and your expectations are replaced with***REDACTED***before the explanation leaves MockServer, whether or notredactSecretsInLogis on. The call runs off the request thread and is bounded byllmRequestTimeoutMillisplus two seconds. When narration is turned off, no backend
is configured, the LLM fails or times out, or two summaries are already running,narrationsays why it is unavailable instead of returning an error.- Three more built-in MCP prompts walk your assistant through the commonest failures.
why_did_verify_fail,fix_my_expectationandwhy_forward_failedtell your own assistant which MockServer tools to call, in which order, and what each result means; MockServer still calls no language
model for these three.fix_my_expectationchanges an expectation only after you agree, and replaces it in place, starting from the fix MockServer suggests withsuggest_expectation_fix;why_forward_failedstarts from the reasonexplain_forward_failurereports, falling back to comparing
fields or reading the logs only when MockServer has no fix, cannot name the cause or recorded no failed forward.
- Ask why one request matched the expectation it did, and why a higher-priority one lost:
- MySQL, Redis, PostgreSQL and length-prefixed binary protocols can now be framed and matched as whole messages, and binary proxying gained several capabilities.
binaryMessageFraming(defaultRAW, unchanged) now also takesMYSQL,REDIS,POSTGRESQLandLENGTH_PREFIX(the last configured
viabinaryMessageLengthPrefixBytes(1, 2, 4 or 8, default 4),binaryMessageLengthPrefixByteOrder(defaultBIG_ENDIAN),binaryMessageLengthPrefixOffset(bytes before the field, default 0) andbinaryMessageLengthIncludesPrefix(whether the length counts the whole message, default
false), which fits protocols such as Thrift's framed transport, MongoDB's wire protocol and DNS over TCP. As withPOSTGRESQL, a message may be at mostmaxRequestBodySizebytes, a connection that declares a longer one or sends bytes the framing does not allow is closed, and an unrecognised
value set as a property is logged once and read as the default. To proxy MySQL, whose server speaks first, also setforwardBinaryServerFirstWaitMillison a connection with a forward target (port forwarding, transparent proxy or PROXY protocol); mocking MySQL with no upstream server is not
possible, as MockServer cannot send the server's greeting. A TLS handshake on a MySQL connection is recognised only straight after anSSLRequest.- A binary expectation on a proxied PostgreSQL connection can now forward the message too, and drop or replace the server's reply. Set the binary response's new
upstreamfield (Java:binaryResponse(bytes).withUpstream(BinaryResponse.Upstream....)):ANSWER_ONLY(the default, unchanged)
answers and does not forward;ANSWER_AND_FORWARDanswers, forwards the
message so the real server still sees it, and drops the server's reply;FORWARD_AND_REPLACEforwards the message and writes the binary response in place of the server's reply once that reply has ended (with adelay, the server's later replies wait behind it, so the client gets them in
order). Both needmatchBinaryExpectationsWhenProxying(on by default) andbinaryMessageFramingset toPOSTGRESQL, which tells MockServer where a reply ends; without that framing the expectation answers asANSWER_ONLYand one warning is logged per connection. The field is in the JSON schema,
the OpenAPI spec and every client library. - A protocol in which the server speaks first, such as MySQL, SMTP or FTP, can now be proxied, with the new
forwardBinaryServerFirstWaitMillis. Set it (for example to250) and a port-forwarded, transparently proxied or PROXY-protocol connection whose client sends nothing for that long is
taken as binary: MockServer opens its upstream connection and relays the greeting, and the rest of the session. It is off (0) by default, because a client that opens a connection and sends HTTP only after the wait would then be relayed to the upstream rather than matched against your
expectations. - A
binaryProxyListenercan be told what the upstream server sends unprompted: the newonUpstreamMessagemethod is called for every read from the upstream server that is not a message's response — bytes it sent unprompted, and the rest of a response that arrived in more than one read. It
is a default method that does nothing, so existing listeners, lambdas included, are unaffected and are not called for these reads. A listener that overrides it and falls more than 64 calls behind slows the upstream server down, except while a message is waiting for its response. - Separately,
matchBinaryExpectationsWhenProxying(on by default; setfalseto always forward) lets a binary expectation answer some messages on an otherwise-proxied connection relayed on one upstream connection, matched only when its bytes equal the expectation's exactly, so a test can use a real server for most
of a session and fake one part of it, such as a canned result for one PostgreSQL query. The upstream never sees an answered message, so the canned reply must suit the session's real state.
- A binary expectation on a proxied PostgreSQL connection can now forward the message too, and drop or replace the server's reply. Set the binary response's new
- A configuration setting you have changed now shows the built-in default it replaced.
--print-configends the setting's line with(default: ...),GET /mockserver/configand the MCPdiagnose_configtool add adefaultfield, and the dashboard's Server Info tab shows the default beside
the value. A default worked out from the JVM heap size, such asmaxExpectations, shows the value worked out now. A credential's default is always shown as***REDACTED***, anddefaultis left out when MockServer cannot work it out.--print-config,GET /mockserver/configand the dashboard's Server Info tab also now list the configuration MockServer ignored: after the properties in effect they list each unrecognisedmockserver.*/MOCKSERVER_*key with its suggested correction (name only, never the value it was set
to), andGET /mockserver/config?nonDefault=truelists only the properties not at their built-in default, which is everything you have changed. A property at its built-in default now shows that default's value instead of(default); a default worked out from another property, such as
ringBufferSize, shows the value worked out now. Credentials are still shown only as***REDACTED***, including at their default.- Eight new event-log gauges on
/mockserver/metrics: four for the in-flight ring (mock_server_event_log_ring_occupancy,_ring_capacity,_in_flight_bytes,_max_in_flight_bytes) and four for the retained deque (mock_server_event_log_retained_entries,_retained_bytes,
_max_retained_entries,_max_retained_bytes). - Expectation-store and accept-queue metrics:
mock_server_expectations_bytesreports live memory held by the store;mock_server_max_expectations_bytesthe byte budget in force (0whenmaxExpectationsSizeInBytesis off);mock_server_expectations_byte_evicted_totalcounts byte-driven
evictions.mock_server_accept_queue_backlog_configuredreports the configured accept-queue depth; on Linux,mock_server_accept_queue_backlog_effectivereports the kernel-capped value. - New JVM-level metrics:
jvm_runtime_info(gauge, always 1, reports the running JVM/GC in labels),jvm_memory_allocated_bytes(a monotonic gauge of cumulative bytes allocated across all threads),jvm_buffer_pool_used_bytes/jvm_buffer_pool_used_buffers(NIO buffer pools), and
netty_direct_memory_used_bytes(Netty's own direct-memory tracking). - Two new histograms:
mock_server_request_read_to_decode_duration_seconds(from MockServer reading the first bytes of a request to it having decoded the headers) andmock_server_request_transport_duration_seconds(from reading a request's headers to the last byte of its response being
written, including delays — unlike the existingmock_server_request_duration_seconds, which stops when a response is handed off for writing). mock_server_inbound_connections_open,mock_server_inbound_connections_rejected_totalandmock_server_inbound_connections_idle_closed_total(seemaxInboundConnectionsbelow).mock_server_scheduler_queued_tasksandmock_server_template_action_queued_tasksshow the depth of the action and template-render queues, which are deliberately not limited because their work must not be dropped.
- New safety limits for stored expectations, pending work and connections, each configurable and each off or generous by default.
maxExpectationsSizeInBytes(default 0 = off) caps stored-expectation memory by byte budget alongside the existingmaxExpectationscount.
maxPendingDelayedResponsesandmaxQueuedTemplateActions(both sized from heap, capped at 100,000, or 1,000 when the JVM reports no heap limit;0restores the old unbounded behaviour) bound requests waiting on a delay or a template thread, answering503 Service UnavailablewithRetry-After: 1past the limit instead of growing memory without bound (a refused request still counts as a match; delayed side actions are dropped, not refused, once their own equal-sized budget is full; a forwarded response skips its chaos latency rather than being refused); delayed WebSocket replies get the
same budget, closing the WebSocket with status1013(Try Again Later) once full, and reads pause at 128 waiting replies per connection (a fixed constant, no setting) so a fast-sending client is slowed by TCP flow control instead of growing memory; reply frames are now sent strictly in delay order.
maxInboundConnections(default 0 = no limit) caps open client connections: a connection beyond the limit is reset at once instead of being accepted, a warning is logged at most every 10 seconds, and new connections are accepted again as soon as one closes. A CONNECT/SOCKS tunnel to MockServer
itself uses two slots, and if its second is refused the client now gets a502instead of waiting forever.mockserver.soBacklog(default 1024, previously hard-coded) makes the TCP accept-queue depth configurable; the effective depth is still capped by the kernel (net.core.somaxconnon
Linux,kern.ipc.somaxconnon macOS). - New Linux-only standalone jars and an HTTP/3 Docker image variant.
-jar-with-dependencies-linux-x86_64.jarand-jar-with-dependencies-linux-aarch_64.jarare ~13 MB smaller by carrying only their own architecture's natives.mockserver/mockserver:<version>-http3(also
mockserver-<version>-http3,latest-http3, andsnapshot-http3/mockserver-snapshot-http3formasterbuilds; Helmimage.variant=http3) is the standard image plus the QUIC native, needed to actually serve HTTP/3 (see Upgrading from 8.0.0). - Dashboard: template dry-run, log-history tuning, and status-ranged breakpoints. The Mocks view's response/forward template steps gain a Test Template button (see above). The dashboard WebSocket now accepts
?logLimit=N(up to 500, default 100) for more log/request rows per update. The
Breakpoints form gains Min status/Max status fields and astatus:scope-box syntax, limited to the Response phase. - A template error body now gives the template line and column and a hint for common mistakes, when the engine gives them, following the engine's error message, so a template bug is easy to tell apart from a request that matched no expectation (see the BREAKING
500-by-default entry under
Changed for the response shape and migration). - A byte budget for memory the OpenAI Responses API mock keeps for
previous_response_id/GET /v1/responses/{id}(maxOpenAiResponsesSizeInBytes, default a twentieth of the heap;0limits by count only). - A startup warning for each insecure default a future major release will change (
forwardProxyBlockPrivateNetworksto becometrue,localBoundIPto become127.0.0.1,attemptToProxyIfNoMatchingExpectationto becomefalse, and the HTTP parsing limits
maxInitialLineLength/maxHeaderSize/maxChunkSizeto become 8192/16384/16384 bytes), once per JVM; setting the property yourself silences its warning, so you can choose now and not be surprised when the defaults change. No default changes in this release. Misspeltmockserver.*/MOCKSERVER_*properties now get a "did you mean" suggestion at startup, in the
form you used (MOCKSERVER_METRICS_ENABLEsuggestsMOCKSERVER_METRICS_ENABLED,mockserver.max_expectationssuggestsmockserver.maxExpectations), naming the property only, never the value (see the--print-config/GET /mockserver/configsub-bullet above for where else ignored keys and
defaults are listed). - A failed verification now ends with a short summary of what MockServer actually saw (
detailedVerificationFailures, default on): the expected vs. actual count, the closest non-matching requests/responses with their differing fields, and — for a sequence — which step failed. The block is
appended, so code that checks the start of the message is unaffected; setdetailedVerificationFailures=falseto drop it. - Client parity: conditional matchers and per-step load checks. The Python, Go, .NET, Rust and PHP clients can now build a conditional (if/then/else) request matcher and read one back: Python adds
ConditionalRequestDefinition(if_request,then_request,else_request,not_condition); Go
addsIf/Then/ElsetoHttpRequestwithConditionalRequest(...); .NET addsIf/Then/ElsewithHttpRequest.RequestIf(...); Rust and PHP addHttpRequest::conditional(...); the .NET, Rust and PHP request matchers also gain the OpenAPI fields (specUrlOrPayload,operationId,
contextPathPrefix) a branch can use. The same five clients' load-scenario steps now take per-step checks (LoadCheck:source,headerName,jsonPath,comparator,value, named in each language's style) and acheckslist; Go, .NET and Rust also gain a typedCHECK_FAILURE_RATE
threshold metric, and Rust'sLoadStepgainsname(...)/label(key, value)andLoadScenariogainslabel(key, value)(see the "Every client can now ask why a request matched" sub-bullet above for theexplain/debugMismatch/explainUnmatchedparity). - Website: a symptom-first Troubleshooting Index, linking a visible symptom (404, forward 502, failed TLS handshake, empty dashboard, LLM mock shape mismatch, container won't start, etc.) to the exact diagnostic to use, and a new "Throughput by hardware size" section on
performance.htmlshowing
healthy/peak request rate, latency and request-history retention from 1 core/512 MB up to 6 cores/2 GB, driven by four load generators per size so the test measures MockServer rather than the generator. - The DNS port MockServer chose can now be read from every embedded API, not just
MockServer.getDnsPort()and the start-up log: alsoClientAndServer.getDnsPort()(including on theClientAndServerthe JUnit 5 extension injects),MockServerRule.getDnsPort(), the Spring listener's
${mockServerDnsPort}placeholder and@MockServerDnsPortannotation, and thednsPortfield ofPUT /mockserver/status's answer (each gives-1, or the field is left out, while DNS mocking is off).@MockServerTestnow also acceptsmockserver.dnsEnabledandmockserver.dnsPort.
Changed
- BREAKING: binary (non-HTTP) proxying now keeps one upstream connection for a client's whole life; forwarding one message per upstream connection, and
forwardBinaryRequestsWithoutWaitingForResponsewith it, are removed. The upstream now sees one connection per client connection, held open
as long as the client's is: everything it sends reaches the client, closing its connection closes the client's too, and a client whose upstream never answers is no longer cut off aftermaxFutureTimeout(there's no time limit on the upstream answering any more — TCP keep-alive,
forwardSocketKeepAlive, still detects a dead upstream). A client that turns TLS on part way through (PostgreSQL'sSSLRequest, as withsslmode=require) now has its upstream connection upgraded to TLS on the same connection, so such a database can be proxied, presenting
forwardProxyPrivateKey/forwardProxyCertificateChainif the upstream asks for a client certificate (a handshake that fails or exceedssocketConnectionTimeoutInMilliscloses both connections with a warning) — SCRAM channel binding needschannelBinding=disable(JDBC) /
channel_binding=disable(libpq/psql) in the client, or MockServer given the server's own certificate and key (privateKeyPath,x509CertificatePath, andcertificateAuthorityCertificateset to the authority that signed it) since a client certificate cannot be passed through, and PostgreSQL
17'ssslnegotiation=directis not supported. A client that starts with TLS from its first byte is relayed the same way, with MockServer opening the upstream connection with TLS from its first byte too, with the same certificate checks (8.0.0 opened a new TLS connection per message).
binaryProxyListeneris now called as soon as each message is read, on a thread of its own, with the response being the first upstream bytes after that message ornull; the event
log now has one "returning binary response" entry per upstream read, not per message. Through an upstream proxy the one connection is always a tunnel now: SOCKS5 throughforwardSocksProxy, andCONNECTthroughforwardHttpsProxyor, when it is the only one set,forwardHttpProxy—
sendingforwardProxyAuthenticationUsername/forwardProxyAuthenticationPasswordasProxy-Authorizationwhen set. 8.0.0 sent a clear connection's bytes toforwardHttpProxyas if it were the server, and sent a TLS connection with onlyforwardHttpProxy, or a clear one with
forwardHttpsProxy, around the proxy; a connection whose only upstream proxy isforwardHttpProxyis no longer forwarded one message per connection — it now tunnels too. Many proxies refuseCONNECTto ports other than 443; if yours does, list the server innoProxyHosts. A proxy that
refuses the tunnel closes the client's connection with one warning naming the proxy, the setting it came from and the status it answered with (for407, whether credentials were sent).forwardProxyTLSX509CertificatesTrustManagerTypestill defaults toANY(trusts any upstream certificate)
for this upstream TLS; set it toJVM/CUSTOMto verify.forwardBinaryRequestsWithoutWaitingForResponseis removed: a value still set as a system property, environment variable or property-file key is ignored, with one warning at start-up saying it was removed; in a JSON configuration it
is ignored. Remove it from your configuration; nothing brings per-message forwarding back, and a protocol in which every message stands alone works the same on one connection. For Java code, theforwardBinaryRequestsWithoutWaitingForResponsemethods ofConfigurationPropertiesand
Configurationare removed, as are bothNettyHttpClient.sendRequest(BinaryMessage, ...)overloads (the relay connects throughNettyHttpClient.connectBinaryRelay, so a subclass that overrode them is not called) and the client-side binary codec classes (MockServerBinaryClientCodec,
MockServerBinaryToNettyBinaryRequestEncoder,NettyBinaryToMockServerBinaryResponseDecoder); remove calls to them. See Added for the framing and forward/replace capabilities this enables. - BREAKING: binary expectations now answer matching messages on a proxied binary connection by default (
matchBinaryExpectationsWhenProxying). 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). Now, on every such connection, which is relayed on one upstream connection, directly or through an upstream proxy's tunnel (forwardHttpProxyincluded), a message whose bytes match a binary expectation is answered by that expectation and is not forwarded; every other
message is forwarded as before, and with no binary expectations nothing changes. MockServer's replies and the upstream's are each kept in order, but not against each other, and MockServer logs a warning when one of its replies may overtake an upstream reply still owed; a message no expectation
could match is forwarded unmatched and adds no log entries. This affects you only if you proxy binary (non-HTTP) traffic while binary expectations are registered on the same MockServer. Migrate: to keep the 8.0.0 behaviour and always forward, set
matchBinaryExpectationsWhenProxying=false(-Dmockserver.matchBinaryExpectationsWhenProxying=false,MOCKSERVER_MATCH_BINARY_EXPECTATIONS_WHEN_PROXYING=false, or"matchBinaryExpectationsWhenProxying": falseinPUT /mockserver/configuration). - BREAKING: a request now writes at most 50 "request didn't match expectation" log entries by default.
maxLoggedMatchFailuresnow defaults to50instead of-1, so at INFO a request compared with more than 50 expectations logs the first 50 of those entries, and its closest-match entry
says how many were left out (for example10 further did-not-match entries were not logged (maxLoggedMatchFailures)). Every expectation is still compared, so which expectation serves the request, the closest-match entry, the 404 hint and explain are unchanged; what changes is how many of these
entriesretrieveLogMessages, theLOG_ENTRIESoutput and the dashboard log show. A flood of unmatched requests against many expectations now costs far less and takes far less of the event log, so fewer received requests are evicted before a verification reads them. Migrate: to keep the
old behaviour setmaxLoggedMatchFailures=-1(-Dmockserver.maxLoggedMatchFailures=-1orMOCKSERVER_MAX_LOGGED_MATCH_FAILURES=-1); set it if your tests or tools count these entries or expect one for every expectation. - BREAKING: when expectations in initialisation files repeat an
id, the last one is now kept, asPUT /mockserver/expectationdoes. Loading aninitializationJsonPathfile, at start-up or on a--watchreload, kept the first expectation for eachidin a file and ignored later ones, so a
file that repeats anidnow serves a different response. The kept expectation is matched in the place of the first one, as when the same expectations are sent toPUT /mockserver/expectation, and a file loaded later still replaces an earlier file's expectation with the sameid.
mockserver lintand the warning logged when a file loads now report each replaced expectation asDUPLICATE_IDand check the kept one for shadowing. Migrate: to keep the old behaviour, remove the repeatedids from your files, or put the expectation you want last. - BREAKING: HTTP/3 needs an opt-in jar or Docker/Helm image variant, and refuses to start rather than silently degrade. The QUIC native (~11 MB across five platforms) moved to the
-jar-with-dependencies-http3classifier (default jar: 104 MB → 93 MB); the QUIC classes are still bundled, so
settinghttp3Portwithout the native now refuses to start with a one-line message naming the cause and fix, instead of logging a warning and silently ignoring the port. This also refuses to start when the HTTP/3 port itself can't be used (most often because another application — or another
MockServer sharing the samehttp3Port, such as parallel test forks reading one properties file — already holds that UDP port); the message names the port and cause without a stack trace, and the ports MockServer had already opened are closed again before it exits.
mockserver-netty-no-dependenciescannot load the native at all (its relocated Netty looks for a differently-named library), and mountingnetty-codec-native-quic-<version>-linux-<arch>.jarinto/libsin the published Docker images does not work, for the same reason — use the-http3image tag
instead. Maven or Gradle users oforg.mock-server:mockserver-nettyalready getio.netty:netty-codec-native-quicfor their platform and need no change. The likeliest way to hit the port-already-in-use case without knowing is more than one MockServer started with the samehttp3Port— two embedded servers in one JVM, test forks running in parallel that share onemockserver.http3Port, or several instances on one host started from
the same properties file; give each its ownhttp3Port, or set it only where HTTP/3 is needed. Embedded in a JVM (ClientAndServer.startClientAndServer,new MockServer, the JUnit 4 rule, the JUnit 5 extension, the Spring test listener) the start throws aRuntimeExceptionwith that message and
the original error as its cause, as it does for a TCP port that is already in use. See Upgrading from 8.0.0. - BREAKING: MockServer refuses to start when DNS mocking is enabled and its DNS port cannot be used, for the same reasons and in the same style as the HTTP/3 change above (
dnsEnabled=true; most often an unavailablednsPort, a port below 1024 the process isn't allowed to bind — fixed by
dnsPort=5053published as-p 53:5053/udp, running as root, or loweringnet.ipv4.ip_unprivileged_port_start(NET_BIND_SERVICEdoes not help a non-root container user) — or more than one MockServer sharing adnsPort).MockServer.getDnsPort()returns the OS-chosen port whendnsPortis
left at its default of0; the configuration page's example port is now5053(its old example,5353, is the mDNS port and is normally busy on macOS). On macOS this now also includes adnsPortthat another application holds for IPv4 only (on0.0.0.0): the operating system would let
MockServer share it while sending the queries for127.0.0.1to that application, so MockServer refuses it, naming the conflict and thelsofcommand that finds the application. Embedded in a JVM (ClientAndServer.startClientAndServer,new MockServer, the JUnit 4 rule, the JUnit 5 extension,
the Spring test listener) the start throws aRuntimeExceptionwith that message and the original error as its cause, as it does for an HTTP port that is already in use. See Upgrading from 8.0.0. - BREAKING: the TypeScript typings for
mockserver-client/llmnow describe it as a plain object of named exports, not a default export, matching what the JavaScript has always done. A default import withoutesModuleInterop/allowSyntheticDefaultImportsno longer compiles (useimport llm = require(...),import * as llm from ...,import { llm } from 'mockserver-client', or turnesModuleInteropon); reading the module's.defaultproperty is now an error (drop.default); a value you declare with the same name as an imported builder type (Provider,Role, and the ten builder
names) is now a compile error (error TS2440) instead of silently shadowing the type import — use the imported value, or rename your import (import { Provider as ProviderName }). A module augmentation ofmockserver-client/llmstill compiles but reaches less — members added to a builder interface or toLlmare no longer visible on the factories/builders or on the object imported from that
path; augmentmockserver-clientor the newmockserver-client/llmTypesinstead (builder types now live there). Named imports,import * as, andrequirein type-checked JavaScript all now type-check;TurnBuilderis now declared alongside the other builder constructors. See Upgrading from
8.0.0. - BREAKING:
LOG_ENTRIESoutput writes each request/response body once, instead of up to three times over. A log entry's ownhttpRequest/httpResponsestill carries the full body; itsmessageandarguments, and theexpectationrecorded for a proxied exchange, now refer to that
request/response by a short form ("POST /orders","201") instead of repeating it — a log of large bodies now retrieves at about a third of its former size (a 1 MiB zero-byte request body went from a 19.9 MB entry to 6.3 MB; 1 MiB binary, 4.2 MB to 1.4 MB). Theexpectationrecorded for a
proxied exchange keeps its id, method, path, headers and status code — only its bodies move to the entry's ownhttpRequest/httpResponse; an expectation that matched the request is still written in full, as are recorded expectations retrieved in any other format and Capture as Mock in the
dashboard. Applies to everyLOG_ENTRIESretrieve (type=LOGS,REQUESTS,REQUEST_RESPONSES,RECORDED_EXPECTATIONS) and the MCPretrieve_logs/raw_retrievetools; the Java client'sretrieveLogEntries().getArguments()returns the short form too. The dashboard and text-format retrieves
(JSON,JAVA) are unaffected and still show full bodies. There is no setting to restore the old form. - BREAKING: an unexpected failure inside MockServer while handling a control-plane request is now a
500with a generic message and correlation id, not a400carrying the exception's bare message (or, for anErrorsuch as a stack overflow, no response at all). Requests MockServer genuinely
rejects (bad JSON, invalid values, unsupported operations) are still400with a message saying what's wrong. This now applies uniformly across every control-plane endpoint, including ones that previously caught and reported their own faults as400(PUT /mockserver/import, chaos, load-scenario,
cassette, breakpoint, contract-test, AsyncAPI and configuration endpoints among them, plusGET /mockserver/llm/optimisationReport/PUT /mockserver/llm/diffRuns, wherediffRunsnow answers unreadable JSON with400rather than500, and both now answer500rather than an empty result when
traffic can't be retrieved), the WAR deployments, and HTTP/3 (which previously left such a request hanging with no response at all). It also now covers a fault on the path to a mocked response that happens outside the mocked response's own handling, such as setting up aCONNECTtunnel or checking
data-plane credentials. Client-side: the Java client raises500s asClientException, andretrieveLogsByCorrelationId/retrieveActiveExpectations(requestDefinition, format)now raise for any status ≥400 instead of returning the error's text as their result (a400is still
IllegalArgumentException); the PHP client raisesMockServerException(wasInvalidRequestException); the Rust client raisesError::UnexpectedStatus(wasError::InvalidRequest); Node/Python/Ruby/Go/.NET are unchanged; the dashboard shows it as an internal error with the correlation id. - BREAKING: a template that fails now answers 500, not 404. A response or forward template that throws, does not parse, runs out of time, or renders output that is not a valid response or request now answers
500with anx-mockserver-template-error: trueheader and the template error as a
plain-text body, becausetemplateFailureAsServerErroris now on by default. In 8.0.0 such a template answered404, the same as a request that matched no expectation (a forward template whose output was not a valid request answered502). The response template of a forward with overrides is
covered too: a failure there, or output that is not valid, answers the500instead of a502or the unchanged upstream response. The template error is still logged as before. Migrate: to keep the old behaviour settemplateFailureAsServerError=false
(-Dmockserver.templateFailureAsServerError=falseorMOCKSERVER_TEMPLATE_FAILURE_AS_SERVER_ERROR=false). - BREAKING: the Node client and launcher now have an
exportsmap. An ES module can now import a deep path without its.jsextension (mockserver-client/llm), and TypeScript finds the typings undernode16/nodenext/bundlerresolution, but a path the map doesn't name (a.d.tsfile,
anything undertest/, the launcher'stasks/mockServer.js) now fails withERR_PACKAGE_PATH_NOT_EXPORTED. The map names the package itself, each module with and without.js(client:index,llm,setupMockServer,mockServerClient,mcpMockBuilder,a2aMockBuilder,sendRequest,
webSocketClient, and the types-onlyllmTypesandmockServerforimport type; launcher:index,downloadJar,downloadBinary) andpackage.json. Resolve<package>/package.jsonand join a file's path to that directory if you need one by location. ThemockserverCLI command and
grunt.loadNpmTasks('mockserver-node')are unaffected. - BREAKING: the Rust client's enums are now
#[non_exhaustive], and server-sent enums gain anUnknownvariant. Amatchlisting every variant no longer compiles (add_ => ...); in exchange, a later MockServer release can add values without breaking your code, and an unknown value from a
newer server (for example a newresponseModeon a retrieved expectation) now reads asUnknowninstead of failing the whole response.Unknowncannot be sent back: serialising it, or sending an expectation/load scenario that holds it, returns an error and nothing reaches the server — use
upsert_rawto resubmit server JSON unchanged.LoadThresholdMetricalso gains aCheckFailureRatevariant, matching the new per-step load check threshold, and is affected by the same#[non_exhaustive]change. - BREAKING: a verification failure for too many matches now says "found … but should have been found …" instead of "not found". Affects
never(),exactly(n),atMost(n),between(m, n)andverifyZeroInteractions()when the upper bound is exceeded — for example "Request found 1 time but
should have been found exactly 0 times, expected:<…> but was:<…>" where it said "Request not found exactly 0 times, …" ("Response found …" for response verification; the loggedVERIFICATION_FAILEDentry's text changes the same way, to "request found too often, it should have been found …").
Failures for too few matches still start with "Request not found"/"Response not found". - BREAKING: a response
statusCodeoutside 100–999 is now rejected, with a400naming the field fromPUT /mockserver/expectation(and every client, in a response, response sequence, fallback response or SSE response). In aninitializationJsonPathfile, or expectations restored from a cloud
blob store viapersistedExpectationsPath, an invalid entry is instead skipped with aWARNnaming it (position, id, path) while the rest of the file loads (failOnInitializationError=truestill fails startup), and when that file is also the save target it's backed up to
<file>.invalid-entries-<timestamp>.bakbefore MockServer's first save over it (if the backup itself can't be written, MockServer logs an error and does not save over the file). A response template that renders an out-of-rangestatusCodenow logs an error and answers500by default
(templateFailureAsServerError; see the BREAKING template-failure entry above) or404withtemplateFailureAsServerError=false.
Migrate: search expectation files for"statusCode" : 0(what a blank 8.0.0 dashboard field saved) and other out-of-range values. - BREAKING: AsyncAPI message verification (
PUT /mockserver/asyncapi/verify, or the Java client'sverifyAsyncMessage) now answers400for fields it doesn't understand, instead of silently checking "at least 1 message". Migrate: putatLeast/atMost/exactlyinsidecount, e.g.
{"channel": "orders", "count": {"atMost": 0}}; the other valid top-level fields arechannel,payloadSubstring,payloadJsonPathandexpectedValue. - BREAKING: response verification now applies every body matcher, in both
PUT /mockserver/verifyandPUT /mockserver/verifySequence(regex, JSON path, JSON Schema, XPath, XML Schema, form, multipart, fuzzy, GraphQL, JSON-RPC, WASM, allOf,matchType,not), where before these were silently
replaced by an empty-object match and the verification passed whenever any recorded response had a JSON object body — a verification that passed only because of this will now fail. Migrate: if a response verification starts failing after you upgrade, the recorded responses do not match its
body matcher — correct the matcher or the system under test. The fix is in the server, so a client that sends verifications as JSON needs no change. Use the newresponse().withBodyMatching(...)in Java (a response built this way can only verify, not be returned as an expectation). - BREAKING: verifying with an OpenAPI request matcher now counts only requests for the matched operation, where before the matcher was ignored and every recorded request counted.
verifySequencewas unaffected. - BREAKING: GenAI trace spans for Gemini and legacy/prompt-string text completions now carry the correct OpenTelemetry operation names,
generate_contentandtext_completion, instead ofchat— matching the OpenTelemetry GenAI semantic conventions, with span names
generate_content <model>andtext_completion <model>to match. Affects Gemini's
generateContent/streamGenerateContent; legacy/v1/completions(incl. Azure), Mistral fill-in-the-middle/v1/fim/completions, DeepSeek/beta/completions, Ollama/api/generate; and forwarded Bedrock InvokeModel calls to models that take a prompt string rather than chat messages (Titan
Text, AI21 Jurassic-2, Cohere Command/Command Light, Meta Llama, Mistral 7B, Mixtral 8x7B, Mistral Small 24.02, DeepSeek-R1) — a mocked Bedrock completion is stillchat. Everything else (chat completions, Responses API, Anthropic Messages, Bedrock Converse and every other Bedrock InvokeModel
call, Cohere chat, Ollama/api/chat, OpenAI-compatible chat endpoints of Gemini/Vertex AI) is stillchat. If a query/alert matchesgen_ai.operation.name == "chat"or achatspan-name prefix, change it to matchchat,generate_contentandtext_completiontogether; token/cost metrics are
unaffected, since they're labelled by provider/model, not operation. Separately, embeddings and rerank spans are now namedembeddings <model>/rerank <model>instead ofchat <model>— update any query/alert selecting these by span name or operation too. - BREAKING: the Node client's
retrieveRecordedRequests/retrieveRecordedRequestsByIdare now typedPromise<RequestDefinition[]>, notPromise<HttpResponse[]>, because a DNS or binary filter can return non-HTTP results. Narrow each result toHttpRequest(e.g.if ('path' in request))
before reading HTTP-specific fields. - BREAKING: with control-plane mTLS (
controlPlaneTLSMutualAuthenticationRequired), the Java client now trusts onlycontrolPlaneTLSMutualAuthenticationCAChainfor MockServer's certificate, instead of also trusting MockServer's own CA (certificateAuthorityCertificate) — including the
MockServerClientbehind the JUnit 4 rule, JUnit 5 extension and Spring test listener. If the server certificate is signed by MockServer's default CA, add it to the chain, or the client fails the TLS handshake; without control-plane mTLS the client trusts what it did before, and its one-time INFO
notice about what it trusts now says this. - BREAKING: with control-plane mTLS, MockServer now accepts only client certificates signed by
controlPlaneTLSMutualAuthenticationCAChain, instead of also accepting ones signed by its own CA; with no chain set, MockServer now refuses to start if its own CA is the bundled default one (the
standalone server,ClientAndServer, the JUnit rule/extension, Spring, and the WAR) and answers400to aPUT /mockserver/configurationthat would lead to it — set your own CA withcertificateAuthorityCertificate/certificateAuthorityPrivateKey, or set the chain; MockServer logs a warning
when the chain you do set includes the bundled default CA. See Security. - BREAKING: the Go client and the Go Testcontainers module move to
/v9module paths, so they can be installed again. Go requires a module at major version 2 or above to end its path in/vN, and the Go module proxy refused the 8.0.0 tags of both modules: the client's path still ended in
/v7and the Testcontainers module's had no suffix, sogo getcould not install either at 8.0.0. From 9.0.0 the paths aregithub.com/mock-server/mockserver-monorepo/mockserver-client-go/v9andgithub.com/mock-server/mockserver-monorepo/mockserver-testcontainers/go/v9, and each major release
now moves them on automatically. Migrate: rungo get github.com/mock-server/mockserver-monorepo/mockserver-client-go/v9(andgo get github.com/mock-server/mockserver-monorepo/mockserver-testcontainers/go/v9if you use it), then change your imports from.../mockserver-client-go/v7and
.../mockserver-testcontainers/goto the/v9paths. - OpenAPI spec problems are now grouped by kind and precisely located (JSON pointer, or line/column for a syntax error), with hints for common causes; a spec that loads but breaks OpenAPI rules now loads (as before) but reports its problems via an
x-mockserver-openapi-load-problemsheader,
?includeLoadProblems=true(response becomes{"expectations": [...], "loadProblems": [...]}), thecreate_expectation_from_openapiMCP tool'sloadProblems, and the dashboard's OpenAPI import, which now reports an import that created no expectations as an error rather than a success. - The MCP
create_expectationtool now rejects a parameter it doesn't have instead of ignoring it, returning an error listing the unknown parameters, the accepted ones, and a likely intended name (body→responseBody,headers→responseHeaders); unknown fields insidechaos, a non-object
responseHeaders, and a non-stringmethod/pathare rejected the same way, while keys starting with_(such as a client's_meta) are let through.raw_expectationnow checks each expectation against the expectation JSON schema before creating any and lists every problem as a JSON path with
what's wrong there (naming the entry when an array was sent), instead of just "Failed to create raw expectation". - An embedding mock with
deterministicFromInputnow refuses an input longer than 1,048,576 characters, and the limit is configurable. 8.0.0 hashed an input of any length, and hashing allocates memory in proportion to the text, so one large request could use a great deal of memory. A longer
input now gets a400in the provider's own error format. The default is above the largest single input any real embedding provider accepts (Cohere Embed v4 takes up to 128k tokens), so a request the real provider would take still passes. Change it withllmEmbeddingMaxInputCharacters.
Random embeddings do not read the text and have no such limit. - Forwarded/proxied requests now carry the client's own
Accept-Encoding(filtered to what MockServer can decompress:gzip,deflate,zstd, andbrwhen Brotli4j is on the classpath) instead of always sendinggzip,deflate; a client sending none now has none sent on its behalf, and one
whose choices MockServer can't decompress getsidentity; the Java client likewise no longer addsAccept-Encoding: gzip,deflateto its own requests. - The dashboard no longer sends whole bodies, or the whole expectation store, on every update, and shortens long values. Bodies over 64 KiB are shown truncated with a Load Full Body button (
GET /mockserver/logEntryBody, with the same control-plane authentication and redaction as the rest of
the dashboard); Replay, Repeat, Clear, Copy as curl and Capture as Mock load the full body first (and decline, saying why, if it can no longer be loaded). The Active Expectations stream similarly truncates any value over 64K characters, and the live editor (Edit, Duplicate, Test, picking an
expectation, editing an LLM conversation) always loads the full expectation from MockServer first. An update is capped at ~16 MB overall, showing the newest rows with a note that older ones are hidden. A long body is now also decoded once per render — with or withoutredactSecretsInLog— rather
than on every update. - The performance page's headline figure is now ~136,000 req/s on six cores at a sub-millisecond median, measured with four load generators, replacing the single-generator figure (60,000 req/s, serving 57,149, p95 21.9 ms) that was actually the limit of the load generator, not of MockServer. The
new run drives one six-core MockServer from four k6 processes on the machine's other CPU socket: 136,327 req/s with no errors, median 0.104 ms, p95 3.3 ms, p95 under a millisecond up to 120,000 req/s; p99 is sensitive to the load generators' own GC pauses (9.57 ms counting only seconds without a
generator GC pause, 17.8 ms counting every second) and the rate itself varies run to run (136,000 req/s in 5 of 10 like-for-like runs, 144,000 in 3, 128,000 in 2) — the page publishes the most common rate and tells you to provision against 128,000 req/s. The old and new figures are not a
before-and-after (different machine, generators and rule); the charts and "Latency by action" table still come from the single-generator run.- Throughput no longer collapses past saturation. Offered more than it could serve, MockServer used to serve less as load rose (26,020, then 23,463, then 19,517 req/s at 32,000, 48,000 and 64,000 offered in an earlier measurement); it now keeps serving close to the offered rate right up to
a 59,905 req/s peak at 64,000 offered, with the median still under a millisecond.
- Throughput no longer collapses past saturation. Offered more than it could serve, MockServer used to serve less as load rose (26,020, then 23,463, then 19,517 req/s at 32,000, 48,000 and 64,000 offered in an earlier measurement); it now keeps serving close to the offered rate right up to
maxHeaderSizenow limits HTTP/2 and HTTP/3 request headers too, and response headers MockServer reads from an upstream, instead of a fixed 8 KB. HTTP/2 and HTTP/3 ignoredmaxHeaderSizeand were capped at a fixed 8 KB regardless of its value; both now take the limit from the same setting,
whose own default changed from unlimited to 256 KB in this release (see Security) — so by default HTTP/2 and HTTP/3 now accept headers up to 256 KB where they accepted 8 KB, and a value you've customised now applies uniformly across protocols. SetmaxHeaderSize=8192to keep the old
HTTP/2/HTTP/3 limit (this also then limits HTTP/1.1 to 8 KB), or2147483647to remove the limit on every protocol. Over HTTP/2 and HTTP/3 the size is counted as those protocols define it (each header's name and value plus 32 bytes, including pseudo-headers, after decompression), so a long URL
or many small headers reach the limit sooner than over HTTP/1.1. A request over the limit is answered431over HTTP/1.1 (connection closed) and HTTP/2 (that stream reset; the connection is closed instead if the headers as sent are more than a quarter over the limit); over HTTP/3 the connection
is closed withH3_EXCESSIVE_LOADand no431is sent; each refusal is logged once atWARN. Trailers, and a request sent through aCONNECT/SOCKS tunnel, are limited the same way. A forwarded/proxied response over the limit now gets a clean502instead of being passed on in part (see
Fixed).- Several overload and resource-exhaustion behaviours are now bounded by default, each with a setting to restore the old unbounded behaviour. Each of these logs or counts via a new metric when it bites (
mock_server_overload_rejections_total,mock_server_response_write_stalls_total, etc.);
requests within the limits are unaffected.- The event log is now bounded by size as well as count (
maxEventLogSizeInBytes, now on by default — a twentieth of heap atWARNand below, a twelfth atINFO+; set to0to restore count-only bounding, though an explicitly-set value you already relied on may now retain fewer entries since
structure/headers are counted too — raise it if so); averifywith an upper bound now fails rather than passes on a truncated log. - Delayed/templated responses and delayed WebSocket replies are capped (
maxPendingDelayedResponses,maxQueuedTemplateActions— see Added for the full behaviour, including the newmock_server_pending_delayed_tasks/mock_server_websocket_read_pauses_totalmetrics); this now also covers a
response or actiondelay,globalResponseDelayMillisand chaos latency. - A client that stops reading a response now has its response end incomplete after 1 minute of nothing being read (
responseWriteStallTimeoutMillis, set0to disable): an HTTP/1.1 connection (or the tunnel it reads through) is closed, and an HTTP/2 or HTTP/3 stream is reset while other streams
on the connection carry on, each stall logged atWARNand counted bymock_server_response_write_stalls_total(labelled byprotocolandscope); a streamed response's own idle-upstream timeout (streamIdleTimeoutSeconds) now measures only an upstream that sends nothing, not time spent
waiting for a slow client. The value applies to connections accepted after it is changed. - Idle client connections now close after 5 minutes by default (
inboundConnectionIdleTimeoutMillis); set it to0to restore the previous behaviour. Only a connection that has sent and received nothing for the whole timeout with nothing in progress is closed: one waiting for a delayed or
breakpoint-paused response, streaming a response (SSE, chunked, gRPC), carrying an open HTTP/2 stream, or used as a WebSocket or raw binary proxy is never closed by it. A TLS handshake counts as activity: the timeout starts again when the handshake completes, so a new connection has the whole
timeout to send its first request. A CONNECT or SOCKS proxy tunnel is closed on the same terms, together with MockServer's internal connection for it: once nothing has passed through it for the whole timeout and no request sent through it is in progress. A tunnel with a request still being
uploaded, a delayed or streamed response, or an open HTTP/2 stream is never closed by it. Anerror()action that keeps the connection open is not waiting for anything once it has sent its bytes, or if it sends none, so that connection counts as idle and is closed once the timeout passes, and
so is a CONNECT or SOCKS proxy tunnel that carried such a request, whatever the bytes were. Mainstream HTTP clients reconnect transparently; in rare cases a request sent at the exact moment of closure may need a retry. - Netty's off-heap buffer memory is capped at a quarter of max heap by default when MockServer runs as its own process (at least 64 MiB, never more than the heap; set
-XX:MaxDirectMemorySize/-Dio.netty.maxDirectMemoryyourself to override) — when the cap is reached, whichever connection next
needs a buffer is closed (which may be unrelated to the one that filled it), a forward fails with502, and the log saysdirect memory limit (io.netty.maxDirectMemory) reached; an HTTP/2 upstream connection may now open only one stream of its own at a time, with the same per-stream
body-piece limit. - TCP chaos
latencyMs/bandwidthBytesPerSecnow behave like a real slow link — inbound data delivered in arrival order, bandwidth shared across a connection rather than per read, and latency+bandwidth set together now add up instead of bandwidth replacing latency, with ~64 KiB held in
transit before MockServer stops reading (so an uploading client is slowed by TCP flow control, not buffered without limit). - Stopping MockServer no longer waits out the full
stopDrainMillisbehind an open SSE/streamed/WebSocket/raw-error()connection (see Fixed).
- The event log is now bounded by size as well as count (
mock_server_evicted_log_entries_totalnow counts evicted entries, not eviction episodes — expect much larger values and re-tune alerts; it is not comparable with pre-upgrade recordings. Similarly,mock_server_dropped_log_events_totalnow carries areasonlabel (ring_full,
in_flight_bytes); scripts reading an unlabelled line, or combining it with another metric in PromQL, need asum()wrapper. Two more metrics now guard against unbounded growth: the forwarded-request metrics label at most 500 distinct upstream hosts (further ones grouped under_other), and a
drift-analysis backlog that fills under a burst of proxied traffic now skips rather than queues without limit, counted by the newmock_server_dropped_drift_analyses_total.maxRequestBodySize/maxResponseBodySizenow have a minimum of 1 byte —0or a negative value previously refused every HTTP/1.1/HTTP/2 body but left HTTP/3 unlimited, and a negative value could stop connections being set up; every protocol now refuses bodies consistently.- A binary connection's "message" is now everything read in one pass (up to 256 KiB), not one network read — fixing message boundaries that previously followed read-size quirks (a run of small messages, then a larger one, could be split into many pieces, and expectations that matched each of two
back-to-back messages must now match the pair instead). What changes for an existing setup: messages that MockServer reads together are always one message — already so with the default (OpenSSL) TLS engine, and in the clear unless the first of them happened to end exactly where a read did (a
first message of 2,048 bytes, or a later one of 65,536 bytes, followed by another without a wait — those were two messages and are now one); with the JDK TLS engine, whichmockserver-netty-no-dependenciesuses because it ships without the native TLS library, two TLS records read together were
two messages and are now one. HTTP, HTTPS, SOCKS and HTTP/2 connections are read exactly as before. A forwarded message can now be up to 256 KiB, where it was capped at 64 KiB. No setting controls this. - Docker/Helm defaults changed for memory and latency behaviour:
- Images now use ZGC by default (
-aotkeeps G1 for its cold-start profile; override withJAVA_TOOL_OPTIONS=-XX:+UseG1GC— note that settingJAVA_TOOL_OPTIONSyourself for any reason replaces the default entirely, so include-XX:+UseZGCunless you want G1 back, and switching to G1 loses
the AppCDS start-up archive, trained under ZGC, so G1 start-up is slower on the standard/local images; on Helm setapp.jvmOptions; the-webhooktest-tool image is unchanged), cutting p95/p99 tail latency sharply at roughly unchanged peak throughput, at the cost of ~60–80 MB more idle RSS. - Five distroless image variants (root, root-snapshot, snapshot, GraalJS, webhook) move from Java 17 to Java 25 (Temurin LTS), and
-clusteredto Java 21 (Infinispan 14 needs an API removed in Java 24) — the standard/local/-aotimages already ran JDK 25/26 and are unchanged; Java 25 prints
one benign native-library startup warning. - The JVM heap cap drops from 75% to 45% of the container memory limit (512 MiB is now the smallest supported limit, 768 MiB for
-clustered), because ZGC's working-set-under-load plus off-heap footprint could otherwise exceed the container limit and get OOM-killed (at 75% of a 2 GiB limit
that footprint exceeded the limit, and even a cap part-way between 75% and the new 45% exceeded a 512 MiB limit under load). Behaviour change at every container size: the heap is three-fifths of what the 75% cap in 8.0.0 gave (232 MiB instead of 384 MiB at 512 MiB, 922 MiB instead of
1,536 MiB at 2 GiB), and the heap-derived defaults follow it — defaultmaxLogEntriesis now 27,136 at 512 MiB, 56,576 at 1 GiB, 115,456 at 2 GiB and 233,472 at 4 GiB (below its 250,000 cap it scales with the heap); against 8.0.0 that is fewer entries on small containers (8.0.0 gave
at most about 45,000 at 512 MiB and 92,500 at 1 GiB, less whatever heap was in use at start-up) and more on large ones (8.0.0's 100,000 cap at 2 GiB and 4 GiB). To keep a larger heap, set-Xmxexplicitly viaJAVA_TOOL_OPTIONS(keeping-XX:+UseZGC) and size the container--memory
limit for it — 256 MiB is no longer supported unless you also set a small explicit heap. - The documentation now lists the GitHub Container Registry images and the correct ECR Public path. Every release since 7.1.0 is also published to
ghcr.io/mock-server/mockserverand-webhook, with the same tags and digests as Docker Hub (except the experimental-aottags and snapshots) — a
way round Docker Hub's anonymous pull limit. The website's ECR Public example usedpublic.ecr.aws/mockserver/mockserver, which doesn't exist; it now usespublic.ecr.aws/t2x9c0i6/mockserver. - Images download less: the standard and
-aotimages ~23% less,-graaljs~19% less and-clustered~16% less (separate per-architecture native layers, uncompressed jar split into/mockserver.jar(10.9 MB) and/mockserver-deps.jar(45.7 MB) layers so unchanged dependencies reuse a cached
layer — see Upgrading from 8.0.0 for theENTRYPOINT-override impact); an upgrade that changes no dependency re-downloads about 84 MB less, since the dependency layer is then unchanged. Unpacked on disk each image is about 117 MB larger, because the jar is stored uncompressed. The
defaultmaxLogEntriescap (independent of the above heap-scaling change) raises from 100,000 to 250,000 on large heaps. - Helm pods become ready ~1 second sooner (readiness probe checks immediately and every second) and gain
app.terminationGracePeriodSeconds(default 45) andapp.preStopSleepSeconds(default 0, needs Kubernetes 1.30+) for graceful draining.
- Images now use ZGC by default (
- Shutdown is now effectively instant (~107 ms → ~0 ms per instance), having dropped a redundant fixed Netty "quiet period" wait; three forwarding/proxy-pass paths no longer block a pool thread for the whole upstream round trip, raising proxy concurrency under a small thread pool (measured: 4
threads, 24 concurrent 500 ms forwards, 4 in-flight/~3,110 ms wall → 24 in-flight/~505 ms). - Numerous allocation and CPU reductions on the request-handling, matching, logging and TLS-handshake hot paths — each independently measured, almost none changing observable behaviour (the one exception, the event-log writer's wake-up cadence, is called out below):
Content-Typeparsing,
HTTP/2 per-stream mapper/address derivation, header ingest, therespondBeforeBody/clear-by-path/expectation-registration paths (several from O(n) or worse down to O(1)/O(log n)), JSON/JSONPath/XPath match-failure and diff building, non-cryptographic id generation for non-security-sensitive ids
(session ids, TLS material, OIDC tokens and QUIC source-address token secrets still use the cryptographic generator — the one exception is the CRUD data-plane resource id, now unique-but-guessable rather than unguessable, a deliberate trade-off for a test fixture), header/query-parameter storage
(flat insertion-ordered store, shared instances for repeated connection headers), per-request object overhead (primitive fields instead of boxed), log-entry body storage (raw bytes canonical, decoded text rebuilt on demand), re-reading
metricsEnabled/dataPlaneAuthenticationRequired/otelPropagateTraceContext/validateRequestsAgainstOpenApiSpec/defaultResponseHeadersonly when configuration changes rather than on every request, and per-handshake TLS certificate/SAN bookkeeping (change Subject Alternative Name lists only
through theConfigurationmethods — mutating the set returned bysslSubjectAlternativeNameDomains()/-Ips()is no longer seen).- About a fifth less CPU per request at the
WARNandERRORlog levels, below the throughput ceiling. Every request's first log entry woke the event-log thread, which had gone to sleep after the previous one, and the Netty worker thread paid for that wake-up. The event-log thread now
checks for new entries every 1–10 ms while traffic is flowing, and is woken straight away only when it has been idle for 50 ms, when 256 entries are waiting, when the entries waiting hold more than a quarter ofmaxEventLogSizeInBytes, or when a verify, retrieve, clear or reset needs it, so
those operations see every earlier request exactly as before and are no slower. At the defaultINFOlevel there is no measurable change. A request's log entry can now reach the console log, disk capture (persistRecordedRequestsToDisk) and the dashboard up to 10 ms later than before. An
idle server still uses no CPU for the event log. - Disk capture (
persistRecordedRequestsToDisk) is also substantially cheaper per exchange and batches its writes (lines are no longer byte-for-byte identical to before, but still parse to the same JSON; a hard kill can now lose up to 8 KB of the most recent lines, where before it lost at
most the line being written), so far fewer log entries are dropped under sustained proxy load. - JavaScript templates are substantially faster and no longer degrade unrelated workloads. Templates previously constructed a throw-away GraalVM engine and re-parsed the script on every request; under concurrent load this monopolised the shared action-dispatch thread pool. The engine is now
shared process-wide with the parsed script cached — roughly a five-fold cut in render latency locally. Template rendering (Velocity, Mustache, JavaScript) also now runs on a dedicated bounded thread pool — a saturation of renders queues only among themselves and the event loop stays free; time
an unrelated request waited behind in-flight template renders fell from ~1 s to effectively zero. Three unnecessary allocations on the hot path are also eliminated: a response body whose bytes were already materialised in the correct charset is no longer encoded a second time on the way to the
wire (a recorded body whoseContent-Typedeclared a charset but whose bytes are malformed for it is now replayed verbatim instead of decoded with replacement characters and re-encoded); HTTP/3 requests with a text body no longer allocate the body twice on the way in; and forward-proxy
authentication no longer allocates a per-request Netty buffer for theProxy-Authorizationvalue. NottableString.withStyle(...)/withSchemaType(...)andParameter.withStyle(...)now return a new instance instead of mutating in place — code that relied on the mutation (called without using the return value) will no longer see the change.
- About a fifth less CPU per request at the
- Dashboard dependency
mermaidupgraded from 11 to 12 (diagram output — agent call graphs, scenario state diagrams — is unchanged). - JUnit rule/extension now default to "dev mode" (
maxLogEntries=1000,maxExpectations=1000instead of deriving from heap), cutting memory across many in-JVM instances; a suite relying on deeper log history should raise the caps explicitly or setmockserver.devMode=false. Only the JUnit 4
rule and JUnit 5 extension are affected (dev mode is JVM-global, so aClientAndServerconstructed directly after one has run in the same test fork does inherit the dev-mode sizes). - Several diagnostic/error messages now say what actually went wrong, with a fix, instead of a generic or misleading one: a forward whose connection fails now names the cause and a suggested fix in the
502and log (see "Forwarding and proxying correctness" below for the exact causes covered);
the dropped-log-events metric's one-time warning, the dashboard's log warning, and a log-truncated verification failure each now name the specific cause and its specific fix (lower log level vs. raiseringBufferSizevs. raisemaxEventLogSizeInBytes/maxLogEntries). Error log entries now carry the request or response they describe: a
failed forward, a request MockServer could not process, a relayed request or response, a request or response that could not be serialized, an undecodable query string and a Netty message that could not be mapped now carry the request or response they describe, whether or notredactSecretsInLog
is on. AretrieveLogMessages/retrieve?type=LOGScall filtered by request now also returns these entries, and the request appears in theirLOG_ENTRIESJSON and the dashboard's request pane. A request with a missingHostheader is now logged, and returned as the body of the400
response, with only the method and path, not the whole request as before. - A flood of unmatched requests no longer re-checks every expectation for the closest-match hint. When a request matches nothing, MockServer finds the closest expectation for the
x-mockserver-closest-match-hintheader (on by default), the opt-in mismatch diagnostic and the DEBUG log. It used
to compare the request with every expectation on every 404. It now remembers, for each method and path, which expectations already differ in method or path, and re-checks only the rest until the expectations change, so repeated 404s for the same endpoint cost a fraction of a full comparison. The
hint is the same as before. The comparison also stops after the first 1,000 expectations in serving order: with more expectations than that, the hint names the closest of those 1,000 and the header ends with; only the first 1000 expectations were checked(the opt-in mismatch diagnostic
body says"truncated": truewithexpectationsChecked).- A flood of unmatched requests at
INFOno longer compares each one with every expectation. At the defaultINFOlog level, a request that matched nothing was compared with every stored expectation so the log could name the closest one, writing one "didn't match" entry for each. With more
than 1,000 expectations this now stops after the expectations that share the request's method and path (and any whose method or path is not a plain value) plus 1,000 others, in serving order. In a benchmark with 5,000 expectations an unmatched request went from about 17 ms to about 4.6 ms.
When expectations were left out, the closest-match log entry ends withonly N of M expectations were checked, so a closer one may exist, and only the compared expectations get a "didn't match" entry. Used-up and expired expectations are still removed, and the namespace count in the DEBUG log
still covers every expectation. Stores of up to 1,000 expectations, and theWARNlevel, are unchanged. maxLoggedMatchFailureslets you cap how many per-expectation "didn't match" log entries are written per request (default50, a BREAKING change from unlimited logging — see the entry above;-1logs every comparison): every expectation is still compared for matching purposes, so the served
expectation, the closest-match hint and explain are all unchanged; the closest-match entry says how many were left out. The warnings MockServer logs when the log event buffer fills, or when the event log starts discarding entries atmaxLogEntries, suggest it as the way to keepINFO,
and so do the failure message of a verification with an upper bound (never(),atMost(n),exactly(n),between) that fails because the log lost entries for either reason, and the dashboard's view of that failure and its dropped-log-events banner.
- A flood of unmatched requests at
- MockServer's MCP server now rejects a
prompts/getcall missing a required argument, instead of returning the prompt with the placeholder (e.g.{{path}}) left in its text — JSON-RPC error-32602naming the missing argument(s) indata.missingArguments; anullargument counts as missing.
Removed
- Helm chart versions 5.3.0–5.14.0 (2018–2022) are no longer served from the HTTP Helm repository. 5.15.0 (the last Java 11-compatible release) and 6.0.0 onwards remain; any removed version is still installable from the OCI registry (
oci://ghcr.io/mock-server/charts/mockserver).
Fixed
mockserver.candidateIndexThresholdandmockserver.jsonSchemaAllowRemoteRefsno longer log a spurious "not a known configuration property" warning when set as a system property (-D), which is the only form that ever applied them; set via a properties file or environment variable they are
still ignored, and the warning now says to use-Dinstead.- Dependency clashes and classloading fixed in the
-no-dependenciesartifacts — user-visible if you use them, not internal-only.mockserver-netty-no-dependenciesno longer makes SLF4J warn "Class path contains multiple SLF4J providers" in a project that has its own SLF4J provider: the jar
bundles the Java Logger provider so that it logs when run on its own; that provider is now relocated and no longer registered, so SLF4J only ever finds your provider, and MockServer logs through it (setslf4j.provideryourself, or have your own provider on the classpath, and this never
overrides it). When your project has no SLF4J provider, MockServer selects the bundled one itself, so the CLI, the Docker image and embedded MockServer still log (#2772). The-no-dependenciesjars no longer ship GraphQL Java, picocli, Chicory, the RabbitMQ and Eclipse Paho clients, HdrHistogram,
jopt-simple, the JSR-305 andjavax.validationannotations, Reactive Streams, java-dataloader, JSpecify, Angus Activation and opentest4j under their original package names, where they could clash with the copies in your own project — they are now relocated undershaded_package, and the build
fails if a bundled library is left unrelocated without a documented reason; the few packages that stay unrelocated, and why, are listed on the Maven Central page (#2772). POM change: the JUnit and Spring-no-dependenciesintegrations now use your project's JUnit or Spring instead of bundling
their own copy —mockserver-junit-rule-no-dependenciesandmockserver-integration-testing-no-dependenciesno longer contain JUnit 4,mockserver-junit-jupiter-no-dependenciesno longer contains JUnit Jupiter and the JUnit Platform, andmockserver-spring-test-listener-no-dependenciesno
longer contains Spring and Micrometer; each POM now declares that framework as aprovideddependency. This is not a breaking change for a project that uses these integrations, since it already has the framework, but it now needsspring-testandspring-contextfor the Spring listener, and
JUnit 4.13 or later for the integration-testing base classes (#2772). - Dozens of provider-shape fixes across Anthropic, Bedrock, DeepSeek, Gemini, Ollama, OpenAI/Azure OpenAI, Groq, OpenRouter, Mistral, Cohere and Voyage mocks, so generated-client and strict-schema consumers no longer reject or misread mocked responses. Anthropic messages now send
"container": null
and"diagnostics": nullas the real API does (Bedrock InvokeModel responses gaincontainertoo, but notdiagnostics, since Bedrock does not support cache diagnostics); both now also sendstop_detailsand a completeusageobject (cache, service tier, thinking tokens —inference_geo
forANTHROPIConly, not Bedrock InvokeModel) matching the real APIs, and a non-AnthropicstopReason(stop,length,tool_calls) sent to Anthropic/Bedrock clients is now converted to Anthropic's own values (end_turn,
max_tokens,tool_use, orrefusalfor a content filter) so one expectation can be shared across providers; mocked Bedrock InvokeModel responses now send theX-Amzn-Bedrock-*-Token-Countheaders boto3-based clients (LangChain'sChatBedrock) read usage from, and a streamed
InvokeModelWithResponseStream's last chunk carries the same metrics. Bedrock Converse (/model/{model}/converse[-stream]) now returns the actual Converse shape instead of InvokeModel's (discussion #2757), with matching conversation-matcher and cost-tracking support, and forwarded Bedrock calls
reporting OpenAI-style usage (its/openai/v1/chat/completionsendpoint, and OpenAI-format models on InvokeModel) are now counted too; Bedrock chaos/enforcement errors use the AWS error body, not Anthropic's. DeepSeek, Groq and OpenRouter mocks now include"logprobs": nullon every choice, as
their real APIs do (MISTRAL,XAIandORCAROUTERresponses still leave it out, matching their published response examples); DeepSeek also gainssystem_fingerprintandusage.prompt_tokens_details; DeepSeek and xAI return reasoning text asreasoning_content, Groq and OpenRouter as
reasoning; DeepSeek addsprompt_cache_hit_tokens/prompt_cache_miss_tokens, xAI reports reasoning tokens outsidecompletion_tokens, Groq adds its timing fields andx_groq, and OpenRouter addscost/is_byok; all report streamed usage correctly (including DeepSeek's "no separate usage
chunk" behaviour, and Mistral/Groq/OpenRouter streams now carrying token usage withoutstream_options.include_usage, as those APIs do); anOPENROUTERmock now reportsnative_finish_reasonon every choice (the upstream model's own reason, alongside the
normalised one) and no longer passes a Gemini/ClaudestopReasonthrough unchanged asfinish_reason(in a stream the field isnulluntil the last chunk, and the closing usage chunk now carries one choice that repeats both values, as OpenRouter's does, where before it had an emptychoices
array). OpenAI/Azure Chat Completions and Responses mocks include previously-missing required fields (logprobs,access_programs,temperature,top_p).- A streamed OpenAI Chat Completions mock now reports token usage when the request asks for it. With
"stream_options": {"include_usage": true}in the request, the stream ends, beforedata: [DONE], with one extra chunk whosechoicesis[]and whoseusageholdsprompt_tokens,
completion_tokens,total_tokensand, when set,prompt_tokens_details.cached_tokensandcompletion_tokens_details.reasoning_tokens. Every other chunk carries"usage": null. The stream never carried usage, even though the documentation's streaming example set it, so the Vercel AI SDK
(which asks for usage by default), LangChainstream_usage, LiteLLM and Langfuse saw zero tokens. This applies toOPENAI,AZURE_OPENAIand the OpenAI-compatible providersXAI,GROQandORCAROUTER. A request that does not ask gets no usage chunk, as from the real OpenAI and Azure
APIs, and is unchanged;XAIandORCAROUTERfollow the same opt-in.MISTRALandDEEPSEEKnever send the extra chunk: they always putusageon the chunk that carries thefinish_reason, andDEEPSEEKadds"usage": nullto every other chunk only when asked.GROQalso always puts
the usage inx_groq.usageon that chunk. - A proxied LLM call whose response is streamed is now counted, so
llmCostBudgetUsdapplies to streaming clients. Token and cost tracking of forwarded LLM traffic read the response as one JSON document, so a Server-Sent Events stream failed to parse and was silently skipped, and an Ollama
stream was read only as far as its first line — coding CLIs and most agents always stream, so they recorded no tokens and no cost, emitted no GenAI span, and never tripped the cost budget. MockServer now reads the usage each provider reports in its stream as the stream is relayed: the final
usage chunk of OpenAI Chat Completions (and of Mistral, OpenRouter and Groq, which send it without being asked),response.completedfor the OpenAI Responses API,message_startandmessage_deltafor Anthropic,usageMetadatafor Gemini, themetadataevent of Bedrock ConverseStream, the
wrapped events of InvokeModelWithResponseStream (or, for a model other than Claude, the token counts Bedrock adds to the stream's last chunk), and thedoneline of Ollama. The call is counted when its stream ends, wherever in the stream the usage is and however long the stream. Nothing is
estimated: a stream that reports no usage is not counted, and MockServer now logs that, atWARNwhen a budget is set and at most once a minute for each provider and model. OpenAI Chat Completions only reports usage in a stream when the client setsstream_options.include_usageto
true, so set it for those calls to count towards the budget. A stream cut off after Anthropic'smessage_startis counted with the tokens reported up to then. - A mocked OpenAI Responses API tool-calling loop now completes end-to-end (
call_id,sequence_number, streamed argument/text deltas, fulloutputonresponse.completed);whenContainsToolResultFornow matches a tool result bycall_id(a chained turn must echo MockServer's owncall_id),
and withstreamingPhysicsset a streamed tool call is paced like text instead of arriving as one piece. - Ollama mocks now answer
/api/generatein its own shape (not/api/chat's), reportdone_reason, and handle preload/unload (empty-message) requests correctly; a streamed Ollama tool call now arrives before the final message. - A chaos content-filter block on a streaming request (Anthropic, Bedrock, Gemini) now streams in the provider's own event shape instead of one unparseable JSON body, and a non-streaming Gemini content-filter block no longer sends the invalid combination of a
SAFETY-stopped candidate together
withpromptFeedback.blockReason(a real blocked prompt has no candidates); OpenAI-family providers keep their pre-stream400. - Mocked Gemini
candidatesTokenCount/totalTokenCountno longer double-count thinking tokens (and proxied Gemini calls now count thinking tokens as output, affecting cost metrics). Voyage rerank mocks now report a non-zerousage.total_tokensestimate. - A tool call's configured
idis now the id the client actually receives across OpenAI-family, Anthropic, Bedrock, Gemini and Ollama providers (previously generated or absent).
- A streamed OpenAI Chat Completions mock now reports token usage when the request asks for it. With
- Embeddings and rerank mocks now return one correctly-shaped vector per input, honour the request's own size/count, and are counted in cost/trace metrics. Each provider's own input field (Gemini
content.parts[].text, Bedrock Titan/Cohere, Ollama/api/embeddings) is now actually read, instead
of all inputs embedding the same empty string; batches return one vector per entry; dimensions/embedding-type defaults match each provider, and malformed or over-limit requests (dimensions, input count, token totals, image size/format) get a provider-shaped400(and an expectation'sdimensions
must now be at least 1, rejected at creation rather than silently producing empty or erroring vectors). Rerank mocks now honourtop_n/top_kand match Cohere's/Voyage's real response shape (id,meta, echoedmodel,return_documents); Cohere's current/v2/rerankpath is now recognised
alongside/v1/rerank, and atop_n/top_kthat is not a positive whole number now gets a400in the provider's own error format (a Voyage request with more than 1,000 documents gets a400with Voyage's{"detail": ...}body). Mocked and forwarded embeddings/rerank calls across
OpenAI, Cohere (including a/v2/chatstream), Voyage, Bedrock and Vertex AI are now counted in GenAI trace spans and LLM token metrics, where several of these calls were previously invisible; Cohere and Voyage have no price in MockServer's pricing table, so their calls add tokens but no cost. - LLM cost/usage tracking is now correct across more gateways and call shapes: forwarded Vertex AI calls through its OpenAI-compatible endpoints and the multi-region hosts (
aiplatform.{us,eu}.rep.googleapis.com) are now counted with their token usage (previously misread as Gemini with no
tokens) — Gemini 2.5/2.0 and Llama 3.3 70B models on these endpoints are priced at Vertex's list prices, while AI21 Jamba and other models on these endpoints add tokens but no cost; native Gemini calls proxied through Vertex AI are now costed at Vertex's prices, not the Gemini API's, and the
These release notes are truncated. This release's changelog entry exceeds GitHub's 125,000 character
limit for a release body. Read the complete entry in
changelog.md.