[7.4.0] - 2026-07-04
Added
-
Dashboard: every language tab now generates typed client code. The composer's Java, Node.js, Python, Go,
C#, Ruby, and Rust tabs construct each client's typed model — fluent builders and typed constructors matching
the website examples — instead of embedding raw JSON, including full LLM response actions in Java. Every
language's generated output is proven equivalent by executing or compiling it against the real client and
comparing the serialized expectation with the registered JSON, and a CI gate compiles the generated Java
against the built client on every build. -
Every client library now round-trips the full expectation model, proven by a shared fidelity harness. The
Go, Rust, C#, Python, Ruby, and Node clients gained typed support for every expectation feature they previously
dropped silently — chaos profiles, rate limits, forward-with-fallback and forward-validate actions, gRPC bidi
responses, before/after actions, steps, capture rules, namespaces, LLM response payloads including moderation,
rerank, and content filters, WebSocket frame matchers, response trailers, DNS matchers, and all body matcher
variants. Forty-four server-validated kitchen-sink fixtures now run as round-trip tests inside each client's own
test suite, with a ratcheting known-gaps ledger that fails CI if a documented gap silently regresses or a fixed
gap is still excused. -
Dashboard: JWT and all-of body matchers are now authorable in the composer. The Advanced request form gains a
JWT section (claims, issuer, audience, algorithm) and an all-of body matcher composing multiple sub-matchers,
emitted in the exact server wire format and round-tripping on edit. -
Dashboard: the Java code tab is now complete and type-safe. Generated Java uses the real client API for
priority, times, and time-to-live, scenario bindings, namespaces, and capture rules — proven by compiling a
kitchen-sink snippet against the built client — and the Java client gains matching withNamespace and withCapture
fluent methods. Actions the Java builder preview cannot represent show an honest notice instead of fabricated code. -
Dashboard: every captured flow is now a launchpad. A "Create From This" menu on traffic detail panes and
log rows fans out into every subsystem pre-filled from that flow — create a mock in the composer, set a
breakpoint, prefill a verification, or add chaos for the host. The traffic inspector also gains structured
Request/Response tabs (headers tables and pretty-printed bodies, with the raw JSON tree kept as a tab),
"Copy as curl" with shell-safe quoting and masked credentials, a Charles-style "Repeat" action (iterations,
bounded concurrency, delay, live progress and cancel), a Proxyman-style diff pool with an editable ignored-headers
list, an unmatched-count chip with why-didn't-this-match and generate-stub actions, and bulk
"Promote to Mocks" over recorded traffic (PUT /mockserver/recordings/promote). -
Dashboard: previously server-only capabilities are now reachable from the UI. Validate recorded traffic
against an OpenAPI spec (/trafficValidate), import GraphQL SDL schemas and mock SCIM providers, import Pact
contracts, generate HTTP expectations from AsyncAPI specs, dry-run WASM modules against a sample request,
reload persisted recording archives (NDJSON or from server disk), a preemption-simulation card and experiment
history in Service Chaos, a control-plane Audit view, a standalone Scenarios view in the navigation, and a
read-only Server Info tab (effective configuration with source tiers, bound ports plus bind-additional-port,
proxy setup with CA download) alongside a server-side decoded-prompt LLM run diff. -
Dashboard: quick chaos and honest latency attribution. A one-toggle Quick Chaos strip (percentage slider
over real per-request fault probabilities, per upstream host) makes fault injection approachable, and the
traffic timing waterfall now distinguishes latency MockServer injected (chaos latency, configured response
delays including the global delay, breakpoint holds) from real upstream/processing time — mock-served
responses carry a timing block for the first time when they inject latency (a plain mock's recorded
output is unchanged). -
Force a response-sequence variant per request. A request matching an expectation with multiple responses
can force which variant it receives via a 0-basedx-mockserver-response-indexheader. Forced requests
consumetimesbut never advance the sequential/switch rotation for other callers; the header is retained
in recordings and filtered from every outbound forward, including WebSocket passthrough. Invalid values are
ignored. -
Dashboard: faster first load, first-run onboarding, and one-click navigation. All views load lazily
(initial bundle down from 259 kB to about 164 kB gzip), a Try It Now path on the get-started view creates a
first mock, returning users get an open-dashboard shortcut, keyboard shortcuts move off
browser-conflicting bindings with a ? help overlay, and matcher testing is available at the point of need
in the composer and on every expectation row. The scenarios view details each scenario's states, bound
mocks, and transitions with edit-in-composer actions, and the advanced composer models scenario bindings
directly. The audit view explains the opt-in audit trail (and the demo enables it).
Fixed
-
Dashboard: editing an LLM or otherwise exotic expectation now generates complete typed client code in
every language. Fields the composer form cannot model — LLM responses with their full completion detail,
response sequences, object callbacks, forward-validate, gRPC bidi, rate limits, and cross-protocol
scenarios — were silently dropped from generated Python and Ruby code and embedded as raw JSON in C# and
Rust; all languages now construct the client's typed model, guarded by a universal test asserting no wire
field is ever absent from generated code. -
Registering forward-validate, forward-with-fallback, or gRPC bidi expectations no longer fails with a
metrics error. The per-action counter constants for these action types were missing, and a guard test
now asserts every action type has one. -
Generated Go code now compiles. All Go code generators — the dashboard expectation, verification, and
load-scenario tabs, the retrieve-as-Go endpoint, and the website examples — emitted the client import without its
/v7 semantic-import-versioning suffix, so generated code never resolved against the published module. -
.NET client: reading an object-callback expectation back from the server no longer throws. The
responseCallback field was typed as a string while the server emits a boolean, losing the whole expectation. -
Python client: multipart body field matchers no longer vanish on registration. Multipart fields were emitted
in an array form the server misroutes; they now use the server's canonical object form. -
Dashboard: editing a mock no longer shows phantom changes. An untouched edit round-trips to a zero diff — the
explicit default forms of priority, times, and timeToLive are preserved instead of appearing as removals, and a
genuine reset shows the explicit unlimited form. -
Dashboard: the Promote to Mocks button no longer wraps in a narrow traffic pane.
-
Dashboard: editing an expectation no longer silently strips fields the form does not model. The composer
now merges its form output onto the original expectation JSON, preserving scenario bindings, namespaces,
response sequences, cross-protocol scenarios, and matcher fields such askeepAliveandsocketAddressin
both quick and advanced modes, with an alert listing the preserved fields. -
Dashboard reliability fixes. Paused breakpoint exchanges survive navigating away from the Breakpoints
view; stream-frame editing is UTF-8 safe and surfaces encode failures; a reconnect re-sends the active
request filter instead of silently streaming unfiltered data; an error-only push no longer blanks the
panels; user-action errors persist until dismissed (and the banner is dismissible); mismatch dialogs report
honest "differs on N field(s)" scores with remediation hints; and a response template rendering invalid
output is now surfaced as aTEMPLATE_GENERATION_FAILEDevent instead of a silent 404 logged as a success. -
Dashboard performance, measured. Idle pushes no longer re-render every panel each second, hidden tabs
buffer instead of processing WebSocket updates, and hot parse/search paths are cached — all verified by a
committed benchmark suite (npm run benchinmockserver-ui) comparing against the pre-optimization
implementations at small and large payload scales. -
Automated release publishing for the Ruby and PHP Testcontainers modules. The
mockserver-testcontainers/ruby
andmockserver-testcontainers/phpmodules now publish automatically as two newsoft_failrelease-pipeline
components.tc-ruby(scripts/release/components/tc-ruby.sh) self-bumps the gemversion.rb, builds inside the
pinned Ruby image andgem pushestestcontainers-mockserverto RubyGems (credential from the existing
mockserver-build/rubygemssecret; skips gracefully if absent).tc-php(scripts/release/components/tc-php.sh)
subtree-splits the module and pushesmaster+ a version tag to themock-server/mockserver-testcontainers-php
Packagist mirror repo, mirroring the PHP-client publish; it skips gracefully until that mirror repo is provisioned
(one-time setup — seemockserver-testcontainers/php/PUBLISHING.md). Both support--dry-run, are release-type
gated (full/post-maven), and have soft post-release verification checks. See
docs/operations/release-process.md. -
Go client:
jwt/allOfmatchers, a/v7module path, and a bundled Testcontainers client. The Go
client (mockserver-client-go) gains typedjwtrequest-matcher andallOfbody-matcher builders (matching
the other client libraries). Its module path now carries the required Semantic Import Versioning suffix —
github.com/mock-server/mockserver-monorepo/mockserver-client-go/v7— so it is properlygo get-able as a
dependency (a future v8 becomes/v8); update imports to add/v7. With that fixed, the Go Testcontainers
module now bundles the client and exposescontainer.Client(ctx)returning a ready-to-use client pointed at
the container's mapped host and port, instead of only documenting manual construction. -
Typed client-library support for the
jwtrequest matcher andallOfbody matcher. The two new
matchers are now first-class in the client libraries as well as the server: the OpenAPI spec, the generated
Node/TypeScript types (jwt,Jwt, and theALL_OF/bodyAllOfbody variant), and the Java model
(request().withJwt(jwt()...),withBody(allOf(...))) all expose them, and every other client library can
send them over the REST wire format. Consumer docs gain Java and Node examples for both matchers. -
Automated package-manager release channels for the CLI (Homebrew, Scoop, winget, Chocolatey, SDKMAN!, asdf/mise).
Six new release-pipeline components (scripts/release/components/{homebrew,scoop,winget,chocolatey,sdkman,asdf}.sh)
publish/update these channels automatically as asoft_failgroup after the binary-bundle step, distributing the same
self-contained jlink bundles (mockserver-<version>-<os>-<arch>.tar.gz/.zip, each carrying its own trimmed Java
runtime) that thebinarycomponent uploads to the GitHub Release. A new tap formula (mock-server/homebrew-tap,
brew install mock-server/tap/mockserver) is added — complementary to, and separate from, the JAR-based homebrew-core
formula (still bumped by BrewTestBot). Each channel renders its manifest from the real published bundle checksums,
supports--dry-run, and skips itself gracefully (never breaking the release) when its target repo or API secret is
not yet configured. Seepackaging/<channel>/release-component.mdanddocs/operations/release-process.md. -
WebSocket proxy passthrough + frame recording. MockServer can now proxy WebSocket connections through to a real
upstream server, not just mock them. When a WebSocket upgrade request (GET+Upgrade: websocket) arrives in proxy
mode and no WebSocket mock expectation matches — or it matches a plainFORWARDexpectation — MockServer completes
the upstream connection (honouringws/wssscheme and TLS), relays the101 Switching Protocolshandshake, and
relays frames bidirectionally (text, binary, ping, pong, close) until either side closes. This lets a system under
test pointed at MockServer as a proxy reach a real WebSocket backend. The relayed traffic is recorded: the upgrade
exchange is logged as aFORWARDED_REQUEST(request = the upgrade, response =101carrying a JSON transcript of the
relayed frames with direction/opcode/payload), soretrieveRecordedRequestsand the dashboard show the WebSocket
traffic. The transcript is flushed to the log once, on connection close. Frame recording is bounded per connection by
the newwebSocketProxyMaxRecordedFramesconfiguration property (default1000; set to0to disable frame
recording — the handshake is still recorded) plus an absolute 8MB transcript cap, following themaxLogEntries
memory-management philosophy. The upstream TLS leg uses the forward-proxy trust manager
(forwardProxyTLSX509CertificatesTrustManagerType, defaultANY) so realwssbackends are reachable, and the same
forwardProxyBlockPrivateNetworksSSRF guard the matched-forward path enforces applies (a WS upgrade to a
loopback / RFC1918 / cloud-metadata target is refused with502). A slow peer cannot exhaust memory — the relay
applies standard writability-based backpressure. An opt-inwebSocketProxyIdleTimeoutSeconds(default0, off)
reaps abandoned relays. For a WS upgrade matched by a plainFORWARDexpectation,Times/verifyapply but
response-shaping features (delay,rateLimit,chaos, breakpoints, drift) do not — use theWEBSOCKET_RESPONSE
mock action for frame-level control. Scope (v1): HTTP/1.1 upgrade relay (plain and TLS upstream); HTTP/2
extended-CONNECT WebSocket is not yet relayed. See
docs/code/netty-pipeline.md. -
Seedable template
fakerfor reproducible fixtures — newtemplateFakerSeedproperty. The template
fakersample-data helper (Velocity$faker, Mustache{{faker.*}}, JavaScriptfaker) can now be seeded
deterministically.templateFakerSeeddefaults to0, which leaves faker unseeded so it produces different,
random values on every render (behaviour unchanged); a non-zero value seeds a per-engineFakerso
faker-driven templates generate reproducible fixtures across runs — the template analogue of the OpenAPI
example generator's fixed-seed model. Determinism is strongest for sequential (single-threaded) generation.
Wired through the config trio (ConfigurationProperties/Configuration/ConfigurationDTO) and honoured
by all three template engines. See docs/code/configuration-reference.md. -
Per-host upstream mTLS — new
forwardProxyClientCertificatesByHostproperty. Outbound client
authentication (mTLS to the upstream) was global-only; it can now vary by upstream host. The property is a
comma-separated list ofhost=certificateChainPath;privateKeyPathentries — a matching upstream host
(case-insensitive) is sent that host's cert/key pair for client authentication, and any host without an entry
falls back to the globalforwardProxyPrivateKey/forwardProxyCertificateChainpair (default empty, so
behaviour is unchanged). Contexts are cached per mapped host while all unmapped hosts share one context, so a
forward proxy that sees many upstreams cannot grow the cache without bound. See
docs/code/tls-and-security.md. -
Testcontainers modules for Ruby and PHP, plus a wired client on every polyglot module. MockServer now
ships official Testcontainers modules for eight languages — Ruby (testcontainers-mockserver, RubyGems)
and PHP (mock-server/mockserver-testcontainers, Packagist) join the existing Java, .NET, Node.js, Python,
Rust and Go modules. Both new modules start themockserver/mockserverimage, wait for
PUT /mockserver/status→ 200, and expose connection helpers (endpoint/getEndpoint,secure_endpoint,
server_port,host).- A wired client on every module. Each polyglot container now returns a ready-wired MockServer client
pointed at the mapped host/port, mirroring the Java module'sgetClient():get_client(Python, Ruby),
getClient()(Node, .NET),getMockServerClient()(PHP — the inherited TestcontainersgetClient()is
reserved for the Docker client), andclient()/async_client()(Rust). The language's MockServer client
is now a dependency of its Testcontainers module. (Go instead documents constructing the client from
ctr.Host(ctx)/ctr.ServerPort(ctx)viamockserver.New(host, port):mockserver-client-gois
published at v7.x without a/v7module-path suffix, so bundling it would make the Go Testcontainers module
itself unresolvable for downstreamgo get.) - Version-matched default image, no more hard-pinned tags. Every module now derives its default image tag
(mockserver-<version>) from the MockServer client version — or, for Go, from the module's own version —
falling back to the mutablelatesttag when the version cannot be resolved, mirroring the Java module,
instead of a hard-coded tag that goes stale.
- A wired client on every module. Each polyglot container now returns a ready-wired MockServer client
-
WASM response shaping (host ABI v3). WASM custom-rule modules can now compute the response, not just
match the request. A module that exports an optionalshape_response(i32 ptr, i32 len) -> i64function is
invoked after a match with a JSON envelope{version:3, request:{…v2 request…}, response:{statusCode, headers, body}}describing the response the matched expectation would return; it returns a (possibly
partial) response JSON{statusCode?, headers?, body?}that MockServer applies — replacing the status,
merging headers, and replacing the body — enabling WASM-computed dynamic responses. Fully backward
compatible: modules without the export stay pure predicates, and a module can export both
match_requestandshape_responseto match first, then shape. Fail-safe: any trap, invalid JSON, or an
over-sized return (capped at 1 MiB) leaves the response unshaped and logs once per module — a broken
shaper never fails the request. Theexamples/wasm/sdk-rustauthoring SDK gainsShapeEnvelope,
ShapeResponseand aResponseBuilder, plusexport_shape_response!/
export_match_and_shape_response!macros, and a newexamples/wasm/rust-shape-responseexample module
(matchesPOST /shape, setsX-Shaped: true, and rewrites a JSON body field).POST /mockserver/wasm/test
accepts an optional candidateresponseand returns the shaped response so IDEs can preview shaping. See
docs/code/wasm-rules.mdand the WASM Custom Rules page. -
SCIM list endpoints now support sorting. The mock SCIM 2.0 provider's
GET {basePath}/Users|Groups
listing accepts the standardsortByandsortOrderquery parameters (RFC 7644).sortByis an
attribute name or nested dotted path (e.g.name.familyName);sortOrderisascending(default) or
descending. Comparison is case-insensitive and resources with no value for the sort attribute are
always ordered last. Sorting is applied after anyfilterand beforestartIndex/countpagination; a
malformedsortBypath or an invalidsortOrderreturns a400error envelope (matching how an invalid
filter is rejected), and theServiceProviderConfignow advertisessortas supported. -
More realistic LLM token estimates and subword streaming by default. The approximate
TokenCounter
behind inferredusagecounts and token-based quotas now approximates GPT-style subword (BPE)
segmentation instead of a plain characters÷4 blend, landing within roughly ±15% of a real tokenizer for
ordinary English prose (validated against GPT-4cl100k_basereference counts). Streaming LLM responses
now emit finer, subword-sized deltas by default (viastreamingPhysics.subwordStreaming), so a
streamed response streams closer to a real provider's per-token cadence out of the box. Behaviour
change: because a stream now carries more, smaller real-token deltas at the sametokensPerSecond, its
total duration is slightly longer than before, and the per-provider streaming wire output is finer-grained.
All streaming-physics timing semantics are preserved (each delta is still one physics event). To restore
the previous whole-word (whitespace-boundary) streaming, setstreamingPhysics.subwordStreamingto
falseexplicitly. -
Editor extensions can start MockServer without Docker. Both the VS Code extension and the JetBrains/IntelliJ
plugin gain a Start (binary, no Docker) command/action that launches MockServer from the self-contained binary
bundle (a jlink-trimmed Java runtime + the shaded jar + launcher — seescripts/build-binary-bundle.sh), so
corporate machines without a Docker daemon can run a local server straight from the editor. The bundle is taken
from a configured local path (mockserver.binaryPathin VS Code, Binary bundle path in JetBrains settings) —
either thebin/mockserverlauncher or the unpacked bundle directory — or, when unset, downloaded on demand
(after an explicit confirmation) for the current OS/architecture from the GitHub release matching the
extension/plugin version and cached (under the extension's global storage / IDE system cache). Checksum
verification against the published.sha256sidecar is fail-closed: a digest mismatch always aborts, and a
sidecar that cannot be fetched (common behind a TLS-inspection proxy, where the small sidecar fetch fails while
the large archive succeeds) aborts too unless the user explicitly confirms installing unverified. The launched
process is tracked for a matching Stop (binary) and terminated on editor/IDE shutdown so it never outlives
the editor holding the port: VS Code streams its output to the MockServer output channel and its download honours
the editor'shttp.proxy/ system-proxy settings; JetBrains runs it as a tracked background process registered
for IDE-shutdown cleanup. Docker-based Start (Docker) is unchanged. See the
IDE Extensions page. -
Match requests by the claims inside a JWT (
jwtrequest matcher). An expectation can now route on a
JSON Web Token carried in a request header —withJwt(jwt().withClaim("sub", "user-1").withClaim("scope", ".*admin.*"))matches only requests whose bearer token carries those claims (each claim value is an exact
string or a regex, with!negation supported). Convenience fieldsissuer(iss),audience(aud,
string or array) andalgorithm(JOSE headeralg) are provided, and the header name (default
authorization) and scheme prefix (defaultBearer) are configurable. The token'sheader.payloadis
decoded with base64url + JSON and its signature is deliberately not verified — this is request matching
for test routing, not authentication (the control-plane JWT auth stack is unchanged). A request with no such
header, or a malformed token, simply does not match (it never raises an error). Exposed onHttpRequestand
through the JSON wire format ("jwt": { "claims": { ... }, "issuer": "...", "audience": "...", "algorithm": "..." }) and JSON schema. (Java server + JSON wire format; typed client-library wrappers to follow.) -
Compose several body matchers that must all match (
ALL_OFbody /allOf). A request body can now be
matched against several body matchers at once, where every one must match the same body — for example
withBody(allOf(jsonPath("$.name"), jsonSchema(schema), regex(".*value.*"))). This reuses the existing body
matcher implementations without changing any of their semantics; each component keeps its ownnotflag and
the composite honours its ownnot(negate the whole conjunction) andoptionalflags. Serialised as
{"type":"ALL_OF","bodyAllOf":[ ... ]}and accepted wherever a body matcher is accepted. (Java server + JSON
wire format; typed client-library wrappers to follow.) -
Fluent
MockServerClient.builder(). The Java client gains a discoverable fluent builder that
covers every existing construction dimension in one place —host(defaultlocalhost),port
(default1080),contextPath,Configuration/ClientConfiguration,portFuture, plus TLS
(secure),proxyConfiguration, control plane JWT (controlPlaneJWT) andrequestOverridethat
previously required post-constructionwith...setters.MockServerClient.builder().host("localhost").port(1080).build()
is equivalent to the corresponding constructor call. The eight existing constructors remain fully
supported and are not deprecated; the builder simply delegates to them, so it introduces no
behaviour change. Misconfiguration stays loud (an emptyhostthrowsIllegalArgumentException, and
portFuture(...)cannot be combined withhost/port/contextPath). See
docs/code/client-and-integrations.mdand the
MockServer Clients page. -
Observability quick-win bundle — Grafana dashboard, Helm ServiceMonitor, and a durable audit file sink. Three
independent additions that make MockServer easier to monitor in production:- Standalone Grafana dashboard for the server metric family.
examples/grafana/mockserver-server.json(with a
README) is an importable dashboard charting request throughput and match outcomes (via the new_total
counters), request-latency percentiles, registered expectations/actions, per-upstream forward/proxy health,
dropped-log-events and chaos counters, and the JVM runtime gauges — every panel references a metric documented in
docs/code/metrics.md. It exposes adatasourcevariable so it imports against any Prometheus data source, and
is kept separate from the existing k6 load-injection dashboard. - Optional Prometheus Operator
ServiceMonitorin the Helm chart.serviceMonitor.enabled=true(disabled by
default) renders amonitoring.coreos.com/v1ServiceMonitor scraping/mockserver/metrics, with
namespace/interval/scrapeTimeout/path/scheme/honorLabels/labels/namespaceSelector/relabelings
values. User-suppliedlabelsare merged over the chart labels (user wins) so areleaselabel for the
PrometheusserviceMonitorSelectoroverrides cleanly. Requires the Prometheus Operator CRDs and
mockserver.metricsEnabled=true. - Durable NDJSON control-plane audit file sink. New
auditLogFileproperty (empty default = off): when set,
each recorded audit entry is also appended as one JSON object per line to the file, giving a restart- and
reset-surviving trail that outlives the bounded in-memory ring. Implemented as a separateAuditFileSink
writer that only observes the same entries — the in-memory ring is untouched (honouring the "never a sink"
contract). Path resolved once on first write, parent dirs created, append-only (rotation out of scope), and
fail-soft (a single WARN then self-disable on IO error, never crashing request handling).
- Standalone Grafana dashboard for the server metric family.
-
OpenAPI request validation now checks
style/explode-serialised array and object parameters. When you
validate traffic against an OpenAPI spec (contract/traffic validation, theverify_trafficMCP tool, or an
OpenAPI-backed expectation returning400on non-conforming requests),array/objectquery, path and header
parameters were previously skipped unless the value already looked like JSON — so a malformed list or object slipped
through. MockServer now decodes each parameter from itsstyle/explodeserialisation before schema validation:
queryform/spaceDelimited/pipeDelimited/deepObject, pathsimple/label/matrix, and headersimple,
for bothexplodevalues, with the OpenAPI defaults applied when the spec omits them. Decoding is type-aware
(each element/property is coerced to the JSON type its item/property schema declares) so a request that was valid
before stays valid — only values the spec genuinely rejects (e.g. a non-integer element in anitems: integer
array, or a non-integer property in adeepObject) now fail. It is fail-open: any value that cannot be soundly
reconstructed (a non-primitive item/property, or an unsupported style combination) skips the schema check exactly
as before, whilerequired-presence is still enforced. Note this is a (spec-conformant) behaviour change for
traffic validation: non-conforming style serialisations that previously slipped through unchecked can now return
400— e.g. a comma-delimited list sent where the spec declares the defaultform/explode: true(which expects
repeated parameters) is decoded as a single element and validated as such. One known edge: an empty value for a
non-explodeformarray decodes as a single empty-string element rather than an empty array. -
OpenAPI example generation honours
discriminator,readOnlyandwriteOnly. Generated examples (OpenAPI
import to expectations,run_contract_test/run_resiliency_test, load scenarios) are now more faithful: for a
oneOf/anyOfschema with adiscriminator, a concrete subschema is chosen and the discriminator property is set
to the matching mapping key (or the referenced schema name when no explicitmappingis given), instead of blindly
taking the first subschema; andreadOnlyproperties are omitted from request examples whilewriteOnly
properties are omitted from response examples, per the OpenAPI spec. Existing callers that do not specify a
direction are unchanged (noreadOnly/writeOnlyfiltering). -
gRPC forward proxy + record/replay — bring the record-then-mock workflow to gRPC. Until now gRPC
support was mock-only: MockServer could decode inbound gRPC to JSON and serve mocked responses, but could
not forward a gRPC call to a real upstream gRPC server. Now, when a gRPC request (HTTP/2 +
application/grpc) matches aFORWARD-class expectation — or arrives in proxy mode with no matching
expectation — MockServer re-encodes the decoded request back into gRPC-framed protobuf, relays it to the
upstream gRPC service, decodes the framed protobuf response back to JSON, and re-frames it for the calling
client. The forwarded exchange is recorded in the event log as aFORWARDED_REQUESTcarrying the decoded
gRPC method path, status, and (when a proto descriptor is registered) the decoded message JSON, so
retrieveRecordedExpectations(andpromote_recordings) produce a replayable gRPC mock — the same
record → snapshot → replay loop already available for HTTP/SSE. A non-OK terminalgrpc-status/grpc-message
delivered by a real upstream in HTTP/2 (or chunked HTTP/1.1) trailers is preserved through the relay and the
recording, rather than being defaulted toOK. Unary and client-streaming request bodies
(single JSON object / JSON array) and unary or server-streaming responses (one or more frames) are handled.
Decoding of the recorded exchange requires the proto descriptor to be loaded on the proxy (via
grpcDescriptorDirectory/grpcProtoDirectory/PUT /mockserver/grpc/descriptors); without descriptors
the gRPC bytes are still forwarded verbatim but recorded undecoded. Full bidirectional streaming forward is
out of scope (it is driven by the multiplex bidi pipeline, not the request/response forward path). The
transform is fail-safe — a non-gRPC request, an unknown method, or any conversion error leaves ordinary
HTTP forwarding byte-for-byte unchanged (GrpcForwardTranslator,org.mockserver.grpc). -
The dashboard now warns when log events are being silently evicted — the #1 cause of "verification
intermittently fails". When MockServer's log ring buffer fills up, the oldest events are dropped, so
verifications and the dashboard silently miss requests. The Dashboard and Traffic views now show a
dismissible warning banner whenever the server'smock_server_dropped_log_eventscounter is non-zero
("The log ring buffer is full, so the oldest events have been dropped (N so far)…"), pointing at
maxLogEntries/ringBufferSizewith a link to the performance docs. The banner reuses the existing
Prometheus metrics endpoint the Metrics view already polls (no new server endpoint) and stays hidden on a
healthy server or when metrics are disabled. It re-appears only if more events are dropped after a
dismissal. -
Bulk actions in the dashboard: multi-select expectations and captured requests to clear them in one go.
The Active Expectations list and the Traffic inspector each gained a "Select" mode with per-row checkboxes,
a select-all toggle and a running count. "Delete selected" removes the chosen expectations (batched per-id
clears) and "Clear selected" removes the chosen captured requests from the log, each behind a confirmation
dialog. Compare mode in the Traffic inspector is unchanged (still capped at two rows for a diff) and is
mutually exclusive with the new uncapped select mode. -
Migration importers for WireMock, Mountebank and Mockoon. Teams moving off another mock tool can now
convert their existing stubs into MockServer expectations in one shot through the existing
PUT /mockserver/importendpoint, via?format=wiremock,?format=mountebankor?format=mockoon
(all three are also auto-detected from the JSON structure when noformatis supplied). Each importer maps
the foreign matcher/response model onto MockServer's:- WireMock stub JSON (single stub,
mappingsarray, or bare array) —method/urlPath/urlPathPattern/
urlPattern/url,queryParameters/headerspredicates (equalTo/matches/contains),bodyPatterns
(equalToJson/matchesJsonPath/contains/matches/equalTo), responsestatus/headers/body/
base64Body/jsonBody/fixedDelayMilliseconds,fault→ connection error,proxyBaseUrl→ forward,
WireMock scenarios → MockServer scenarios, andpriority(inverted, since WireMock 1 = highest). - Mountebank imposters (
http/httpsonly;tcp/smtpskipped with a warning) —equals/deepEquals/
contains/matches/exists/startsWith/endsWithpredicates → matchers,is→ response,proxy→
forward,fault→ connection error,_behaviors.wait→ delay,_behaviors.repeat→Times, and multiple
isresponses → one cycling multi-response expectation. - Mockoon environments — each
route→ expectation(s) with:parampath segments converted to regex,
responsestatusCode/headers/bodyandlatency→ delay, responserules→ matchers (with descending
priority so array order and thedefaultcatch-all are preserved), andresponseModeSEQUENTIAL/RANDOM
→ the matching MockServer response mode.
Every foreign construct with no faithful MockServer equivalent (TCP imposters, XPath/XML predicates, response
templating/transformers, compoundand/or/notpredicates, JavaScriptinject, unsupported rule
operators, …) produces a structured warning in the response body —{ "expectations": [...], "warnings": [...] }— rather than being silently dropped. Secret redaction is on by default (as for HAR/Postman/Pact
import). No new runtime dependencies (Jackson only). Seedocs/code/request-processing.mdand the
Importing Expectations page. - WireMock stub JSON (single stub,
-
AsyncAPI broker mocking — Kafka Avro/Confluent Schema Registry, AMQP subscribe/verify, and MQTT 5. The
mockserver-asyncmodule gains three enterprise-broker parity features, all driven from the existing
PUT /mockserver/asyncapibrokerConfig:- Kafka Avro in the Confluent Schema Registry wire format. Set
kafkaValueFormat: "avro"to publish and
consume Kafka messages framed asmagic byte + schema id + Avro binary, byte-compatible with real Confluent
Avro producers/consumers. Two modes: registry-backed (kafkaSchemaRegistryUrl— the schema is registered
under<topic>-valueon publish and resolved by id on consume) and registry-less (an inlineavroSchema
plus a fixedavroSchemaId). Consumed Avro is decoded back to JSON so.../asyncapi/verifysubstring and
JSON-path checks work unchanged. Implemented with Apache Avro (Apache 2.0) plus a hand-rolled 5-byte
framing and a minimal JDK-HttpClientSchema Registry REST client — deliberately avoiding the
Confluent Community License serde stack. Protobuf is deferred. - AMQP (RabbitMQ) subscribe/verify. AMQP is no longer publish-only: with
consume: true, MockServer now
subscribes to and records AMQP messages for verification, mirroring Kafka/MQTT. The queue is derived from the
channel'sbindings.amqp(queue-based consumes the named queue; routingKey-based declares the exchange and
binds a private queue on the routing key). - MQTT 5.
mqttProtocolVersion: 5selects the Paho v5 client for publish and subscribe (default3/3.1.1);
v5 additionally delivers message headers (e.g. correlation IDs) as MQTT 5 user properties on publish and
records them as headers on consume — which MQTT 3 cannot carry.
New Docker-gated live-broker tests (Kafka, RabbitMQ, Mosquitto) plus non-Docker serde/wire-format unit tests
cover all three. See docs/code/async-messaging.md. - Kafka Avro in the Confluent Schema Registry wire format. Set
-
Mock OpenAI Realtime & Gemini Live voice APIs over WebSocket — new
RealtimeMockBuilder. MockServer can
now mock the two dominant realtime (voice) LLM protocols so agents/apps that use them can be tested offline,
with no real API and no audio hardware. A new pure event codec pair inmockserver-core
(org.mockserver.llm.realtime.OpenAiRealtimeCodec/GeminiLiveCodec) generates the provider-correct
WebSocket event stream for one scripted assistant turn, and the Java clientRealtimeMockBuilder
(org.mockserver.client) wires it into a standardhttpWebSocketResponseexpectation — an initial pushed
session.createdplus per-incoming-frame matchers — so no new action type, DTO, or JSON schema is
required (exactly as A2A streaming reuseshttpSseResponse). OpenAI Realtime (GA 2025 event protocol,
wss://.../v1/realtime): pushessession.createdon connect, acknowledgessession.updateand
conversation.item.create, and answers eachresponse.createwith the full lifecycle —response.created
→response.output_item.added→response.content_part.added→ per-token
response.output_audio_transcript.delta+response.output_audio.delta(audio modality) or
response.output_text.delta(text modality) → the matching*.donemarkers →response.donewith usage.
Gemini Live (BidiGenerateContent): answerssetup→setupCompleteand eachclientContentturn with
a streamedserverContentchunk sequence +generationComplete/turnCompletecarryingusageMetadata.
Streaming timing follows a deterministictokensPerSecond/ time-to-first-token model; audio bytes are opaque
silence placeholders (the fidelity target is the event protocol, not audio DSP). Deferred protocol corners
(server VAD / input-audio-buffer events, function-call output items, GeminitoolCall/realtimeInput, etc.)
are documented indocs/code/ai-protocol-mocking.mdrather than half-implemented. -
Prometheus
_totalcounters for the five monotonic metrics (correctrate()/increase()). The five
genuinely-monotonic counts —requests_received_count,expectations_not_matched_count,
response_expectations_matched_count,forward_expectations_matched_count, andllm_chaos_injected_count—
now additionally publish a proper PrometheusCounteralongside their legacy gauge:
mock_server_requests_received_total,mock_server_expectations_not_matched_total,
mock_server_response_expectations_matched_total,mock_server_forward_expectations_matched_total, and
mock_server_llm_chaos_injected_total. This is non-breaking and additive — the legacy_countgauges are
retained unchanged so the dashboard UI and existing Grafana dashboards keep working, while PromQL
rate()/increase()queries can now use the true monotonic_totalseries (e.g.
rate(mock_server_requests_received_total[5m])). The new counters are incremented in lock-step with the
legacy gauges from the same call sites and are mirrored to the OTLP export as observable monotonic counters. -
WASM matcher envelope v2 — query parameters and cookies in
match_request. The richer WASM ABI now
exposes the request's query-string parameters and cookies to a module, so a rule can route on
?tenant=acmeor asessioncookie, not just method/path/headers/body. The JSON envelope passed to
match_requestgained a top-levelversionfield (currently2) plusqueryStringParameters(name to
array of values) andcookies(name to single value). The change is additive and backward compatible:
every envelope version is a strict superset of the previous one, so existing version-1 modules (which read
only method/path/headers/body and ignore unknown fields) keep working unchanged — guarded by a
WasmRuntimeRequestV2AbiTestthat runs the version-1 example module against a version-2 envelope. The
mockserver-wasm-sdkRust authoring crate gainsreq.query_param(...),req.cookie(...)and
req.version()accessors (returningNoneagainst an older envelope), and a new
examples/wasm/rust-request-v2/sample module (with prebuilt.wasm) demonstrates query-parameter and
cookie routing. ThePOST /mockserver/wasm/testendpoint acceptsqueryStringParametersandcookiesin
the sample request. Seedocs/code/wasm-rules.md. -
Test a WASM rule from the editor extensions. The VS Code and JetBrains MockServer extensions can now
callPOST /mockserver/wasm/testto check what a WASM module does against a sample request without
uploading it or creating an expectation, complementing the existing WASM module upload/list wiring. -
Deterministic embeddings are now semantically plausible, so offline RAG-retrieval tests can rank. A mocked
embeddings response withdeterministicFromInput: truepreviously produced a hash-seeded uniform-random unit
vector, so cosine similarity between related texts was meaningless — you could not test vector-search / RAG
ranking against a mock. MockServer now builds the deterministic vector by n-gram feature hashing: the input
is tokenised (Unicode-aware, lowercased) into word unigrams, word bigrams, and character 3-grams; each feature
is hashed (seeded FNV-1a) into a bucket with a signed, sublinear-TF-weighted contribution; the result is
L2-normalised. Texts that share vocabulary now have a higher cosine similarity (paraphrases ~0.3–0.6) while
unrelated texts stay near-orthogonal (~0.0–0.1) — e.g."the cat sat on the mat"ranks far above
"quarterly financial report"against"a cat sits on a mat"— so retrieval code can rank related documents
offline with no real embedding model. The vector stays deterministic for the same input, seed, and
dimensions and unit-length (feature-less input falls back to a seeded non-zero vector); the
dimensions/seedparameters, provider JSON envelopes, and the non-deterministic (default) random path are
unchanged. -
Authenticated cross-node cluster verify/retrieve fan-in. The opt-in cluster fan-in
(clusterVerifyFanIn) now works on a cluster with control-plane authentication enabled. A new
clusterFanInPeerAuthTokenproperty (envMOCKSERVER_CLUSTER_FAN_IN_PEER_AUTH_TOKEN, default empty)
gives the peer accessor (HttpClusterPeerAccessor) a credential to present on every cross-node query:
when set, it is sent verbatim as the control-planeAuthorizationheader (include the scheme, e.g.
Bearer <jwt>), so peers accept the fan-in query instead of rejecting it with 401/403. All nodes must
share the same token. With no token (the default) no credential is sent — unchanged, non-breaking
behaviour; fan-in remains off by default. The property is wired through the config trio
(ConfigurationProperties/Configuration/ConfigurationDTO) and is covered by the reflective DTO
round-trip drift guard. The programmaticretrieve(REQUESTS/REQUEST_RESPONSES)path that backs
dashboard export / one-shot traffic queries already fans in when enabled (verified with a test). Still
node-local by design (documented, no shared clock across nodes):verifySequencecross-node ordering,
the live dashboard WebSocket log-view stream, rate-limit / chaos-quota counters, and mTLS
client-certificate peer authentication. Seedocs/code/clustered-state.md. -
Startup warm-up removes first-request latency — new
startupWarmupproperty (default on). The very
first request handled by a freshly started MockServer was a few hundred milliseconds slower than every
request after it because the request-handling path (Netty HTTP codec, JSON serialisation, response writers)
only loads and initialises on first use — a cost paid by every readiness poll, including Testcontainers wait
strategies. MockServer now sends itself a single backgroundPUT /mockserver/statusloopback request
immediately after the ports bind, so that one-off cost is paid off the start-up thread and the first real
request is fast. The warm-up never delays port binding, is fail-soft (any failure is ignored and logged only
at TRACE), and uses a control-plane endpoint that creates no recorded requests or log events, so it never
pollutesverify/retrieve. Disable with-Dmockserver.startupWarmup=false/
MOCKSERVER_STARTUP_WARMUP=false(e.g. in a locked-down environment where MockServer must not connect to
itself). -
MCP spec 2025-06-18 negotiation with structured tool output and resource links. MockServer's MCP
server (McpRequestProcessor) now advertises and negotiates the 2025-06-18 MCP revision while staying
backward compatible: a client that requests2025-06-18gets it, clients still on2025-03-26/2024-11-05
keep getting their requested revision (echoed back), and an unknown/omitted version falls back to the latest
2025-06-18(negotiateProtocolVersion, stored per-McpSession). For sessions that negotiated 2025-06-18+,
tools/callresults additionally carrystructuredContent(the machine-readable tool-result object)
alongside the existing text block; older sessions are unchanged. The JavaMcpMockBuildergains
withOutputSchema(...)(advertised intools/list),respondingWithStructured(text, structuredJson)
(emitsstructuredContent), andrespondingWithResourceLink(uri, name, description, mimeType)(emits a
resource_linkcontent item), and now defaultsprotocolVersionto2025-06-18. TheMcpContractTest
conformance tester defaults to2025-06-18, records the server's negotiated version, and validates the new
structuredContent/resource_linkshapes when present (optional — older servers still pass).Mcp-Session-Id
emission/handling was already in place. Elicitation (elicitation/create) and the GET SSE server-push stream
are not mocked (they require a server→client channel MockServer's request/response model does not have); JSON-RPC
batching remains accepted for back-compat. No MCP tools were added or reclassified. -
Expectation-authoring and record/replay control tools on the MCP server. An AI coding agent
(Claude Code, Cursor, etc.) can now stand up and drive mocks entirely through the MCP server at
/mockserver/mcp, closing the "AI agents can only read, not author" gap. Three new tools are added,
each delegating to the existingHttpStatecontrol-plane operation (no logic fork):
list_expectations(READ — active expectations, optionally filtered by method/path;
PUT /retrieve?type=ACTIVE_EXPECTATIONS),set_operating_mode(MUTATE — switch SIMULATE/SPY/CAPTURE;
PUT /mockserver/mode), andpromote_recordings(MUTATE — turn recorded traffic into active mocks with
redaction/consolidation/parameterization;PUT /mockserver/recordings/promote). Each tool is classified
read-vs-mutate so the control-plane authorization gate applies — a MUTATE tool requires the MUTATE role
whencontrolPlaneAuthorizationEnabledis on. The pre-existing authoring/read tools (create_expectation,
raw_expectation,clear_expectations,verify_request,retrieve_recorded_requests,
retrieve_request_responses) are unchanged; the/mockserver/modeand/mockserver/recordings/promote
REST handlers were refactored onto new sharedHttpState.setMode(...)/HttpState.promoteRecordings(...)
methods so REST and MCP share one code path. -
OpenAI Responses API server-side state —
previous_response_idchaining,store, and
GET /v1/responses/{id}. MockServer's Responses API mock (OPENAI_RESPONSES) is no longer stateless:
each issuedPOST /v1/responsesresponse is recorded (by default; honours the request'sstoreflag) in a
new process-wideOpenAiResponsesStore, so agents that chain turns viaprevious_response_id— sending only
the new turn plus the prior response id — now run against the mock.OpenAiResponsesCodec.decodeprepends the
stored prior conversation when a request carries aprevious_response_id, so conversation matchers and usage
inference see the full dialogue, andGET /v1/responses/{id}returns the stored response body. The store is
bounded (LRU), cleared on server reset, and fully back-compatible — a request with noprevious_response_id
and the defaultstore:truebehaves exactly as before (it only additionally records the response). -
OpenAI-compatible provider aliases: Mistral, xAI (Grok), DeepSeek, Groq, and OpenRouter. Five new
Providervalues whose codecs and runtime clients delegate to the OpenAI Chat Completions implementations
(exactly asAZURE_OPENAIdoes), distinguished by host (api.mistral.ai,api.x.ai,api.deepseek.com,
api.groq.com,openrouter.ai) in bothLlmProviderSnifferandProviderDetector. Proxy observability now
classifies traffic to these gateways as LLM (with provider-correct GenAI spans and cost metrics) instead of
dropping it as non-LLM. Approximate, clearly-flagged pricing rows were added for each inLlmPricing
(OpenRouter routes vendor-prefixed model ids such asopenai/gpt-4oto the underlying vendor's table). -
Chaos experiment composition: recurring runs, staged TCP/lifecycle faults, steady-state pre-check, and
history. TheChaosExperimentOrchestratornow composes fault primitives beyond a single one-shot HTTP run,
all as new optional fields that default to the previous behaviour:
(1) Recurring cron experiments — set"recurring": truealongside acronScheduleand, after each clean
completion, the experiment records the run and re-arms itself for the next cron occurrence (e.g. a
"nightly-error-storm"on"0 2 * * *") instead of going terminal after one run; a stop, auto-halt, or SLO
breach still ends it for good.
(2) Staged TCP / connection-lifecycle faults — a stage may carry atcpProfilesmap (host →
TcpChaosProfile) applied/reset with the same discipline as HTTPprofiles, so transport-level faults
(RST, GOAWAY, latency, bandwidth, preemption) get the same auto-halt and stage progression; a stage is valid
with HTTP profiles, TCP profiles, or both.
(3) Steady-state baseline pre-check — with ansloCriteria, an optionalbaselineWindowMillisevaluates
the SLO over the pre-experiment lookback window before applying stage 0 and refuses to start
(aborted_baseline_unhealthy, verdict attached) if the steady state does not already hold, instead of running
and blaming the experiment.
(4) Bounded experiment history — every terminal transition (including each recurring run and a
baseline-refused start) is appended to a bounded ring (last 50, newest first) exposed at
GET /mockserver/chaosExperiment/historyfor recurring-run trails and CI trend dashboards. The new
recurring,tcpProfiles, andbaselineWindowMillisfields round-trip through the experiment definition JSON. -
Typed mock-drift client methods across all 8 client libraries. Each client now exposes a typed wrapper
for the drift-detection control plane —retrieveDrift()(GET /mockserver/drift, returns the parsed
{ count, drifts }report) andclearDrift()(PUT /mockserver/drift/clear) — so programmatic users no
longer have to hand-roll raw HTTP. Added to the Java (retrieveDrift/clearDrift), Node
(retrieveDrift/clearDrift), Python (retrieve_drift/clear_drift), Ruby (retrieve_drift/clear_drift),
Go (RetrieveDrift/ClearDrift), .NET (RetrieveDrift/ClearDrift+ async variants), Rust
(retrieve_drift/clear_drift) and PHP (retrieveDrift/clearDrift) clients, each following that client's
existing control-plane conventions, with mocked-transport unit tests. The drift-detection documentation now
shows client-library tabs alongside the REST examples. -
Experimental JDK 25 AOT-cache Docker image variant (Project Leyden) for ~2x faster container startup.
Newdocker/aot/Dockerfilebuilds a MockServer image on a jlink-trimmed Temurin 25 runtime with an
ahead-of-time cache (JEP 483/514) baked in via a training run at image build time. Measured time-to-ready
roughly halves versus the standard image (~0.35 s vs ~0.7–0.8 s fromdocker runto a 200 from
PUT /mockserver/status) with identical behaviour — it is the real HotSpot JVM, so 100% feature parity.
Published from this release as opt-inX.Y.Z-aot/latest-aottags (Docker Hub + ECR Public),
error-isolated in the release pipeline like the clustered image, and selectable from the MockServer
Testcontainers module vianew MockServerContainer(MockServerContainer.aotImage()). The Testcontainers
documentation now explains the fast-test hierarchy (suite-scoped container +reset(), in-process
MockServer for Java tests) and the runtime-level options evaluated (AppCDS, AOT cache, GraalVM native
image) with measured figures and the reasons native-image is not supported. (relates to #2385) -
Mock-drift detection master switch and sampling. New
driftDetectionEnabled(boolean, defaulttrue) turns
mock-drift analysis of forwarded responses on or off, anddriftSampleRate(double0.0–1.0, default1.0)
analyses only a sampled fraction of forwarded responses. Both defaults preserve the previous always-on behaviour;
setdriftDetectionEnabled=false(or lowerdriftSampleRate) to cut the per-forward overhead when proxying at
high volume. -
Runnable Kubernetes example: load-injection metrics visualised in Grafana over Prometheus and OpenTelemetry.
Newexamples/kubernetes/load-injection-observabilitystands up a local k3s cluster (via k3d) running MockServer,
Prometheus, an OpenTelemetry Collector and Grafana with a provisioned dashboard that renders the full
mock_server_load_*family — active VUs, throughput, latency percentiles, failures, status codes, throttling and
data transfer — alongside JVM heap/GC/threads and real pod CPU/memory, driving the point that MockServer's
first-class load-injection metrics can be charted next to the system under test on one dashboard. The Load
Injection and Examples documentation pages now lead with this observability advantage and link the example. -
Recorded expectations can be consolidated, parameterised, and promoted to active mocks in one REST call.
GET /mockserver/retrieve?type=RECORDED_EXPECTATIONS&consolidate=true¶meterize=truenow post-processes
captured recordings:RecordedExpectationPostProcessor.consolidate()groups exchanges by request shape into a
singleTimes.unlimited()expectation, infers{id}path-parameter slots from varying URL segments, strips
volatile headers, and sequences differing responses as aSEQUENTIAL httpResponseslist. A new
PUT /mockserver/recordings/promoteREST endpoint filters, redacts, consolidates/parameterises and activates
recorded traffic in one step — the REST equivalent of the MCPcreate_expectations_from_recorded_traffic
tool.PUT /mockserver/import?format=harnow also accepts?consolidate/?parameterizeto collapse
repetitive HAR captures. Default (non-?consolidate) retrieval is unchanged and non-breaking. -
SSE, WebSocket, and gRPC stream messages can now be rendered as Velocity, Mustache, or JavaScript templates.
An optionaltemplateTypefield (VELOCITY|MUSTACHE|JAVASCRIPT) onhttpSseResponse/
httpWebSocketResponse/grpcStreamResponsemakes each event or message payload a response template rendered
against the triggering request — using the same engines, context (request fields,jsonPath, built-in helpers,
faker, scenario state), and lazy-caching asHttpResponseTemplateActionHandler. SSE renders a per-event copy so
the stored event is never mutated; WebSocket renders text frames before breakpoint interception; binary frames are
never templated. JavaScript streaming payloads require GraalJS and fail loudly with the same actionable error the
response-template path uses when GraalJS is absent. Opt-in and non-breaking: without atemplateTypeevery
payload is emitted byte-for-byte unchanged. -
Load scenario steps can assert response correctness and abort when a check-failure threshold is exceeded.
EachLoadStepnow accepts an optionalcheckslist — each check tests the responsestatuscode,
aheadervalue, or ajsonPathexpression — and acheckFailureRatethreshold (0.0–1.0). When the
observed failure rate for a step's checks breaches the threshold, MockServer can abort the scenario or record
a threshold violation, so a load run generating incorrect responses (e.g. the upstream returning500s that
the test was silently ignoring) fails the scenario rather than producing meaningless throughput numbers. -
Expectations can now match on the client certificate presented in a mutual-TLS handshake. A new
clientCertificaterequest matcher selects expectations by the leaf certificate of the client's mTLS chain:
subject(Common Name, full Distinguished Name, or any Subject Alternative Name — DNS / IP / email / URI),
issuer(CN or full DN), orfingerprintSha256(SHA-256 of the DER encoding, colon/whitespace and case
normalised). Each criterion is aNottableString(exact, regex, or!-negated); negation uses De Morgan
semantics across candidate forms so!Xonly matches when no candidate equalsX. Non-breaking: an
expectation without aclientCertificatematcher behaves exactly as before, and a request that presents no
chain never matches a non-blank criterion. Matching only — mTLS authentication (MTLSAuthenticationHandler)
is unchanged. -
Recorded traffic can now be persisted to disk and re-imported on demand (opt-in). With
persistRecordedRequestsToDiskenabled (default off), the append-only NDJSON archive captures both forwarded
(FORWARDED_REQUEST) and mocked (EXPECTATION_RESPONSE) request/response pairs, flushed one JSON object per
line, so the complete session survives ring-buffer eviction and server restarts;redactSecretsInLogredaction
still masks credentials on write. NewPUT /mockserver/import?format=recordingreloads the archive
(from the request body, or frompersistedRecordedRequestsPathvia?source=disk) via
RecordedTrafficImporter, re-injecting each pair into the event log exactly as an in-memory recording —
idempotent, since reloaded entries never grow the disk archive. The importer skips and counts malformed or
crash-truncated lines (exposed inx-mockserver-recorded-requests-skipped) and only rejects a body where
every non-blank line is unparseable; an empty archive imports 0 entries (201) rather than400.
Re-importedEXPECTATION_RESPONSEexchanges are recorded as forwarded under disposition-based verification
— a known limitation, documented. Both defaults preserve existing behaviour. -
verify()andretrieve()can scatter-gather across all cluster members (opt-in). The request/response
event log is per-node, so behind a load balancerverify()andretrieve(REQUESTS/REQUEST_RESPONSES)
previously saw only the traffic that hit the queried node — a silent correctness trap. Two new properties
(clusterVerifyFanIn,clusterVerifyFanInPeers) enable scatter-gather:ClusterFanInqueries each peer's
local log with afanInLocalOnly=truerecursion guard, merges results, and appliesVerificationTimesto the
combined count. Fail-closed: an unreachable peer yields a502verify failure rather than a partial result.
Off by default (non-breaking).verifySequencecross-node ordering, response-aware verify, and dashboard log
fan-in are deferred.
Changed
-
The standard and local Docker images now run on a JDK 25 runtime. The
mockserver/mockserverimage
(built fromdocker/local/Dockerfile, and itsdocker/Dockerfiledownload-mode reference) now bakes a
jlink-trimmed Eclipse Temurin 25 runtime and its AppCDS archive, moving off JDK 17. The MockServer library
itself is still compiled to the Java 17 bytecode floor — this is a runtime-only change (running the same jar on
a newer JVM), with no API or behaviour changes. The jlink step uses the JDK-25--compress=zip-6form (the
legacy numeric--compress=2was removed after JDK 17). The baked startup-optimisation figure (~0.57 s) was
measured on JDK 17 and should be re-measured on JDK 25; a single local container observation was comparable. -
Standard Docker image starts ~34% faster — Application Class Data Sharing (AppCDS) archive baked in at
image build. The standardmockserver/mockserverimage now trains an AppCDS class archive over MockServer's
own classes during the image build (the same train-at-build approach as the-aotvariant, on a
jlink-trimmed JDK 17 runtime), so the JVM maps pre-parsed class data instead of re-loading ~5,800 classes on
every container start. Measured launch-to-ready dropped from ~0.86 s to ~0.57 s on the same machine. This is
the standard HotSpot JVM with full feature parity — no behaviour changes — and if the archive is ever
missing or unreadable the JVM logs a warning and starts normally without it. Image size is unchanged
(the trimmed runtime offsets the archive). The-aotvariant (JDK 25 Leyden) remains the fastest-start
option. -
Fewer startup threads when not proxying — the forward-client event-loop group is now created lazily.
The Netty event-loop group used to forward/proxy requests to upstream services (5 threads by default,
clientNioEventLoopThreadCount) was created at server construction even for pure-mock deployments that
never proxy. It is now created on the first forward/proxy action, so mock-only servers start with fewer
threads and less allocation. Proxy behaviour is unchanged, including the guarantee that forwarded requests
never share event loops with the server's own worker threads (the deadlock-prevention isolation is
preserved exactly). -
Faster startup when TLS is not used — BouncyCastle security provider now registers lazily. The
BouncyCastle JCE provider (several hundred classes) was loaded and registered during server construction
even when no TLS connection was ever made. Registration is now deferred to the first operation that
actually needs it (dynamic certificate or key generation, typically the first HTTPS connection), removing
that class-loading cost from start-up for plain-HTTP usage. Behaviour is unchanged for TLS users — the
provider registers exactly as before on first use, andproactivelyInitialiseTLS=truestill initialises
everything eagerly at start-up.
Fixed
-
Fixed the Python client's
Body.regex(...)factory producing a literal-string match instead of a real
regex matcher. It previously emitted the wire form{"type": "REGEX", "string": <value>}; MockServer's
BodyDTODeserializertreats astringvalue-key as aSTRINGbody (overriding thetypefield), so
Body.regex(".*admin.*")was silently deserialised to aStringBodyand only matched the literal text
.*admin.*, never as a regex. It now emits the schema-correct{"type": "REGEX", "regex": <value>}(the
same object as the existingRegexBody), so a request body that merely contains the pattern matches as
intended. (The parallelBody.regex_match(...)helper introduced in the same unreleased cycle has been
removed as redundant — use the now-correctBody.regex(...).) -
Fixed
JAVASCRIPTresponse/forward templates silently degrading when the optional GraalJS engine
(org.graalvm.polyglot:polyglot+js) is absent from the classpath — as it is in the standard netty
jar-with-dependencies and Docker image. Previously such a template logged an error and returned an empty
(null) response, so a user who wrote a JavaScript template got a confusing degraded result. It now
fails loudly with a clear, actionable error: "JavaScript response templates require the GraalJS
engine, which is not on the classpath. Add the org.graalvm.polyglot:js (or js-community) dependency, or
use the Velocity or Mustache template engine." Behaviour is unchanged when GraalJS is present. The
response-templates documentation now states that only Velocity and Mustache are available by default and
that JavaScript requires adding the optional GraalJS dependency (or thegraaljsDocker image variant). -
Fixed silent loss of expectations and log events on JVMs that report an undefined (
-1) heap max via JMX
(for example GraalVM native images or unusual servlet-container setups): the heap-based defaults for
maxExpectationsandmaxLogEntriescomputed a negative capacity, so expectations were accepted with201
but never stored and log events were dropped from startup. The sizing now falls back toRuntime.maxMemory()
and floors both defaults at 1,000 when no usable heap ceiling is reported. (relates to #2385) -
Header-only
FORWARD_REPLACEmodifications no longer collapse streaming responses, and content-type-less
streams are relayed incrementally on all forward paths. Any response override on a
forwardOverriddenRequestpreviously forced full body aggregation (disableStreaming=true), so adding a
single CORS or trace header to an SSE or LLM streaming upstream silently buffered the entire response and
could cause the client to time out waiting for response headers. A header-only modification (status / headers
/ cookies, no body change) is now applied to the streamed response head while body chunks are relayed
untouched; only a body-affecting override (body/schema replacement, JSON patch/merge-patch, or a response
template) still disables streaming. Additionally, content-type-less streaming (SSE without
Content-Type: text/event-stream) is now relayed incrementally on the transparent CONNECT relay and the
HTTP/2 upstream forward path as well as the HTTP/1.1 path.
Security
-
Control-plane mTLS now validates a full PKIX certificate path and enforces the clientAuth Extended
Key Usage.MTLSAuthenticationHandlerpreviously validated a presented client certificate only with a
single-level signatureverify()plus a validity-windowcheckValidity()check. It now builds a proper
PKIXCertPathfor the presented certificate and validates it against the configured control-plane CA(s)
as trust anchors (revocation checking disabled by default, consistent with the rest of the codebase, so
validation stays fully offline). It additionally enforces Extended Key Usage: when the client certificate
carries an EKU extension it must permitclientAuth(id-kp-clientAuth1.3.6.1.5.5.7.3.2) or
anyExtendedKeyUsage, so a certificate scoped toserverAuthonly can no longer authenticate as a
control-plane client. A certificate with no EKU extension remains unrestricted and is still accepted (RFC
5280 practice), so this is backward compatible with existing client certificates. Enabled only when
controlPlaneTLSMutualAuthenticationCAChainis configured. -
SOCKS5 proxy authentication now compares the username and password in constant time.
Socks5ProxyHandlercompared the configured proxy credentials withString.equals, whose early-exit on
the first differing byte is a timing side channel an attacker could use to recover the credentials one
byte at a time. Both the username and password are now compared with a shared constant-time helper
(ConstantTimeEquals,MessageDigest.isEqualon UTF-8 bytes) — the same timing-safe comparison the
data-plane authenticator already uses, now extracted so there is a single audited implementation. Correct
credentials are still accepted and wrong credentials still rejected exactly as before. -
Documented that the
/mockserver/metricsPrometheus scrape endpoint is unauthenticated by design,
and how to secure it. The scrape endpoint is intentionally served outside the control-plane auth gate
(Prometheus/OTEL scrapers cannot present a control-plane certificate or bearer token), so its labels
(upstream_host; LLMprovider/modeltoken & cost counters) are readable by anyone with network reach.
No behaviour change:metricsEnabled=false(the default) already fully disables it (returns404, exposes
nothing), and the JSON snapshotPUT /mockserver/retrieve?type=METRICSremains behind the control-plane
auth gate — only the scrape endpoint is open. The API Security page and internal docs now spell out the
trade-off and the three ways to lock it down: disable it, restrict it at the network layer, or prefer
PUSH-based export (OpenTelemetry OTLP metrics or Prometheus Remote-Write), which exposes no scrape endpoint. -
The dashboard and UI WebSocket now require control-plane authentication when it is enabled. With
mTLS/JWT/OIDC control-plane auth configured,GET /mockserver/dashboard*and the
/_mockserver_ui_websocketupgrade were previously served without credentials, so any network-reachable
client could receive a live push of all captured traffic — including request and response bodies. Both are
now gated by the sameHttpState.controlPlaneRequestAuthenticatedcheck asPUT /mockserver/configuration;
the WebSocket upgrade returns a raw401/403handshake rejection when credentials are absent or
insufficient. The dashboard is treated as a read, so a read-only control-plane role may view it. When no
control-plane auth is configured (the default) the dashboard and WebSocket remain open, and/status/
/readyare always credential-free. The SPA'suseWebSockethook now shows an actionable auth-required
message on401/403. -
Experimental HTTP/3: QUIC tokens now bind the client source address, and CONNECT-UDP relay targets can be
restricted. Two defence-in-depth fixes apply whenhttp3Portis non-zero (off by default). (1)
Source-address-validating retry tokens:InsecureQuicTokenHandler(plaintext, trivially forgeable) is
replaced bySourceAddressQuicTokenHandler(HMAC-SHA256, per-server random key, client IP bound), so a
forged-source Initial packet cannot obtain a valid token — mitigating QUIC address-spoofing and traffic
amplification across IPv4 and IPv6. (2) CONNECT-UDP relay restriction: newhttp3ConnectUdpAllowedTargets
(comma-separated host /host:portallowlist, default empty) limits MASQUE relay targets; non-listed
targets are refused with403. The existingforwardProxyBlockPrivateNetworkspolicy (private, loopback,
link-local, cloud-metadata ranges) is also honoured on the QUIC relay path. Both default to the previous
open behaviour unless a restriction is opted into.
Documentation
- Consumer doc navigation improvements on four pages.
configuration_properties.htmlgains a searchable property index (filter input + full table of all ~150 properties with section links, client-side JS, links open the accordion automatically) and anchors for the Clustering and Cloud Blob Store sections.using_openapi.htmlgains a capability overview table (generate expectations / use as request matcher / verify / clear / contract test — per spec format).debugging_issues.htmlgains a retrieval methods quick-reference table (REST path + Java client method + return type for each retrieve type).proxy/configuring_sut.htmlgains a proxy-type comparison TOC table (code changes required, multi-host support per proxy type).