Skip to content

MockServer 9.0.0

Latest

Choose a tag to compare

@jamesdbloom jamesdbloom released this 15 Oct 21:51
· 85 commits to master since this release

[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 /v9 module paths (the Go module proxy refused their 8.0.0 tags) — run go get github.com/mock-server/mockserver-monorepo/mockserver-client-go/v9 (and the Testcontainers module's /v9 path if you use it) and change your
    imports from the /v7/unsuffixed paths to the /v9 ones; see Changed.
  • HTTP/3 users (http3Port set): the QUIC native library now ships separately — use the -jar-with-dependencies-http3 jar or the mockserver/mockserver:<version>-http3 Docker 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 its dnsPort cannot be bound, instead of logging a warning and running without DNS. Leave dnsPort at its default of 0 to let the OS pick a free port, or set dnsEnabled=false.
  • TypeScript users of mockserver-client/llm: the deep-import typings no longer declare a default export; import from mockserver-client itself, use import llm = require(...)/import * as llm from ..., or turn on esModuleInterop (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 forwardBinaryRequestsWithoutWaitingForResponse setting (a value still set is ignored, with a warning); remove it from your configuration.
  • Anyone reading a request or response body from message/arguments of LOG_ENTRIES output (including the MCP retrieve_logs/raw_retrieve tools): read it from the entry's httpRequest/httpResponse instead — message/arguments now refer to that request/response by a short form
    such as "POST /orders" or "201".
  • Anyone treating a control-plane 400 as any failure, or catching the Java client's IllegalArgumentException for non-input failures: an unexpected server-side fault is now a 500 with a generic message and correlation id; 400 is now reserved for requests MockServer genuinely rejects. The
    Java client's retrieveLogsByCorrelationId and retrieveActiveExpectations(requestDefinition, format) now throw instead of returning the error's text.
  • Anyone importing a path inside mockserver-client/mockserver-node other than the package or a documented module: both packages now have an exports map, so an unlisted path (.d.ts files, tasks/mockServer.js, etc.) now fails to resolve — resolve <package>/package.json and join a file's
    path to that directory if you need one by location.
  • Control-plane mTLS, Java client: the client now trusts only controlPlaneTLSMutualAuthenticationCAChain for 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 with certificateAuthorityCertificate/certificateAuthorityPrivateKey.
  • Rust client users who match on its enums: every public enum is now #[non_exhaustive] (add a wildcard arm), and server-sent enums (ResponseMode, BinaryUpstream, CrossProtocolTrigger, the Load* enums) gain an Unknown variant that cannot be sent back; LoadThresholdMetric also 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 statusCode of 0 or outside 100–999 (most likely from a blank 8.0.0 dashboard status field): it is now rejected at creation with a 400, or skipped with a WARN (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's verifyAsyncMessage): put atLeast/atMost/exactly inside count, not at the top level — the endpoint now answers 400 for 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 operationId now count, instead of every recorded request (verifySequence was unaffected).
  • Anyone selecting LLM completions by gen_ai.operation.name == "chat": Gemini calls are now generate_content, and prompt-string completions (legacy /v1/completions, Mistral FIM, Ollama /api/generate, prompt-based Bedrock InvokeModel models) are now text_completion — update to match
    chat, generate_content and text_completion together.
  • TypeScript users of the Node client's retrieveRecordedRequests/retrieveRecordedRequestsById: now typed Promise<RequestDefinition[]> instead of Promise<HttpResponse[]>; narrow each result to HttpRequest before 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 to false to always forward as in 8.0.0.
  • Anyone relying on every "didn't match" comparison being logged: the new maxLoggedMatchFailures property now defaults to 50, so at INFO only the first 50 "didn't match" entries per request are logged; set maxLoggedMatchFailures=-1 to keep logging every comparison.
  • Anyone with expectations in an initializationJsonPath file whose ids repeat: the file now keeps the last expectation for each repeated id, not the first; remove the repeated ids, 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 answers 500 by default (templateFailureAsServerError); set it to false to 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 #define block 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 to 0 to remove it.
  • Netty upgraded from 4.2.17.Final to 4.2.19.Final, and BouncyCastle from 1.85 to 1.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 whose SETTINGS_INITIAL_WINDOW_SIZE change would
    overflow a stream's flow-control window now has its connection closed with FLOW_CONTROL_ERROR, as RFC 9113 requires, instead of having that one stream reset; and in a multipart/form-data request, a file part whose Content-Type is not a plausible media type (for example text) is now read as
    application/octet-stream, so a partContentTypes matcher for such a part must name application/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 DEL 0x7F, 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, runBinary and the mockserver command ran the bundle's mockserver.bat under cmd.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_OPTS is split on whitespace with quoted text kept together, so -Dx="a b" sets x to a b on Linux and macOS too. A shell option passed in spawnOptions is now ignored.
    • The Python client's binary launcher no longer starts MockServer through a shell on Windows. start() ran the mockserver.bat launcher with shell=True, so cmd.exe could treat characters in the launcher path or in extra_args as commands rather than text. It now runs cmd.exe itself with
      every argument quoted, so characters such as &, | and ^ reach MockServer unchanged. start() raises ValueError if the launcher path or an extra_args entry contains a double quote, a line break or NUL, or if together they contain more than one %, which cmd.exe would 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.exe on Windows. BinaryLauncher.start let Ruby quote the arguments for cmd.exe, which follows different rules, so characters in the launcher path or in extra_args could be read as commands. It now builds the
      cmd.exe command line itself and raises MockServer::Error if the launcher path or an extra_args entry 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.exe on Windows. They ran mockserver.bat through cmd.exe with the standard argument quoting, which cmd.exe does not follow, so characters in the launcher path (from MOCKSERVER_BINARY_CACHE or the
      user's cache folder) could be read as commands. Each now builds the cmd.exe command 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.exe on Windows. BinaryLauncher::start() let PHP quote the launcher path and $extraArgs for cmd.exe, which follows different rules, so characters in them could be read as commands. It now builds the cmd.exe
      command line itself and throws BinaryInstallException if 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_URL made of repeated / no longer takes the Node launcher seconds to process.
  • 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 dompurify resolves to 3.4.16 (DOM XSS via IN_PLACE sanitize with a
    node-removing hook, Dependabot alert 593); dashboard npm overrides pins lodash-es to 4.18.1 (_.template/_.unset/_.omit advisories; the affected functions are tree-shaken from the built dashboard anyway); the Node Testcontainers module's own development lockfile resolves newer undici,
    brace-expansion and @grpc/grpc-js (Dependabot alerts 581, 582, 584, 586, 591, 592 — the published package is unaffected, since it ships only dist).
  • A zstd request or streamed compressed upstream response can no longer exhaust MockServer's memory. A zstd frame declares its decompressed size, and MockServer previously allocated that much up front (17 bytes claiming 1.5 GB threw OutOfMemoryError on a 256 MB heap) — now decompressed in
    64 KB pieces, with maxRequestBodySize/maxResponseBodySize enforced 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 zstd stream, 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 towards streamIdleTimeoutSeconds (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 for responseWriteStallTimeoutMillis).
    • 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 CONNECT tunnel is limited the same way, but by maxRequestBodySize (10 MB by default) — the limit that tunnel already applied to a response it collected whole, not maxResponseBodySize; through such a tunnel this backpressure is new for a streamed response, which before
      was queued in memory as fast as it arrived.
  • 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 by maxInitialLineLength and the trailers only by maxHeaderSize, 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 answers 400 and closes the connection once more than 8 KB of
    one chunk-size line, or of the final 0 line and its trailers counted together, is waiting to be read, on requests sent to it directly and through a CONNECT or 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 lower maxInitialLineLength or maxHeaderSize still 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) and maxHeaderSize (256 KB) were previously unlimited; set either to 2147483647 to remove the limit. A longer
    request line now gets 414, larger headers 431, 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 an ERROR and matched/answered from whatever had been read (a request
    line over the limit, for example, as GET /bad-request). This also applies through CONNECT/SOCKS tunnels. A request is also now unreadable, and gets 400, when the request before it on the same connection left bytes behind — for example a client that writes a GET body with neither
    Content-Length nor chunked encoding, as Node's http.request does — where it used to get 404 for GET /bad-request. HTTP/2 and HTTP/3 now take their header limit from maxHeaderSize too (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.
  • forwardProxyBlockPrivateNetworks now 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 a 502 for HTTP); if you turn the setting on and forward to a Tailscale or other
    100.64.0.0/10 address on purpose, leave the setting off to keep those forwards working.
    • forwardProxyBlockPrivateNetworks now also applies to the MCP tools that send requests to a URL you give them. With the setting enabled, 8.0.0 let run_contract_test, run_resiliency_test and run_mcp_contract_test send 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.
    • forwardProxyBlockPrivateNetworks now 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 $ref in 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 default ProxySelector the first time it loads a spec with the setting on; the wrapper stays for the life of the
      JVM, and a ProxySelector your 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.
    • forwardProxyBlockPrivateNetworks now 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 the FAIL_FAST failure policy makes the response a 502 Bad Gateway (with BEST_EFFORT the response goes ahead), a refused
      load scenario request is counted as a failed request of kind blocked (one warning for the run). proxyRemoteHost is 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.
    • forwardProxyBlockPrivateNetworks now applies to every forwarded and proxied request. With the setting enabled, 8.0.0 checked only httpForward actions. 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 gets 502 Bad Gateway, a binary
      connection is closed, and a warning names the blocked address. A request sent through forwardHttpProxy also 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 gets 502 Bad Gateway and a warning instead of 404 Not Found, because that name resolves to a private address — see Upgrading from 8.0.0.
  • Configuration changes made through PUT /mockserver/configuration now 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 a PUT that reported
    success (including enabling control-plane authentication, or lowering maxLogEntries/maxEventLogSizeInBytes, or enabling redactSecretsInLog) left request handling on the old, insecure or unenforced values; settings given at startup, and servers started through ClientAndServer, the JUnit
    integrations or Spring, were not affected. A PUT that changes authentication or TLS settings is now also applied atomically: control-plane authentication/authorization and the server TLS settings (tlsMutualAuthenticationRequired and its certificate chain) switch from all-old to all-new with
    nothing in between — closing a sub-millisecond window where a PUT switching 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 rejected PUT (an invalid certificate path, a negative
    globalResponseDelayMillis) now answers 400 having 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 later PUT's. Outbound TLS connections now cache their TLS
    setup only under the exact settings it was built from, so a PUT changing forwardProxyTLSX509CertificatesTrustManagerType mid-build can no longer leave a stale trust-all setup cached under the old settings; turning http2Enabled off at runtime now also stops outbound TLS connections offering
    HTTP/2.
  • redactSecretsInLog now masks credentials everywhere the event log is shown or retrieved, including the console/retrieved curl rendering of a proxied or forwarded request, JSON log message/arguments/expectation fields, the separate cookie list, HAR and format=CURL exports, 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); redactSecretsInRecordedExpectations now 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/generateExpectation and 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 of redactSecretsInLog. The same unconditional redaction covers the new narration field on PUT /mockserver/explain/explainUnmatched (see Added): credential values are masked whether or not redactSecretsInLog is on. The
    explain_request, explainUnmatched and other MCP/REST diagnostic output, by contrast, is redacted the same way the event log is — i.e. only when redactSecretsInLog is enabled — except diagnose_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 (from PUT /mockserver/retrieve?type=LOGS&format=LOG_ENTRIES or 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; a not(...) 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 /explain alias, so a mock of your own on PUT /explain keeps 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 typed explain(correlationId) method returning a RequestExplanation.
    • 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_request takes the correlation ID of a request MockServer has received and returns the same answer as PUT /mockserver/explain. An unknown
      or evicted correlation ID comes back as a tool error. diagnose_config lists 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_EXPECTATIONS or max-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 each mockserver.* / 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_config always masks
      credentials, and explain_request masks credentials quoted from your traffic when redactSecretsInLog is 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, debugMismatch and explainUnmatched calls (explain, debug_mismatch and explain_unmatched in Python, Ruby and
      Rust; Explain, DebugMismatch and ExplainUnmatched in Go and .NET, which also has Async versions), joining the Java client's typed explain. explain takes 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; debugMismatch compares a request you describe with every active
      expectation, field by field; explainUnmatched lists 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": true to PUT /mockserver/explainUnmatched to 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/lint checks your active expectations, or the expectations you send in the body without storing them, and mockserver lint expectations.json checks files with no server running. It reports an
      expectation that another one evaluated earlier always matches first (shadowed), times, timeToLive or percentage of 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 with PUT /mockserver/expectation now runs the quick per-expectation checks too: the expectations are still stored, and any problems are counted in an x-mockserver-lint-warnings response header and logged as a WARN. Expectations loaded from an initializationJsonPath file 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 most maxIssues issues, default 100), and test_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_template runs the
      template code it is sent, so, like its REST endpoint, it needs the mutate role. Output is redacted as the event log is when redactSecretsInLog is 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, with templateFailureAsServerError=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 each scenario.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/test alias, 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/explain and PUT /mockserver/explainUnmatched can add a plain-language summary from your own LLM. When the new llmExplainNarrationEnabled property is on (default false), a runtime LLM backend is configured (llmProvider, or a provider API-key environment variable)
      and the caller adds ?narrate=true, the response gains a narration field, 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 not redactSecretsInLog is on. The call runs off the request thread and is bounded by llmRequestTimeoutMillis plus two seconds. When narration is turned off, no backend
      is configured, the LLM fails or times out, or two summaries are already running, narration says 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_expectation and why_forward_failed tell 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_expectation changes an expectation only after you agree, and replaces it in place, starting from the fix MockServer suggests with suggest_expectation_fix; why_forward_failed starts from the reason explain_forward_failure reports, falling back to comparing
      fields or reading the logs only when MockServer has no fix, cannot name the cause or recorded no failed forward.
  • MySQL, Redis, PostgreSQL and length-prefixed binary protocols can now be framed and matched as whole messages, and binary proxying gained several capabilities. binaryMessageFraming (default RAW, unchanged) now also takes MYSQL, REDIS, POSTGRESQL and LENGTH_PREFIX (the last configured
    via binaryMessageLengthPrefixBytes (1, 2, 4 or 8, default 4), binaryMessageLengthPrefixByteOrder (default BIG_ENDIAN), binaryMessageLengthPrefixOffset (bytes before the field, default 0) and binaryMessageLengthIncludesPrefix (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 with POSTGRESQL, a message may be at most maxRequestBodySize bytes, 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 set forwardBinaryServerFirstWaitMillis on 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 an SSLRequest.
    • 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 upstream field (Java: binaryResponse(bytes).withUpstream(BinaryResponse.Upstream....)): ANSWER_ONLY (the default, unchanged)
      answers and does not forward; ANSWER_AND_FORWARD answers, forwards the
      message so the real server still sees it, and drops the server's reply; FORWARD_AND_REPLACE forwards the message and writes the binary response in place of the server's reply once that reply has ended (with a delay, the server's later replies wait behind it, so the client gets them in
      order). Both need matchBinaryExpectationsWhenProxying (on by default) and binaryMessageFraming set to POSTGRESQL, which tells MockServer where a reply ends; without that framing the expectation answers as ANSWER_ONLY and 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 to 250) 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 binaryProxyListener can be told what the upstream server sends unprompted: the new onUpstreamMessage method 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; set false to 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 configuration setting you have changed now shows the built-in default it replaced. --print-config ends the setting's line with (default: ...), GET /mockserver/config and the MCP diagnose_config tool add a default field, and the dashboard's Server Info tab shows the default beside
    the value. A default worked out from the JVM heap size, such as maxExpectations, shows the value worked out now. A credential's default is always shown as ***REDACTED***, and default is left out when MockServer cannot work it out.
    • --print-config, GET /mockserver/config and the dashboard's Server Info tab also now list the configuration MockServer ignored: after the properties in effect they list each unrecognised mockserver.* / MOCKSERVER_* key with its suggested correction (name only, never the value it was set
      to), and GET /mockserver/config?nonDefault=true lists 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_bytes reports live memory held by the store; mock_server_max_expectations_bytes the byte budget in force (0 when maxExpectationsSizeInBytes is off); mock_server_expectations_byte_evicted_total counts byte-driven
      evictions. mock_server_accept_queue_backlog_configured reports the configured accept-queue depth; on Linux, mock_server_accept_queue_backlog_effective reports 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) and mock_server_request_transport_duration_seconds (from reading a request's headers to the last byte of its response being
      written, including delays — unlike the existing mock_server_request_duration_seconds, which stops when a response is handed off for writing).
    • mock_server_inbound_connections_open, mock_server_inbound_connections_rejected_total and mock_server_inbound_connections_idle_closed_total (see maxInboundConnections below).
    • mock_server_scheduler_queued_tasks and mock_server_template_action_queued_tasks show 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 existing maxExpectations count.
    maxPendingDelayedResponses and maxQueuedTemplateActions (both sized from heap, capped at 100,000, or 1,000 when the JVM reports no heap limit; 0 restores the old unbounded behaviour) bound requests waiting on a delay or a template thread, answering 503 Service Unavailable with Retry-After: 1 past 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 status 1013 (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 a 502 instead 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.somaxconn on
    Linux, kern.ipc.somaxconn on macOS).
  • New Linux-only standalone jars and an HTTP/3 Docker image variant. -jar-with-dependencies-linux-x86_64.jar and -jar-with-dependencies-linux-aarch_64.jar are ~13 MB smaller by carrying only their own architecture's natives. mockserver/mockserver:<version>-http3 (also
    mockserver-<version>-http3, latest-http3, and snapshot-http3/mockserver-snapshot-http3 for master builds; Helm image.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 a status: 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; 0 limits by count only).
  • A startup warning for each insecure default a future major release will change (forwardProxyBlockPrivateNetworks to become true, localBoundIP to become 127.0.0.1, attemptToProxyIfNoMatchingExpectation to become false, and the HTTP parsing limits
    maxInitialLineLength/maxHeaderSize/maxChunkSize to 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. Misspelt mockserver.*/MOCKSERVER_* properties now get a "did you mean" suggestion at startup, in the
    form you used (MOCKSERVER_METRICS_ENABLE suggests MOCKSERVER_METRICS_ENABLED, mockserver.max_expectations suggests mockserver.maxExpectations), naming the property only, never the value (see the --print-config/GET /mockserver/config sub-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; set detailedVerificationFailures=false to 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
    adds If/Then/Else to HttpRequest with ConditionalRequest(...); .NET adds If/Then/Else with HttpRequest.RequestIf(...); Rust and PHP add HttpRequest::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 a checks list; Go, .NET and Rust also gain a typed CHECK_FAILURE_RATE
    threshold metric, and Rust's LoadStep gains name(...)/label(key, value) and LoadScenario gains label(key, value) (see the "Every client can now ask why a request matched" sub-bullet above for the explain/debugMismatch/explainUnmatched parity).
  • 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.html showing
    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: also ClientAndServer.getDnsPort() (including on the ClientAndServer the JUnit 5 extension injects), MockServerRule.getDnsPort(), the Spring listener's
    ${mockServerDnsPort} placeholder and @MockServerDnsPort annotation, and the dnsPort field of PUT /mockserver/status's answer (each gives -1, or the field is left out, while DNS mocking is off). @MockServerTest now also accepts mockserver.dnsEnabled and mockserver.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 forwardBinaryRequestsWithoutWaitingForResponse with 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 after maxFutureTimeout (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's SSLRequest, as with sslmode=require) now has its upstream connection upgraded to TLS on the same connection, so such a database can be proxied, presenting
    forwardProxyPrivateKey/forwardProxyCertificateChain if the upstream asks for a client certificate (a handshake that fails or exceeds socketConnectionTimeoutInMillis closes both connections with a warning) — SCRAM channel binding needs channelBinding=disable (JDBC) /
    channel_binding=disable (libpq/psql) in the client, or MockServer given the server's own certificate and key (privateKeyPath, x509CertificatePath, and certificateAuthorityCertificate set to the authority that signed it) since a client certificate cannot be passed through, and PostgreSQL
    17's sslnegotiation=direct is 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).
    binaryProxyListener is 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 or null; 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 through forwardSocksProxy, and CONNECT through forwardHttpsProxy or, when it is the only one set, forwardHttpProxy —
    sending forwardProxyAuthenticationUsername/forwardProxyAuthenticationPassword as Proxy-Authorization when set. 8.0.0 sent a clear connection's bytes to forwardHttpProxy as if it were the server, and sent a TLS connection with only forwardHttpProxy, or a clear one with
    forwardHttpsProxy, around the proxy; a connection whose only upstream proxy is forwardHttpProxy is no longer forwarded one message per connection — it now tunnels too. Many proxies refuse CONNECT to ports other than 443; if yours does, list the server in noProxyHosts. 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 (for 407, whether credentials were sent). forwardProxyTLSX509CertificatesTrustManagerType still defaults to ANY (trusts any upstream certificate)
    for this upstream TLS; set it to JVM/CUSTOM to verify. forwardBinaryRequestsWithoutWaitingForResponse is 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, the forwardBinaryRequestsWithoutWaitingForResponse methods of ConfigurationProperties and
    Configuration are removed, as are both NettyHttpClient.sendRequest(BinaryMessage, ...) overloads (the relay connects through NettyHttpClient.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 (forwardHttpProxy included), 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": false in PUT /mockserver/configuration).
  • BREAKING: a request now writes at most 50 "request didn't match expectation" log entries by default. maxLoggedMatchFailures now defaults to 50 instead 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 example 10 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
    entries retrieveLogMessages, the LOG_ENTRIES output 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 set maxLoggedMatchFailures=-1 (-Dmockserver.maxLoggedMatchFailures=-1 or MOCKSERVER_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, as PUT /mockserver/expectation does. Loading an initializationJsonPath file, at start-up or on a --watch reload, kept the first expectation for each id in a file and ignored later ones, so a
    file that repeats an id now serves a different response. The kept expectation is matched in the place of the first one, as when the same expectations are sent to PUT /mockserver/expectation, and a file loaded later still replaces an earlier file's expectation with the same id.
    mockserver lint and the warning logged when a file loads now report each replaced expectation as DUPLICATE_ID and check the kept one for shadowing. Migrate: to keep the old behaviour, remove the repeated ids 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-http3 classifier (default jar: 104 MB → 93 MB); the QUIC classes are still bundled, so
    setting http3Port without 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 same http3Port, 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-dependencies cannot load the native at all (its relocated Netty looks for a differently-named library), and mounting netty-codec-native-quic-<version>-linux-<arch>.jar into /libs in the published Docker images does not work, for the same reason — use the -http3 image tag
    instead. Maven or Gradle users of org.mock-server:mockserver-netty already get io.netty:netty-codec-native-quic for 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 same http3Port — two embedded servers in one JVM, test forks running in parallel that share one mockserver.http3Port, or several instances on one host started from
    the same properties file; give each its own http3Port, 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 a RuntimeException with 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 unavailable dnsPort, a port below 1024 the process isn't allowed to bind — fixed by
    dnsPort=5053 published as -p 53:5053/udp, running as root, or lowering net.ipv4.ip_unprivileged_port_start (NET_BIND_SERVICE does not help a non-root container user) — or more than one MockServer sharing a dnsPort). MockServer.getDnsPort() returns the OS-chosen port when dnsPort is
    left at its default of 0; the configuration page's example port is now 5053 (its old example, 5353, is the mDNS port and is normally busy on macOS). On macOS this now also includes a dnsPort that another application holds for IPv4 only (on 0.0.0.0): the operating system would let
    MockServer share it while sending the queries for 127.0.0.1 to that application, so MockServer refuses it, naming the conflict and the lsof command 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 a RuntimeException with 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/llm now describe it as a plain object of named exports, not a default export, matching what the JavaScript has always done. A default import without esModuleInterop/allowSyntheticDefaultImports no longer compiles (use import llm = require(...), import * as llm from ..., import { llm } from 'mockserver-client', or turn esModuleInterop on); reading the module's .default property 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 of mockserver-client/llm still compiles but reaches less — members added to a builder interface or to Llm are no longer visible on the factories/builders or on the object imported from that
    path; augment mockserver-client or the new mockserver-client/llmTypes instead (builder types now live there). Named imports, import * as, and require in type-checked JavaScript all now type-check; TurnBuilder is now declared alongside the other builder constructors. See Upgrading from
    8.0.0
    .
  • BREAKING: LOG_ENTRIES output writes each request/response body once, instead of up to three times over. A log entry's own httpRequest/httpResponse still carries the full body; its message and arguments, and the expectation recorded 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). The expectation recorded for a
    proxied exchange keeps its id, method, path, headers and status code — only its bodies move to the entry's own httpRequest/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 every LOG_ENTRIES retrieve (type=LOGS, REQUESTS, REQUEST_RESPONSES, RECORDED_EXPECTATIONS) and the MCP retrieve_logs/raw_retrieve tools; the Java client's retrieveLogEntries().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 500 with a generic message and correlation id, not a 400 carrying the exception's bare message (or, for an Error such as a stack overflow, no response at all). Requests MockServer genuinely
    rejects (bad JSON, invalid values, unsupported operations) are still 400 with 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 as 400 (PUT /mockserver/import, chaos, load-scenario,
    cassette, breakpoint, contract-test, AsyncAPI and configuration endpoints among them, plus GET /mockserver/llm/optimisationReport/PUT /mockserver/llm/diffRuns, where diffRuns now answers unreadable JSON with 400 rather than 500, and both now answer 500 rather 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 a CONNECT tunnel or checking
    data-plane credentials. Client-side: the Java client raises 500s as ClientException, and retrieveLogsByCorrelationId/retrieveActiveExpectations(requestDefinition, format) now raise for any status ≥400 instead of returning the error's text as their result (a 400 is still
    IllegalArgumentException); the PHP client raises MockServerException (was InvalidRequestException); the Rust client raises Error::UnexpectedStatus (was Error::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 500 with an x-mockserver-template-error: true header and the template error as a
    plain-text body, because templateFailureAsServerError is now on by default. In 8.0.0 such a template answered 404, the same as a request that matched no expectation (a forward template whose output was not a valid request answered 502). The response template of a forward with overrides is
    covered too: a failure there, or output that is not valid, answers the 500 instead of a 502 or the unchanged upstream response. The template error is still logged as before. Migrate: to keep the old behaviour set templateFailureAsServerError=false
    (-Dmockserver.templateFailureAsServerError=false or MOCKSERVER_TEMPLATE_FAILURE_AS_SERVER_ERROR=false).
  • BREAKING: the Node client and launcher now have an exports map. An ES module can now import a deep path without its .js extension (mockserver-client/llm), and TypeScript finds the typings under node16/nodenext/bundler resolution, but a path the map doesn't name (a .d.ts file,
    anything under test/, the launcher's tasks/mockServer.js) now fails with ERR_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-only llmTypes and mockServer for import type; launcher: index, downloadJar, downloadBinary) and package.json. Resolve <package>/package.json and join a file's path to that directory if you need one by location. The mockserver CLI command and
    grunt.loadNpmTasks('mockserver-node') are unaffected.
  • BREAKING: the Rust client's enums are now #[non_exhaustive], and server-sent enums gain an Unknown variant. A match listing 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 new responseMode on a retrieved expectation) now reads as Unknown instead of failing the whole response. Unknown cannot be sent back: serialising it, or sending an expectation/load scenario that holds it, returns an error and nothing reaches the server — use
    upsert_raw to resubmit server JSON unchanged. LoadThresholdMetric also gains a CheckFailureRate variant, 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) and verifyZeroInteractions() 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 logged VERIFICATION_FAILED entry'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 statusCode outside 100–999 is now rejected, with a 400 naming the field from PUT /mockserver/expectation (and every client, in a response, response sequence, fallback response or SSE response). In an initializationJsonPath file, or expectations restored from a cloud
    blob store via persistedExpectationsPath, an invalid entry is instead skipped with a WARN naming it (position, id, path) while the rest of the file loads (failOnInitializationError=true still fails startup), and when that file is also the save target it's backed up to
    <file>.invalid-entries-<timestamp>.bak before 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-range statusCode now logs an error and answers 500 by default
    (templateFailureAsServerError; see the BREAKING template-failure entry above) or 404 with templateFailureAsServerError=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's verifyAsyncMessage) now answers 400 for fields it doesn't understand, instead of silently checking "at least 1 message". Migrate: put atLeast/atMost/exactly inside count, e.g.
    {"channel": "orders", "count": {"atMost": 0}}; the other valid top-level fields are channel, payloadSubstring, payloadJsonPath and expectedValue.
  • BREAKING: response verification now applies every body matcher, in both PUT /mockserver/verify and PUT /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 new response().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. verifySequence was unaffected.
  • BREAKING: GenAI trace spans for Gemini and legacy/prompt-string text completions now carry the correct OpenTelemetry operation names, generate_content and text_completion, instead of chat — matching the OpenTelemetry GenAI semantic conventions, with span names
    generate_content <model> and text_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 still chat. 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 still chat. If a query/alert matches gen_ai.operation.name == "chat" or a chat span-name prefix, change it to match chat, generate_content and text_completion together; token/cost metrics are
    unaffected, since they're labelled by provider/model, not operation. Separately, embeddings and rerank spans are now named embeddings <model>/rerank <model> instead of chat <model> — update any query/alert selecting these by span name or operation too.
  • BREAKING: the Node client's retrieveRecordedRequests/retrieveRecordedRequestsById are now typed Promise<RequestDefinition[]>, not Promise<HttpResponse[]>, because a DNS or binary filter can return non-HTTP results. Narrow each result to HttpRequest (e.g. if ('path' in request))
    before reading HTTP-specific fields.
  • BREAKING: with control-plane mTLS (controlPlaneTLSMutualAuthenticationRequired), the Java client now trusts only controlPlaneTLSMutualAuthenticationCAChain for MockServer's certificate, instead of also trusting MockServer's own CA (certificateAuthorityCertificate) — including the
    MockServerClient behind 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 answers 400 to a PUT /mockserver/configuration that would lead to it — set your own CA with certificateAuthorityCertificate/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 /v9 module 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
    /v7 and the Testcontainers module's had no suffix, so go get could not install either at 8.0.0. From 9.0.0 the paths are github.com/mock-server/mockserver-monorepo/mockserver-client-go/v9 and github.com/mock-server/mockserver-monorepo/mockserver-testcontainers/go/v9, and each major release
    now moves them on automatically. Migrate: run go get github.com/mock-server/mockserver-monorepo/mockserver-client-go/v9 (and go get github.com/mock-server/mockserver-monorepo/mockserver-testcontainers/go/v9 if you use it), then change your imports from .../mockserver-client-go/v7 and
    .../mockserver-testcontainers/go to the /v9 paths.
  • 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-problems header,
    ?includeLoadProblems=true (response becomes {"expectations": [...], "loadProblems": [...]}), the create_expectation_from_openapi MCP tool's loadProblems, 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_expectation tool 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 inside chaos, a non-object
    responseHeaders, and a non-string method/path are rejected the same way, while keys starting with _ (such as a client's _meta) are let through. raw_expectation now 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 deterministicFromInput now 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 a 400 in 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 with llmEmbeddingMaxInputCharacters.
    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, and br when Brotli4j is on the classpath) instead of always sending gzip,deflate; a client sending none now has none sent on its behalf, and one
    whose choices MockServer can't decompress gets identity; the Java client likewise no longer adds Accept-Encoding: gzip,deflate to 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 without redactSecretsInLog — 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.
  • maxHeaderSize now 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 ignored maxHeaderSize and 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. Set maxHeaderSize=8192 to keep the old
    HTTP/2/HTTP/3 limit (this also then limits HTTP/1.1 to 8 KB), or 2147483647 to 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 answered 431 over 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 with H3_EXCESSIVE_LOAD and no 431 is sent; each refusal is logged once at WARN. Trailers, and a request sent through a CONNECT/SOCKS tunnel, are limited the same way. A forwarded/proxied response over the limit now gets a clean 502 instead 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 at WARN and below, a twelfth at INFO+; set to 0 to 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); a verify with 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 new mock_server_pending_delayed_tasks/mock_server_websocket_read_pauses_total metrics); this now also covers a
      response or action delay, globalResponseDelayMillis and chaos latency.
    • A client that stops reading a response now has its response end incomplete after 1 minute of nothing being read (responseWriteStallTimeoutMillis, set 0 to 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 at WARN and counted by mock_server_response_write_stalls_total (labelled by protocol and scope); 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 to 0 to 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. An error() 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.maxDirectMemory yourself 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 with 502, and the log says direct 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/bandwidthBytesPerSec now 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 stopDrainMillis behind an open SSE/streamed/WebSocket/raw-error() connection (see Fixed).
  • mock_server_evicted_log_entries_total now 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_total now carries a reason label (ring_full,
    in_flight_bytes); scripts reading an unlabelled line, or combining it with another metric in PromQL, need a sum() 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 new mock_server_dropped_drift_analyses_total.
  • maxRequestBodySize/maxResponseBodySize now have a minimum of 1 byte — 0 or 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, which mockserver-netty-no-dependencies uses 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 (-aot keeps G1 for its cold-start profile; override with JAVA_TOOL_OPTIONS=-XX:+UseG1GC — note that setting JAVA_TOOL_OPTIONS yourself for any reason replaces the default entirely, so include -XX:+UseZGC unless 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 set app.jvmOptions; the -webhook test-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 -clustered to Java 21 (Infinispan 14 needs an API removed in Java 24) — the standard/local/-aot images 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 — default maxLogEntries is 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 -Xmx explicitly via JAVA_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/mockserver and -webhook, with the same tags and digests as Docker Hub (except the experimental -aot tags and snapshots) — a
      way round Docker Hub's anonymous pull limit. The website's ECR Public example used public.ecr.aws/mockserver/mockserver, which doesn't exist; it now uses public.ecr.aws/t2x9c0i6/mockserver.
    • Images download less: the standard and -aot images ~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 the ENTRYPOINT-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
      default maxLogEntries cap (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) and app.preStopSleepSeconds (default 0, needs Kubernetes 1.30+) for graceful draining.
  • 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-Type parsing,
    HTTP/2 per-stream mapper/address derivation, header ingest, the respondBeforeBody/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/defaultResponseHeaders only when configuration changes rather than on every request, and per-handshake TLS certificate/SAN bookkeeping (change Subject Alternative Name lists only
    through the Configuration methods — mutating the set returned by sslSubjectAlternativeNameDomains()/-Ips() is no longer seen).
    • About a fifth less CPU per request at the WARN and ERROR log 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 of maxEventLogSizeInBytes, 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 default INFO level 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 whose Content-Type declared 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 the Proxy-Authorization value.
    • NottableString.withStyle(...)/withSchemaType(...) and Parameter.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.
  • Dashboard dependency mermaid upgraded 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=1000 instead of deriving from heap), cutting memory across many in-JVM instances; a suite relying on deeper log history should raise the caps explicitly or set mockserver.devMode=false. Only the JUnit 4
    rule and JUnit 5 extension are affected (dev mode is JVM-global, so a ClientAndServer constructed 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 502 and 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. raise ringBufferSize vs. raise maxEventLogSizeInBytes/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 not redactSecretsInLog
    is on. A retrieveLogMessages / retrieve?type=LOGS call filtered by request now also returns these entries, and the request appears in their LOG_ENTRIES JSON and the dashboard's request pane. A request with a missing Host header is now logged, and returned as the body of the 400
    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-hint header (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": true with expectationsChecked).
    • A flood of unmatched requests at INFO no longer compares each one with every expectation. At the default INFO log 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 with only 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 the WARN level, are unchanged.
    • maxLoggedMatchFailures lets you cap how many per-expectation "didn't match" log entries are written per request (default 50, a BREAKING change from unlimited logging — see the entry above; -1 logs 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 at maxLogEntries, suggest it as the way to keep INFO,
      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.
  • MockServer's MCP server now rejects a prompts/get call missing a required argument, instead of returning the prompt with the placeholder (e.g. {{path}}) left in its text — JSON-RPC error -32602 naming the missing argument(s) in data.missingArguments; a null argument 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.candidateIndexThreshold and mockserver.jsonSchemaAllowRemoteRefs no 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 -D instead.
  • Dependency clashes and classloading fixed in the -no-dependencies artifacts — user-visible if you use them, not internal-only. mockserver-netty-no-dependencies no 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 (set slf4j.provider yourself, 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-dependencies jars no longer ship GraphQL Java, picocli, Chicory, the RabbitMQ and Eclipse Paho clients, HdrHistogram,
    jopt-simple, the JSR-305 and javax.validation annotations, 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 under shaded_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-dependencies integrations now use your project's JUnit or Spring instead of bundling
    their own copy — mockserver-junit-rule-no-dependencies and mockserver-integration-testing-no-dependencies no longer contain JUnit 4, mockserver-junit-jupiter-no-dependencies no longer contains JUnit Jupiter and the JUnit Platform, and mockserver-spring-test-listener-no-dependencies no
    longer contains Spring and Micrometer; each POM now declares that framework as a provided dependency. This is not a breaking change for a project that uses these integrations, since it already has the framework, but it now needs spring-test and spring-context for 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": null as the real API does (Bedrock InvokeModel responses gain container too, but not diagnostics, since Bedrock does not support cache diagnostics); both now also send stop_details and a complete usage object (cache, service tier, thinking tokens — inference_geo
    for ANTHROPIC only, not Bedrock InvokeModel) matching the real APIs, and a non-Anthropic stopReason (stop, length, tool_calls) sent to Anthropic/Bedrock clients is now converted to Anthropic's own values (end_turn,
    max_tokens, tool_use, or refusal for a content filter) so one expectation can be shared across providers; mocked Bedrock InvokeModel responses now send the X-Amzn-Bedrock-*-Token-Count headers boto3-based clients (LangChain's ChatBedrock) 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/completions endpoint, 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": null on every choice, as
    their real APIs do (MISTRAL, XAI and ORCAROUTER responses still leave it out, matching their published response examples); DeepSeek also gains system_fingerprint and usage.prompt_tokens_details; DeepSeek and xAI return reasoning text as reasoning_content, Groq and OpenRouter as
    reasoning; DeepSeek adds prompt_cache_hit_tokens/prompt_cache_miss_tokens, xAI reports reasoning tokens outside completion_tokens, Groq adds its timing fields and x_groq, and OpenRouter adds cost/is_byok; all report streamed usage correctly (including DeepSeek's "no separate usage
    chunk" behaviour, and Mistral/Groq/OpenRouter streams now carrying token usage without stream_options.include_usage, as those APIs do); an OPENROUTER mock now reports native_finish_reason on every choice (the upstream model's own reason, alongside the
    normalised one) and no longer passes a Gemini/Claude stopReason through unchanged as finish_reason (in a stream the field is null until 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 empty choices
    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, before data: [DONE], with one extra chunk whose choices is [] and whose usage holds prompt_tokens,
      completion_tokens, total_tokens and, when set, prompt_tokens_details.cached_tokens and completion_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), LangChain stream_usage, LiteLLM and Langfuse saw zero tokens. This applies to OPENAI, AZURE_OPENAI and the OpenAI-compatible providers XAI, GROQ and ORCAROUTER. A request that does not ask gets no usage chunk, as from the real OpenAI and Azure
      APIs, and is unchanged; XAI and ORCAROUTER follow the same opt-in. MISTRAL and DEEPSEEK never send the extra chunk: they always put usage on the chunk that carries the finish_reason, and DEEPSEEK adds "usage": null to every other chunk only when asked. GROQ also always puts
      the usage in x_groq.usage on that chunk.
    • A proxied LLM call whose response is streamed is now counted, so llmCostBudgetUsd applies 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.completed for the OpenAI Responses API, message_start and message_delta for Anthropic, usageMetadata for Gemini, the metadata event 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 the done line 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, at WARN when 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 sets stream_options.include_usage to
      true, so set it for those calls to count towards the budget. A stream cut off after Anthropic's message_start is 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, full output on response.completed); whenContainsToolResultFor now matches a tool result by call_id (a chained turn must echo MockServer's own call_id),
      and with streamingPhysics set a streamed tool call is paced like text instead of arriving as one piece.
    • Ollama mocks now answer /api/generate in its own shape (not /api/chat's), report done_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
      with promptFeedback.blockReason (a real blocked prompt has no candidates); OpenAI-family providers keep their pre-stream 400.
    • Mocked Gemini candidatesTokenCount/totalTokenCount no 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-zero usage.total_tokens estimate.
    • A tool call's configured id is now the id the client actually receives across OpenAI-family, Anthropic, Bedrock, Gemini and Ollama providers (previously generated or absent).
  • 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-shaped 400 (and an expectation's dimensions
    must now be at least 1, rejected at creation rather than silently producing empty or erroring vectors). Rerank mocks now honour top_n/top_k and match Cohere's/Voyage's real response shape (id, meta, echoed model, return_documents); Cohere's current /v2/rerank path is now recognised
    alongside /v1/rerank, and a top_n/top_k that is not a positive whole number now gets a 400 in the provider's own error format (a Voyage request with more than 1,000 documents gets a 400 with Voyage's {"detail": ...} body). Mocked and forwarded embeddings/rerank calls across
    OpenAI, Cohere (including a /v2/chat stream), 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.