Skip to content

MockServer 9.2.0

Latest

Choose a tag to compare

@jamesdbloom jamesdbloom released this 27 Oct 00:11
· 28 commits to master since this release

[9.2.0] - 2026-10-26

Security

  • In the mockserver-netty-no-dependencies jar and the Docker images, the bundled Kafka client used for AsyncAPI mocking now refuses the JndiLoginModule and LdapLoginModule JAAS login modules in a saslJaasConfig, as Kafka does by default since CVE-2023-25194. The jar renames the libraries it bundles, and the renaming changed the module names Kafka checks against, so the check never matched (#2869)
  • With dataPlaneAuthenticationRequired on, a mocked endpoint could still be reached without credentials in several ways. A path that only starts with /mockserver (such as /mockserverfoo), or a /mockserver/... path that the server does not handle (such as /mockserver/nope, or /mockserver/status and /ready over HTTP/3), skipped the check and was answered by any matching expectation, a catch-all one included. Over HTTP/1.1, an expectation with respondBeforeBody answered the first request on a connection without checking credentials. And a gRPC bidirectional-streaming expectation (respondWithGrpcBidi) answered over HTTP/2 or HTTP/3 without checking them. gRPC server reflection also answered without credentials, listing the loaded services and message types, and so did a gRPC chaos fault and the UNIMPLEMENTED answer for an unknown gRPC method, which told the caller which methods exist. All of these are now refused (401, or gRPC status UNAUTHENTICATED) like any other mocked endpoint. A refused request does not use up the expectation's Times, move its scenario on or count towards a gRPC chaos profile. The control plane, the readiness and liveness probes (the gRPC health check included), and CORS preflights for the control plane still work without data-plane credentials. Data-plane authentication does not apply to DNS mocking
  • dataPlaneAuthenticationRequired now applies to the WAR deployments (mockserver-war, mockserver-proxy-war) and to InProcessMockServer, which served their mocks without credentials whatever it said. Credentials are checked before a request can match an expectation, so a refused request uses up no Times and moves no scenario, and a CORS preflight for a control-plane route stays open, as on the standalone server. An in-process request carries its credentials in its headers. In a WAR, tlsMutualAuthenticationRequired is now enforced too: a mocked request over plain HTTP gets 426 Upgrade Required, and one over HTTPS without a client certificate verified by the servlet container gets 401; the container's trust store decides which certificates are accepted (see upgrading to 9).
  • The Kafka SASL JAAS configuration for AsyncAPI broker mocking can now only be set at startup, with the new asyncKafkaSaslJaasConfig property (-Dmockserver.asyncKafkaSaslJaasConfig or MOCKSERVER_ASYNC_KAFKA_SASL_JAAS_CONFIG). A JAAS configuration names the Java login class the Kafka client loads, and PUT /mockserver/asyncapi let any control-plane caller choose it through brokerConfig.kafkaSecurity.saslJaasConfig. That request is now rejected with a 400 that names the startup property, and so is any other field in kafkaSecurity that MockServer does not know, so no Kafka client setting that loads a class can be passed in; saslMechanism and securityProtocol must be one of Kafka's own values. A request that selects SASL_PLAINTEXT or SASL_SSL uses the startup JAAS configuration (or the JVM's own JAAS file). Because every SASL login is now held by the server, such a request may only use the startup asyncKafkaBootstrapServers: it must omit kafkaBootstrapServers or name the same brokers, or it gets a 400 and no credential is sent, and it gets a 400 while no startup bootstrap servers are set. asyncKafkaBootstrapServers can now only be set at startup: PUT /mockserver/configuration ignores it and logs a warning. PUT /mockserver/configuration ignores asyncKafkaSaslJaasConfig and logs a warning, and GET /mockserver/configuration never returns it. To upgrade, move the saslJaasConfig value from your requests to asyncKafkaSaslJaasConfig, set asyncKafkaBootstrapServers at startup to the brokers it logs in to, and keep securityProtocol and saslMechanism in the request (see upgrading to 9)
  • retrieve with forwardUnmatchedTo now needs a control-plane role that may change state. The parameter repoints where unmatched requests are forwarded, but control-plane authorization treated the call as a read, so with controlPlaneAuthorizationEnabled a principal with only the READ role could redirect unmatched traffic. Such a principal now gets 403, as it already did for extractBodiesLargerThan; a retrieve without either parameter is still a read (see upgrading to 9)
  • Upgraded DOMPurify (dashboard UI dependency) to 3.4.17 to fix GHSA-cg2g-xh64-v4q7
  • Build-time dependency fixes, none shipped in a published package: the Node Testcontainers module's own development lockfile resolves undici 7.30.1 (Dependabot alerts 610, 611, 612; the published package ships only dist), and the VS Code extension's development dependency @vscode/vsce now resolves @azure/msal-node 7.1 through an npm overrides entry (alert 613; it reaches the build only through @vscode/vsce → @azure/identity, which has no release that allows msal-node 7 yet)
  • The paths MockServer writes files to, and the protoc executable it runs, can now only be set at startup: auditLogFile, persistedExpectationsPath, persistedRecordedExpectationsPath, persistedRecordedRequestsPath, memoryUsageCsvDirectory and directoryToSaveDynamicSSLCertificate, like recordedBodyFilesDirectory. Before, PUT /mockserver/configuration (or a client's updateConfiguration) could change them on a running server, so anyone who could reach the control plane could redirect the audit log (before its first write) or the generated certificates and their private keys into another directory, such as one on the extension classpath, or point a format=recording disk import (which reads persistedRecordedRequestsPath) at another file. grpcProtocPath, which names the protoc executable MockServer runs, is now startup-only for the same reason. A PUT that sets a different value is still answered, but the value is ignored and a warning is logged; a PUT that sends the value already in force logs nothing. Set them with a system property, environment variable, properties file or Configuration instead (see upgrading to 9)
  • The settings that decide who may use the control plane, what each user may do, whether control-plane changes are audited, and where dashboard usage events go can now only be set at startup: controlPlaneTLSMutualAuthenticationRequired, controlPlaneTLSMutualAuthenticationCAChain, the controlPlaneJWTAuthentication* and controlPlaneOidc* settings, controlPlaneAuthorizationEnabled, controlPlaneScopeMapping, controlPlaneAuditEnabled and dashboardAnalyticsEndpoint, like the controlPlaneBasicAuthentication* settings. Before, anyone allowed to change the configuration with PUT /mockserver/configuration could switch control-plane authentication off, trust their own keys or identity provider, give themselves a role, switch auditing off before making changes, or send every dashboard viewer's usage events to their own server. A PUT that sets a different value is still answered, but the value is ignored and a warning is logged, and GET /mockserver/configuration reports the value in force; the dashboard's Audit Trail view no longer has an on/off switch (see upgrading to 9)
  • A PUT /mockserver/configuration that changes where the default LLM backend sends requests (llmBaseUrl, or llmProvider while llmBaseUrl is empty) without also supplying llmApiKey now clears the held key and logs a warning, so the key is never sent to the new endpoint. Before, anyone allowed to change the configuration could collect the key by pointing llmBaseUrl at their own server. A PUT that supplies both applies both; a JSON llmBackendsConfig value that changes a backend's baseUrl or provider while leaving its key masked is not applied (see upgrading to 9)
  • A held secret is no longer sent to a destination that a control-plane caller can choose. clusterVerifyFanInPeers and clusterFanInPeerAuthToken can now only be set at startup: PUT /mockserver/configuration could repoint the fan-in peers at any host, and every verify or retrieve then sent it the cluster peer token, which grants write access to every node. A PUT that supplies a different value is ignored and logs a warning. The detect_llm_drift MCP tool, which a read-only caller may use, now refuses a cassette whose provider is not the configured LLM backend's provider and sends nothing; before, it replayed the cassette with that provider's client, which, when no llmBaseUrl was set, sent the held llmApiKey to that provider's default host. An AZURE_OPENAI backend with no llmBaseUrl now sends nothing, instead of sending its key to the placeholder host example-resource.openai.azure.com. See upgrading to 9.
  • noProxyHosts can now only be set at startup, like the upstream proxies it applies to (forwardHttpProxy, forwardHttpsProxy, forwardSocksProxy). Before, anyone allowed to change the configuration with PUT /mockserver/configuration could add hosts to it and so route forwarded traffic around an upstream proxy relied on for logging or data-loss checks. A PUT that sets a different value is still answered, but the value is ignored and a warning is logged, and GET /mockserver/configuration reports the value in force (see upgrading to 9)
  • Control-plane credentials no longer reach the event log. A successful control-plane call (for example changing the clock, the configuration, a scenario or a chaos experiment) used to store its whole request, Authorization header included, in the log, where anyone allowed to read logs (PUT /mockserver/retrieve?type=LOGS, including a read-only principal when control-plane authorization is on) and the dashboard could see it. Bearer tokens (JWT and OIDC) were exposed this way before this release. Every request the control-plane authentication gate sees is now logged with its Authorization, Proxy-Authorization, cookie and API-key headers masked, whatever redactSecretsInLog says; authentication and authorization failure entries are masked the same way. Mocked (data-plane) requests are still logged exactly as they were sent, so verification by header is unchanged; the diagnostic entry for a cut-short or undecodable request masks credentials whatever its path. TRACE-level wire logging still prints raw traffic, headers included, to the console or log file (not to the event log), so keep TRACE logs private. If your control plane uses bearer tokens and others could read its logs, treat the tokens it logged as exposed

Added

  • Error actions: "resetConnection": true answers a matched request with a TCP reset instead of a response, so the client sees "connection reset by peer" (ECONNRESET), where dropConnection closes the connection cleanly (Java client: error().withResetConnection(true); also in the Kotlin DSL, every client library and the dashboard composer). Over TLS no TLS close message is sent first. Over HTTP/2 the whole connection is reset (use streamError to reset one request); over HTTP/3, which has no TCP reset, the QUIC connection is closed with H3_INTERNAL_ERROR. Through a CONNECT or SOCKS tunnel the client's own connection is reset. WireMock and Mountebank CONNECTION_RESET_BY_PEER faults now import as resetConnection. Not available in the WAR deployment, like every error action. See error actions
  • Response templates: a systemValue helper reads an environment variable or a system property, like WireMock's systemValue: Velocity $systemValue.env('APP_REGION') and $systemValue.property('app.version'), JavaScript systemValue.env('APP_REGION'), Mustache {{#systemValue.env}}APP_REGION{{/systemValue.env}} and {{#systemValue.property}}app.version{{/systemValue.property}}. It only reads names listed in the new startup-only templateSystemValueAllowList property, like WireMock's --permitted-system-keys: a comma-separated list of exact names or prefixes ending in *, such as APP_*,HOSTNAME. The list is empty by default, so systemValue returns an empty value for every name until you set it. A name that is not on the list also returns an empty value, the same as a name that is not set, and is logged as a warning once. A bare * is ignored, and there is no way to list names or values. One list covers both environment variables and system properties, and MockServer's own settings (names starting with mockserver. or MOCKSERVER_, in any case) and the javax.net.ssl key and trust store passwords are never readable, whatever the list says; an entry that covers them is reported when MockServer starts, as is what templates can read. PUT /mockserver/configuration cannot change the list and logs a warning if asked to. Anyone who can create expectations, call them or read the log can read the values of the names you list, so list only values that are safe to share. Templates that do not use systemValue run as before. See templateSystemValueAllowList
  • Kotlin DSL, like WireMock's kotlin-wiremock: the new org.mock-server:mockserver-client-kotlin artifact (also in mockserver-bom) adds Kotlin builders and extension functions to MockServerClient and ClientAndServer, for example mockServer.expect { request { method = "GET"; path = "/x" } respond { status = 200; body = "ok" } }. An expectation can respond, forward or error, with times, timeToLive, priority and id; requests match on method, path, headers, query and path parameters, cookies, an exact, JSON or any Java body matcher, and customize { } reaches every other Java builder method. verify(times) { } and verifySequence { } verify requests, and clear { }, retrieveRecordedRequests { } and retrieveActiveExpectations { } take the same request block; standalone request { }, response { } and expectation { } builders return Java objects for any other client method. It brings in kotlin-stdlib 2.4.20 and changes nothing for Java users. See Kotlin DSL
  • In-process mode with no HTTP server, like WireMock's DirectCallHttpServer: new InProcessMockServer() (in mockserver-core) handles requests as Java calls, for hosts such as AWS Lambda or a test of an HTTP adapter. handle(HttpRequest) returns the HttpResponse, and handleAsync(HttpRequest) returns a CompletableFuture. Requests go through the same matching and actions as a running MockServer, so expectations, templates, delays, forwards (through MockServer's own HTTP client and any configured forwardHttpProxy, forwardHttpsProxy or forwardSocksProxy), class callbacks, verification and the log behave the same. Most of the REST control plane works through handle too, with control-plane authentication as configured; routes only the Netty server serves (/mockserver/configuration, /mockserver/ready, /mockserver/metrics, the dashboard) answer 404. upsert, verify, retrieveRecordedRequests, reset and getHttpState() give Java access to the control plane; they are in-JVM calls and are not authenticated, so if handle receives untrusted traffic, enable control-plane authentication or drop /mockserver/ paths first. handle blocks the caller for a response delay or a forward; handleAsync does not. An unmatched request is answered 404 and never proxied to the host in its Host header. Object callbacks (which need a WebSocket; use a class callback), SSE, WebSocket and gRPC streaming, and connection faults are not available. close() stops every thread it started (a delayed response still pending completes when its delay elapses), and every method then throws IllegalStateException. The Netty server is unchanged. See running in-process
  • Raw network traffic logging, like WireMock's --print-all-network-traffic, for debugging only: start MockServer with logRawNetworkTraffic=true to log every read and write on each connection to MockServer, and on the HTTP requests it sends itself (forwards, proxied requests, webhooks), at INFO on the org.mockserver.logging.RawNetworkTrafficLog logger. HTTP/1.1 is shown as text after TLS decryption, HTTP/2 as decoded frames and other protocols as a hex dump, at most 4,096 bytes per read or write, with line breaks and control characters escaped so traffic cannot forge log lines. Authorization, Proxy-Authorization, cookies, API-key and other credential headers are masked, but bodies, query strings and hex dumps (including their text column) are logged as sent and may contain secrets, so do not use it in production. It can only be set at startup (PUT /mockserver/configuration ignores it with a warning), and when it is off nothing is added to a connection. Not logged: HTTP/3, the upstream connections of proxied WebSockets and relayed binary connections, requests between cluster members, and metrics export.
  • Date-time request matchers, like WireMock's before, after and equalToDateTime. Give a header, cookie, query-string or path-parameter value as an object such as {"after": "now -1 hours"} (Java client: withHeader(string("X-Issued-At"), NottableDateTimeString.after("now -1 hours"))) and the request value is compared as a date-time. The expected value is now, now with an offset (now -1 hours, now +3 days, now PT30M) or an ISO-8601, RFC-1123 or epoch date-time. Optional fields set the request value's format (actualFormat: a pattern such as dd/MM/yyyy, or iso, rfc1123, epochSeconds, epochMillis), truncate either side (truncateExpected, truncateActual, such as first day of month or startOfDay), truncate now after its offset (applyTruncationLast), set the time zone used for values without one, truncation and calendar offsets (zone, default UTC) and negate the match (not). now follows the controllable clock. Parsing is strict, so 30 February never matches, and a request value that is not a date-time never matches, even when negated. An invalid matcher is rejected when the expectation is created, with the reason. Plain string values are unchanged, so a value that happens to read before:2026-01-01 still matches that text, and expectations without date-time matchers do no extra work. Every client library models the object form and round-trips it: Go mockserver.After("now -1 hours") with HeaderMatcher and the other *Matcher builders; Rust MatcherValue::after(..) with header_matcher and the others; .NET MatcherValue.After(..); Python DateTimeMatcher.after(..); Ruby MockServer::DateTimeMatcher.after(..); PHP DateTimeMatcher::after(..) with the new headerMatcher, queryStringParameterMatcher, cookieMatcher and pathParameter builders; and Node typings (DateTimeMatcher). Each has with... options matching Java's, and plain string values are sent unchanged. The dashboard composer can edit a date-time value, keeps any object-form value it does not edit, and its code tabs generate these builders. The WireMock importer now maps before, after and equalToDateTime header and query predicates (with expectedOffset, actualFormat and truncation) onto them. A failed expectation parse now also says why. See date-time matching
  • Date-time matching inside JSON and XML bodies, like WireMock's matchesJsonPath and matchesXPath with before, after or equalToDateTime. A JSON_PATH or XPATH body matcher takes an optional dateTime field holding the same date-time matcher as a header value, for example {"type": "JSON_PATH", "jsonPath": "$.order.created", "dateTime": {"after": "now -1 days"}} (Java client: jsonPath("$.order.created", after("now -1 days")) or xpath("/order/@expires", after("now"))). The body then matches when a value the JSONPath selects, or the text of a node the XPath selects, is a matching date-time; when the expression selects several, one is enough. A JSON string is read as written and a whole number as epoch seconds or millis; an XPath must select nodes (elements or attributes). A body without the value, or whose value is not a date-time, does not match, even when the comparison is negated. JSONPath and XPath bodies without dateTime run exactly as before, with no extra allocation. Every client library supports it: Go JSONPathBody(..).WithDateTime(mockserver.After(..)), Rust Body::json_path(..).with_date_time(MatcherValue::after(..)), .NET Body.OfJsonPath(..).WithDateTime(MatcherValue.After(..)), Python Body.json_path(.., DateTimeMatcher.after(..)) and XPathBody(date_time=..), Ruby Body.json_path(.., date_time: ..), PHP HttpRequest::jsonPathBody(.., DateTimeMatcher::after(..)), the new xpathBody and matcherBody, and Node typings (JsonPathBodyMatcher, XPathBodyMatcher). The dashboard composer has a Date-Time Comparison field for JSON Path and XPath bodies, and its code tabs generate these builders. The WireMock importer now maps matchesJsonPath and matchesXPath with a before, after or equalToDateTime result matcher onto them, instead of dropping the comparison with a warning. See date-time matching in a body
  • Webhooks: a before- or after-action can now render its request with a Velocity, Mustache or JavaScript template, using httpRequestTemplate in place of httpRequest (Java client: afterAction().withHttpRequestTemplate(template(...)); also modelled by the .NET (AfterAction.HttpRequestTemplate), Go (AfterAction.HttpRequestTemplate), Node (AfterAction.httpRequestTemplate type), Python (AfterAction(http_request_template=...)), Ruby (AfterAction.new(http_request_template: ...)) and Rust (ExpectationAction::request_template) clients, kept verbatim by the PHP client, and offered as a Request template option for before/after actions in the dashboard composer and its code tabs). The template sees the triggering request with the same variables and helpers as response templates and must render a whole request; the rendered destination is checked by forwardProxyBlockPrivateNetworks when it is enabled (it is off by default), and a template that fails is logged and not sent (a blocking FAIL_FAST before-action returns 502). Escape request values in the template (Velocity $esc.java / $esc.url) and keep them out of the destination, or a caller can redirect the webhook. Webhooks using httpRequest and {$request...} runtime expressions are unchanged.
  • Response templates: the dates helper now works on any date and in any time zone, not only on the current time in UTC. dates.parse reads ISO-8601, RFC-1123, epoch seconds or millis, or text in a given pattern; dates.format(date, pattern, zone) formats any date; dates.offset moves a date by amounts such as "-2 hours" or an ISO-8601 period such as P1M; dates.truncate (and methods such as startOfDay(), firstDayOfMonth(), lastDayOfMonth() and firstDayOfNextYear()) moves it to the start of a period in its own time zone; dates.now(zone) gives the current time in a zone; and dates.rfc1123() writes the HTTP date format for headers such as Expires and Last-Modified. The returned dates can be chained, for example $dates.parse($!request.headers['Date'][0]).plusHours(1).rfc1123(). Mustache, which cannot pass arguments, gets the no-argument forms ({{ dates.now.firstDayOfNextMonth.rfc1123 }}) and a {{#dates.render}}date|offset=1 days|format=yyyy-MM-dd{{/dates.render}} section. The current time still comes from the server clock, so clock control applies, and the existing dates methods return exactly what they did before. An invalid date, pattern, time zone, offset or truncation fails the template with an error naming the value
  • A FILE response body can now pick its file from the request, like WireMock's templated bodyFileName. Set "templatedFilePath": true with a templateType of MUSTACHE or VELOCITY, and filePath is rendered against the request before the file is read, for example "filePath": "responses/{{ request.pathParameters.name.0 }}.json". The request-supplied part of the path must stay inside the directory named by the fixed part of filePath before the first template expression, and that fixed part must name a directory (not just /, ./ or a drive). A .. or . segment, a leading or doubled /, \, :, %, a control character such as a null byte, or a symbolic link out of that directory is refused with the same 500 as a file that cannot be read. This is opt-in: without templatedFilePath, or when the path has no template syntax, the path is used exactly as before. Every client library supports it: Java FileBody.withTemplatedFilePath(true), Node typings templatedFilePath, Python Body.file(..., templated_file_path=True), Ruby Body.file(..., templated_file_path: true), Go FileBody.TemplatedFilePath and ResponseBuilder.BodyFromTemplatedFile, .NET FileBody.TemplatedFilePath and WithTemplatedFileBody, Rust Body::with_templated_file_path(true) and PHP fileBody(..., true); each omits the field when it is false, so existing expectations serialise unchanged. The dashboard composer has a matching option on a FILE body, and its generated code carries the flag in every language
  • Response templates: the request model in Velocity, Mustache and JavaScript now has formData (the fields of an application/x-www-form-urlencoded body), parts (the parts of a multipart/form-data body, each with name, filename, contentType, headers, body and bodyAsBase64), host, port, scheme and baseUrl (from the Host header or HTTP/2 and HTTP/3 :authority, else the local address), bodyAsBase64, and id (the request's log correlation id). Each is worked out only when a template reads it, so other templates and non-templated responses cost nothing extra; a part's body and bodyAsBase64 are built only when read, so reading an upload's filename does not copy its content. Form and multipart decoding is bounded (1,000 fields; 100 parts; 10 MiB, separate from maxRequestBodySize), hitting a bound logs a warning that a template dry run reports, and a malformed body gives an empty map rather than an error. host, port and baseUrl come from the client-set Host header; X-Forwarded-* headers are not used. In JavaScript these fields are not enumerated, so Object.keys(request) and JSON.stringify(request) are unchanged.
  • Recording: three new options on retrieving recorded expectations (PUT /mockserver/retrieve?type=RECORDED_EXPECTATIONS), matching WireMock's recording options. repeatsAsScenarios=true turns a request that was recorded several times with different responses into one expectation that replays those responses in the recorded order and then keeps returning the last one; recordings count as the same request only when every header and other matched part is equal, and a request that got the same response every time is left as it was. captureHeaders=X-Tenant,Accept keeps only the named request headers in each recorded expectation (an empty value keeps none). extractBodiesLargerThan=<bytes> writes each larger response body to a file and returns a FILE body that points at it; files are written only to the directory set by the new recordedBodyFilesDirectory property (empty by default, which disables this and makes such a retrieve a 400; it can only be set at startup), are named after a SHA-256 of their contents, never after anything in the request, are never deleted by MockServer, and are capped at 1,000 files, 256 MB and maxFutureTimeoutInMillis of writing per retrieve. With control-plane authorization on, a retrieve that writes files needs the MUTATE role. Extracted bodies are written exactly as recorded, secrets included (redactSecretsInRecordedExpectations does not redact bodies), so use a private directory and review its files before sharing them. Each option is off unless given, and only changes the returned expectations, not the recorded traffic. The Java client takes them as retrieveRecordedExpectations(request, recordedExpectationsOptions().withRepeatsAsScenarios(true)...), which also carries the existing consolidate and parameterize options. See recording options
  • Response templates: new helpers matching WireMock's pickRandom, array, arrayJoin, range, formatXml, hostname and transformerParameters, and DataFaker locales. The lists helper has pickRandom, array, arrayJoin and range (Velocity $lists.pickRandom('a', 'b'), JavaScript lists.range(1, 5), Mustache {{#lists.pickRandom}}a|b{{/lists.pickRandom}} and {{#lists.range.1.5}}{{.}}{{/lists.range.1.5}}); pickRandom follows templateFakerSeed, and a list or range has at most 10,000 values. xpath.formatXml(xml) (Mustache {{#xpath.formatXml}}{{{ request.body }}}{{/xpath.formatXml}}) pretty-prints XML with the same DOCTYPE-refusing parser as the rest of MockServer, and fails on XML nested over 256 deep or output over four times its input (and over 64 KiB). hostname is the server's host name: the HOSTNAME environment variable when set (such as the pod name), otherwise looked up once in the background, never on the request path; it shows callers the server's internal name. faker.locale('de-DE') (Mustache {{ faker.locale.de-DE.name.fullName }}) gives DataFaker data for a locale; only the locales DataFaker ships are used (an unsupported country falls back to the language, an unsupported language to English with a warning), so a locale from request data such as Accept-Language cannot grow memory. arrayJoin output is capped at 64 KiB or four times its values. A template can now carry parameters, a JSON object it reads as parameters.* ($parameters.greeting, {{ parameters.greeting }}, parameters.greeting), in response, forward, forward-response and webhook templates; it is in the JSON schema and the OpenAPI spec, and every client sets it: Java template(...).withParameter("greeting", "hello") / withParameters(map), .NET HttpTemplate.OfType(...).WithParameter(...) / WithParameters(...), Go TemplateBuilder.Parameter(...) / Parameters(...) (HttpTemplate.Parameters), Node typings parameters, Python HttpTemplate(parameters=...) / with_parameter(...), Ruby HttpTemplate.new(parameters: ...) / with_parameter(...), PHP HttpTemplate::withParameter(...) / withParameters(...), and Rust Expectation::respond_template_with_parameters(...) / forward_template_with_parameters(...) and HttpOverrideForwardedRequest::response_template_with_parameters(...), read back with response_template_parameters() (the Rust HttpTemplate struct is unchanged, so code that builds it as a struct literal still compiles; the parameters are kept in the owning struct's extra map, so a retrieved expectation keeps them when it is upserted again); each leaves the field out when unset. The dashboard composer has a Parameters field on response and forward templates, keeps a loaded template's parameters, and every code tab writes them. Templates that use none of these, and responses that are not templated, run as before: Velocity and Mustache hold the new helpers in their shared bindings, and JavaScript binds them only for a template that names them
  • Spring tests: @MockServerInstance starts several named MockServers for one test class, like WireMock's @ConfigureWireMock. Repeat it once per service; each instance gets its own free port and sets mockserver.<name>.port and mockserver.<name>.baseUrl in the Spring Environment, plus any property names you list in portProperties or baseUrlProperties (these override application.properties and @TestPropertySource). A MockServerClient or ClientAndServer field annotated @InjectMockServer("<name>") receives that instance's client. initializationJsonPath loads expectations into one instance from a file, classpath resource, glob or file-system directory, and properties takes per-instance mockserver.* settings. Each instance is reset after every test, then restores the expectations it had at startup, and stops when the last Spring context using it closes (usually when the JVM exits, or earlier with @DirtiesContext). Test classes that share an instance must not run in parallel in one JVM. @MockServerTest is unchanged and can be used alongside it. See several named MockServer instances
  • Proxying: two new properties restrict where MockServer may send requests, like WireMock's proxy target allow and deny lists. forwardProxyAllowedHosts lists the only hosts that forward actions, proxied requests, replays and webhooks may go to, and forwardProxyDeniedHosts lists hosts they must not go to; each is a comma-separated list of host names (api.example.com), domains (*.example.com, which also matches example.com), IP addresses and CIDR blocks (10.0.0.0/8). The deny list wins over the allow list. IP addresses and CIDR blocks are also matched against the address a name resolves to (an IPv4 address written as IPv6 is matched as IPv4, and a listed loopback address also covers ::1, 0.0.0.0 and ::), and where MockServer connects to the target itself it connects to the address it checked, so DNS rebinding cannot get round them; through an upstream proxy, which resolves the name itself, it can. A refused target is answered exactly as a forwardProxyBlockPrivateNetworks refusal is (502 for HTTP, a closed connection for binary and WebSocket proxying, an unsent webhook, and a warning). Both are fixed at startup: PUT /mockserver/configuration ignores them with a warning, so a control-plane request cannot widen them. An invalid entry is rejected when set, and one that arrives another way (an environment variable or properties file) makes MockServer refuse every target until it is fixed. Both are empty by default, which leaves forwarding exactly as before. They also apply to fetching an OpenAPI spec from a URL, its remote $refs and redirects; specs read from a file or the classpath are unaffected. See forwardProxyAllowedHosts
  • Request matching by client IP address, like WireMock's withClientIp: "clientIp": "10.0.0.0/8" matches requests whose client address is that IP address or falls in that CIDR range, IPv4 or IPv6, and "!10.0.0.0/8" matches every other address (Java withClientIp, .NET WithClientIp / ClientIp, Go ClientIP, Python with_client_ip / client_ip, Ruby with_client_ip / client_ip:, Rust client_ip (held in extra, so no public field changed), PHP clientIp, and the Node typings' clientIp). The client address is the connection's TCP peer, or the source a PROXY protocol header names when transparent proxying is on; it is never read from X-Forwarded-For. With transparent proxying on, any client can send a PROXY header, so clientIp is a matching criterion, not access control. The range is parsed once when the expectation is created and an invalid value is rejected with the reason. Until the first clientIp is applied in the JVM (the switch covers every MockServer instance in the process and is never cleared, even by reset), requests record their address as before, with nothing extra allocated per request; each connection's request mapper is 8 bytes larger (72 rather than 64). After that, each connection keeps one 24-byte holder that also retains the peer's InetAddress (about 48 bytes for IPv4), and the address bytes (24 or 32 bytes) are derived on the first comparison. Matching adds no parsing, DNS lookup or allocation per request, and expectations without a clientIp run unchanged. It also filters verification and retrieved requests. The dashboard composer has a Client IP field, and its code generators emit each client's setter. Because the match diagnostics count every match field, totalFieldCount and matchedFieldCount in debugMismatch, explain and the 404 closest-match diagnostic are one higher (19 rather than 18); the ranking of expectations is unchanged. An expectation rejected by a model validation now reports the reason instead of only "exception while parsing".
  • Expectation metadata, and retrieving and clearing expectations by metadata: an expectation can carry a metadata object of any labels you want (string keys, any JSON values, such as an owner, a ticket number or tags), set when it is created and returned when it is retrieved. It never affects matching, and it is left out of the per-request match log. PUT /mockserver/retrieve?type=active_expectations and PUT /mockserver/clear accept metadataKey (optionally with metadataValue) and metadataJsonPath to select expectations by their metadata; a text value is compared as text and any other value as JSON. In the Java client, use withMetadata(...) when creating an expectation, retrieveActiveExpectationsByMetadata(...), clearByMetadata(...), clearByMetadataKey(...), and the new ExpectationFilter. The Node, Python, Ruby, Go, .NET, Rust and PHP clients support metadata, retrieving and clearing by metadata or unmatched, and paging too, under their own naming (for example clear_by_metadata in Python and Ruby, ClearByMetadata in Go and .NET); each refuses a blank key, a null value or a clear that selects nothing before sending anything. The dashboard shows an expectation's metadata, keeps it when you edit the expectation, and includes it in the generated code. See creating expectations
  • Unused expectations: unmatched=true on PUT /mockserver/retrieve?type=active_expectations returns only the expectations that have never matched a request since they were created or last changed, and on PUT /mockserver/clear removes them. Each MockServer instance keeps its own count. Java client: retrieveUnmatchedExpectations() and clearUnmatchedExpectations(). Like a namespace clear, a metadata or unmatched clear removes expectations only and leaves the event log as it is, and every filter you give (namespace, request matcher or id, metadata, unmatched) must match. An empty filter value (such as metadataKey=), unmatched=false on its own, a filter given twice, or a name that starts with metadata or unmatched but is not a filter (such as metadata_key) is rejected with 400 instead of clearing everything. Filter names are not case-sensitive; other unknown parameters are ignored, as elsewhere. Filter values are used literally, so a leading ! or ? is not a not or optional marker. A clear or retrieve whose query string cannot be decoded (for example a bare %) is now rejected with 400 rather than read as having no parameters, and so is a clear with an empty namespace=. See clearing and resetting
  • Paging: offset and limit on PUT /mockserver/retrieve for active_expectations (highest priority first, then oldest first) and recorded_expectations (in the order received), applied after every other filter and for every format. Java client: retrieveActiveExpectations(request, ExpectationFilter) and retrieveRecordedExpectations(request, offset, limit). An invalid value, or one of these parameters on a type it does not apply to, is rejected with 400. See retrieving active expectations
  • Static files: the new staticFilesDirectory property serves a directory of files for GET and HEAD requests that match no expectation and are not proxied, like WireMock's __files directory. A request for /assets/site.css gets assets/site.css from that directory, with a Content-Type from the file extension; a path ending in / gets that folder's index.html, and a HEAD request gets the file's Content-Length without the body. Expectations always win, and anything not served (a missing file, a folder, another method, a /mockserver/ control-plane path, or a file over 50 MiB) still gets the usual 404. Files are read on a small pool of their own, never on a network thread; with more than 1,000 reads waiting, further requests get a 503 (when MockServer runs as a WAR, files are read on the servlet container's request thread and this limit does not apply). Only plain paths inside the directory are served: .., encoded or double-encoded separators, backslashes, drive letters, control characters, hidden files and folders (names starting with .) and symbolic links out of the directory are refused, and folders are never listed. It is empty by default, which leaves unmatched requests exactly as before, and it is fixed at startup: PUT /mockserver/configuration ignores it with a warning. See staticFilesDirectory
  • Request matching with logical OR, like WireMock's or: an httpRequest holding an anyOf list matches a request when any one of its request matchers does, tried in order and stopping at the first match; "not": true beside it matches when none does. Each entry can be the usual request fields, an OpenAPI matcher, an if/then/else conditional or another anyOf, and a conditional's branches can now be an anyOf. It works for expectations, verification and retrieving recorded requests, and an empty anyOf is rejected. Every client builds it: Java requestAnyOf(...), .NET HttpRequest.RequestAnyOf / WithAnyOf, Go AnyOfRequest / RequestBuilder.AnyOf, Python AnyOfRequestDefinition, Ruby AnyOfRequestDefinition, Rust HttpRequest::any_of (held in extra, so no public field changed), PHP HttpRequest::anyOf, and the Node typings' AnyOfRequestDefinition. When a request matches none of the entries, explain, explainUnmatched and debugMismatch give the expectation's differences for the closest entry and add an anyOf object naming it (closestBranch, counted from 0) with each entry's differing fields; the Python, Ruby, Go, .NET and PHP diagnostics models read it, and the dashboard's mismatch dialog shows it. The dashboard composer keeps an anyOf (and an if/then/else) request matcher exactly as loaded when an expectation is edited, rather than adding form fields beside it that the server would reject; its code tabs write the branches in every language, and its match preview evaluates each one. Expectations without anyOf are matched and indexed exactly as before; an anyOf expectation is checked for every request (like a conditional one) and allocates nothing beyond its entries. See any of request matcher
  • Extensions, like WireMock's ExtensionFactory: your own Java classes implementing org.mockserver.extension.Extension that MockServer loads when it starts. Name them in the new extensions property (--extensions on the command line, MOCKSERVER_EXTENSIONS, -Dmockserver.extensions, @MockServerTest / per-instance @MockServerInstance properties) or pass classes or suppliers to Configuration.extensions(...) when embedded; extensionScanning=true also loads those listed in META-INF/services files. Each extension starts before MockServer builds anything else, is told the bound ports once they are bound (/mockserver/ready waits for it), and is stopped in reverse order when MockServer stops; an extension that also implements ExpectationInitializer seeds expectations at startup. A class that cannot be loaded, a duplicate or invalid name, or a start or onStarted that throws or exceeds extensionStartTimeoutMillis (default 30 seconds) refuses start-up and stops the extensions already started. The three properties are fixed at startup: PUT /mockserver/configuration ignores them with a warning, so the control plane can never load code. With no extension configured, nothing is scanned or loaded and request handling is unchanged. Works in the Netty server, both WARs (where onStarted gets no ports) and embedded use. See extensions
  • Extension admin routes, like WireMock's AdminApiExtension: an extension that also implements org.mockserver.extension.AdminApiExtension answers every request under /mockserver/extensions/<name>/. The routes are part of the control plane: the same authentication, browser-origin check, CORS headers and audit log apply, and with controlPlaneAuthorizationEnabled every route needs the role that may change state (GET included) unless the extension's isReadOnly says it only reads. The extension's code never runs on a network thread or MockServer's shared scheduler: at most extensionAdminMaxInFlight calls (default 32) run at once on a pool of their own, and a call beyond that, including one an extension makes back into MockServer, gets 503 (counted as extension_admin in mock_server_overload_rejections); a call that has not answered within extensionAdminTimeoutMillis (default 30 seconds) gets one 500 and is interrupted, and its late result is discarded. A call made while MockServer is stopping gets 503 and never reaches the extension. A thrown exception gets 500 with a correlation id. In a WAR the call runs on the request thread, with the limit but no timeout. Only loaded extension names are taken, so any other path under /mockserver/extensions/ behaves as before; an expectation loaded at start-up for a path under a loaded name logs a warning, because it no longer matches. Both properties are fixed at startup. With no admin extension loaded, nothing is built and request handling is unchanged. See serving your own control-plane routes
  • Extension listeners, like WireMock's ServeEventListener, StubLifecycleListener and GlobalSettingsListener: an extension that implements ServeEventListener is told about each request MockServer answered (mocked, forwarded or not matched, with the request, the response, the expectation id and when; bodies cut to maxLoggedBodyBytes); one that implements ExpectationLifecycleListener is told about each expectation added, updated or removed and why (API, FILE_INITIALISER, CLASS_INITIALISER, FILE_WATCHER, EXHAUSTED, EXPIRED, EVICTED or CLUSTER; an expectation used up across a cluster is EXHAUSTED and marked fleet-wide); and one that implements SettingsListener is told about each PUT /mockserver/configuration. Each listener extension runs on its own thread, so a slow or stuck listener never slows MockServer: events wait in a queue bounded by the new extensionListenerQueueSize (default 10,000) and, for served requests and expectation changes, by extensionListenerMaxBytes (default about 5% of the heap, shared between those listeners, and weighed by the bodies and expectations the events hold), and anything that does not fit is dropped and counted in mock_server_extension_listener_dropped_total without affecting the event log or verification. Both properties are fixed at startup. When MockServer stops, queued events are delivered for up to stopDrainMillis before the extensions stop. With no listener loaded, nothing changes: each removal or change of an expectation costs one check and allocates nothing. See listening to what MockServer does
  • Extension template helpers, like WireMock's TemplateHelperProviderExtension: an extension that implements org.mockserver.extension.TemplateHelperProvider adds its own helpers to response and forward templates under ext.<extension name> — $ext.acme.Strings.upper($request.path) in Velocity, ext.acme.Strings.upper(request.path) in JavaScript (the methods of a public interface you choose), and {{#ext.acme.upper}}{{request.path}}{{/ext.acme.upper}} in Mustache, whose helper receives the section after it is rendered. Each call runs on a helper thread of its own, never on a network thread; all the calls one template makes share one budget, the new extensionHelperTimeoutMillis (default 1 second), after which a running call is interrupted and the template fails as any failing template does; at most extensionHelperMaxInFlight (default 32) calls run at once, and one over the limit fails at once. Only httpResponseTemplate and httpForwardTemplate can call helpers: a webhook, secondary action, delay, file body, stream, load scenario or dry-run template that calls one, or a mistyped helper name, fails with an error instead of rendering as written. Helpers work over HTTP/1.1, HTTP/2 and HTTP/3 alike. Both properties are fixed at startup. Without such an extension, templates are unchanged and do no extra work. See template helpers
  • Extension request filters (experimental), like WireMock's request filters: an extension that implements org.mockserver.extension.RequestFilter sees every request that reaches expectation matching, over HTTP/1.1, HTTP/2 and HTTP/3 (plain and gRPC), in the WAR deployments and in InProcessMockServer (handle and handleAsync), and can let it through unchanged (FilterResult.proceed()), change it (proceedWith(request)), or answer it (respond(response)). It never sees control-plane requests, a CORS preflight for a control-plane path, requests refused by data-plane authentication, requests answered over HTTP/1.1 by a respondBeforeBody expectation, gRPC bidirectional streams answered by a grpcBidiResponse expectation, or the built-in gRPC health, reflection and chaos-profile answers, so a filter is not an access control: use dataPlaneAuthenticationRequired for that. It does see a browser's CORS preflight for a mocked endpoint (FilterContext.isCorsPreflight()), an unserved /mockserver/... path, and, over HTTP/3, /mockserver/status and /mockserver/ready, which HTTP/3 answers by expectation matching. Filters run on their own threads (extensionDataPlaneMaxInFlight, default 32) with a time budget (extensionDataPlaneTimeoutMillis, default 1000 ms), never on the threads that serve connections; a gRPC call's grpc-timeout includes the time its filter takes. A filter that throws, times out, returns null, moves a request onto the control plane or changes a gRPC request's method gets a 500 naming the extension unless its failOpen() is true (after a timeout, only if every later filter also fails open, and the later filters do not run); the event log records the received request once (as filtered), an INFO entry when a filter changed it, and a WARN for each failure. Pipelined HTTP/1.1 responses stay in order. A filter may change where a proxied request goes; forwardProxyBlockPrivateNetworks and the forward host allow and deny lists apply to it only when they are turned on. A request whose filter is still running when MockServer stops is answered 503 at once over HTTP/3, when a WAR is undeployed and when an InProcessMockServer is closed; over HTTP/1.1 and HTTP/2 the graceful stop drain (stopDrainMillis) lets it finish first, and it gets the 503 only if it outlasts the drain. Either way the filter threads end. The website's Extensions page shows how to write and load one, and examples/java has a runnable filter. With no such extension loaded nothing changes
  • InProcessMockServer now loads extensions configured with extensions (or configuration().extensions(...)); before, it ignored them. They start when the instance is created and stop in reverse order on close() or a refused start; onStarted gets an empty list of ports. Admin routes are reached by passing /mockserver/extensions/<name>/... requests to handle, with the same control-plane authentication as other REST requests, and run on the calling thread with no extensionAdminTimeoutMillis. Served-request and expectation listeners and $ext template helpers work as on a running server; a settings listener is never called, because PUT /mockserver/configuration is not available in-process. With no extensions configured nothing changes.
  • Extensions in Docker and Helm: every MockServer image now loads any JAR in /extensions, last on its classpath, so docker run -v $(pwd)/ext:/extensions:ro -e MOCKSERVER_EXTENSIONS=com.example.AuditExtension mockserver/mockserver adds extensions without building an image, in every variant including -graaljs, -clustered and -aot (use it instead of /libs, which those images fill themselves). Start-up is unchanged when nothing is mounted, and the images' start-up archives still apply. The Helm chart's new extensions.volume value mounts any volume (a ConfigMap, Secret, PersistentVolumeClaim, ...) read-only at /extensions, and extensions.classes and extensions.scanning set the two properties. MockServer now refuses to start when an enabled output file (persisted expectations, recorded expectations or recorded requests, the audit log or the memory-usage CSV files) would be written into the folder an extension was loaded from. See adding extensions
  • Extension response transformers (experimental), like WireMock's ResponseTransformerV2: an extension that implements org.mockserver.extension.ResponseTransformer sees each response MockServer sends over HTTP/1.1, HTTP/2 or HTTP/3, from the WAR deployments and from InProcessMockServer (mocked, forwarded and proxied responses, and the responses MockServer makes itself, such as its 404, overload 503, breakpoint aborts, rate-limit 429, chaos errors, failed templates and failed forwards, which it can tell apart with TransformContext.isGenerated()) and can return a changed one. It is given a copy, so a change it makes in place reaches nothing else. When it changes the body, a Content-Length it left unchanged is recalculated, but one it sets itself is sent as it is, so a wrong one breaks the response and its keep-alive connection; a forwarded body is as the upstream sent it, so a transformer that replaces a compressed body must also set or remove Content-Encoding. CORS and defaultResponseHeaders are added after it, so it cannot remove them; the event log, verification and after-actions still see the response before it, so an after-action or webhook can fire for a response a failing transformer then replaces with a 500. A streamed response is shown head only. A transformer that throws, returns null or takes longer than extensionDataPlaneTimeoutMillis (1 s by default) has its response replaced by a 500, unless its failOpen() returns true (after a timeout, only if every later transformer also fails open, and the later transformers do not run). It does not see control-plane responses, MockServer's own answers to CORS preflights, interim (1xx) responses, SSE, WebSocket and gRPC streams, or the answers to requests a request filter never sees (such as respondBeforeBody expectations). A gRPC unary response is shown as JSON, before it is encoded, and over HTTP/3 a grpc-timeout that elapses while the transformer runs still ends the call with DEADLINE_EXCEEDED. In a WAR and in InProcessMockServer.handle, the request's own thread waits for the transformer, for at most the timeout. The website's Extensions page shows how to write and load one, and examples/java has a runnable transformer. A server without such an extension is unchanged
  • Control-plane HTTP Basic authentication, alongside mTLS, JWT and OIDC. Set controlPlaneBasicAuthenticationRequired=true and configure users with controlPlaneBasicAuthenticationCredentials (username:passwordHash[:scopes], separated by ;) or controlPlaneBasicAuthenticationUsersFile (one per line, the format htpasswd -B writes). Passwords are only accepted as bcrypt, argon2 or PBKDF2 hashes, never in plain text, with bounded work factors (bcrypt cost 4 to 16, PBKDF2 10,000 to 10,000,000 iterations, argon2 at most 256 MiB, 16 passes and 16 lanes; a warning names any below OWASP guidance), and are checked in constant time, taking the same time whether or not the user exists. Basic credentials are refused without TLS unless controlPlaneBasicAuthenticationAllowInsecure is set, which is for tests only. After 5 wrong passwords from one address that address must wait 1 second, doubling up to 60 seconds, before its credentials are checked again, at most 4,096 addresses are tracked, and a user who already logged in is not locked out; clients behind one shared address share the wait. Basic is an alternative to the other methods: with mTLS, JWT or OIDC also enabled, a request is accepted with valid Basic credentials or by passing the other methods. The user name is the audited principal (verified-basic), the scopes configured for a user are mapped to roles by controlPlaneScopeMapping, a request refused over HTTPS gets a WWW-Authenticate: Basic challenge (never over plain HTTP), the dashboard, callback WebSocket and MCP endpoints (HTTP/1.1, HTTP/2 and HTTP/3) are checked the same way, and failed attempts are logged without the credentials. Authentication and authorization failure log entries now mask Authorization, Proxy-Authorization, cookie and API-key headers for every mechanism, including JWT bearer tokens. The settings are fixed at startup, the credentials are never returned by GET /mockserver/configuration and are masked in the printed configuration, and MockServer refuses to start when Basic is enabled with no users or a plain-text password. Every client can send the credentials on its HTTP requests and on its callback and breakpoint WebSockets: Java withControlPlaneBasicAuthentication(username, password), Node basicAuthUsername / basicAuthPassword options, Python and Ruby control_plane_basic_auth, Go WithControlPlaneBasicAuthentication, .NET WithControlPlaneBasicAuthentication, Rust control_plane_basic_authentication and PHP withControlPlaneBasicAuthentication (HTTP requests only, as the PHP client has no WebSockets); a bearer token, when also configured, is sent instead. Each client warns when Basic credentials are configured on a plain HTTP connection, and the Java client no longer puts the Authorization header in a ClientException message. With the property off, control-plane authentication is unchanged. See control plane Basic authentication
  • Every client library can now shape retrieved recorded expectations with the recording options added to the Java client: repeatsAsScenarios, captureHeaders and extractBodiesLargerThan, plus the existing consolidate and parameterize. Node passes an options object, retrieveRecordedExpectations(path, {repeatsAsScenarios: true, captureHeaders: ['X-Tenant'], extractBodiesLargerThan: 1024}), typed as RecordedExpectationsOptions; Python retrieve_recorded_expectations(options=RecordedExpectationsOptions().with_repeats_as_scenarios()), sync and async, and Ruby retrieve_recorded_expectations(options: RecordedExpectationsOptions.new...) combine it with offset and limit; Go RetrieveRecordedExpectationsWithOptions, .NET RetrieveRecordedExpectations(request, RecordedExpectationsOptions.Options()...) and its async form, Rust retrieve_recorded_expectations_with_options and PHP retrieveRecordedExpectationsWithOptions are new methods, so existing calls are unchanged. Each client sends an option only when it is set (a boolean only when true), sends an empty header list as captureHeaders= (which keeps no headers), and rejects a blank header name, a name containing a comma or a negative byte threshold before sending anything. See recording options
  • Differential corpus tests for the data plane: golden response bytes, frames and serialised log entries for HTTP/3, gRPC over HTTP/2 and HTTP/3, the WAR servlet and InProcessMockServer, compared byte for byte, so an internal change that alters what MockServer sends or logs fails the build. CI fails if the HTTP/3 corpus is skipped.

Changed

  • HTTP/3 now works in the standard Docker image (mockserver/mockserver:<version>, snapshot) and the shaded -no-dependencies jars: set http3Port and publish that UDP port. The standard image keeps the QUIC native library in /usr/lib, so this also works with a read-only root filesystem; the -graaljs, clustered- and -aot images serve HTTP/3 too, but need a writable /tmp. The -http3 image tags (<version>-http3, latest-http3, snapshot-http3) and the Helm value image.variant=http3 are deprecated: they are now the same as the standard image and are kept for this release only, so switch to the standard tag. The start-up message when the native library is missing now names the standard image (#2869)
  • Java model: Body has a new copy() method, and HttpRequest and HttpResponse a new deepCopy(), which request filter and response transformer extensions use to give each extension its own copy of a request or response. Every built-in body implements copy(). A custom subclass of Body must override copy() to be used with those extensions; otherwise copying it throws UnsupportedOperationException. Nothing else calls them, so nothing changes for a server without such an extension.
  • forwardProxyBlockPrivateNetworks (SSRF protection) can now only be set at startup, like forwardProxyAllowedHosts and forwardProxyDeniedHosts. Before, PUT /mockserver/configuration (or a client's updateConfiguration) could switch it off on a running server, so anyone who could reach the control plane could turn the protection off. A PUT that sets a different value is still answered, but the value is ignored and a warning is logged; set it with a system property, environment variable, properties file or Configuration instead (see upgrading to 9)
  • Java client: clear(expectationId), clear(expectationId, type) and clearByNamespace(namespace) now throw IllegalArgumentException for a null or blank id or namespace, instead of sending a request that cleared every expectation. clear((RequestDefinition) null) still clears everything, as documented
  • WireMock import (PUT /mockserver/import?format=wiremock) now maps more WireMock constructs onto MockServer features instead of warning: matchesXPath (with xPathNamespaces and an equalTo or contains result matcher) and equalToXml become XPath and XML body matchers; binaryEqualTo a binary body matcher; urlPathTemplate a {name} path whose names match any one segment unless pathParameters constrains them; delayDistribution (uniform, lognormal with the same median and 99th percentile, fixed) a delay distribution, with fixedDelayMilliseconds added to it as WireMock does; chunkedDribbleDelay a chunked response with a delay between chunks; bodyFileName a file body, read from the WireMock __files directory given by the new filesDirectory query parameter (a path on the machine running MockServer; without it the stub still gets a warning); a response-template transformer whose template uses only simple request references (request.path, request.query.x, request.headers.X, request.pathSegments.[n] and similar) a Mustache response template; and postServeActions / serveEventListeners webhooks after-actions. The EMPTY_RESPONSE fault closes the connection, and MALFORMED_RESPONSE_CHUNK and RANDOM_DATA_THEN_CLOSE now send WireMock's bytes before closing it, without a warning; CONNECTION_RESET_BY_PEER resets the connection with a TCP reset (the new resetConnection error action), without a warning. Handlebars helpers, triple-brace output, custom transformers, stub metadata and the per-stub persistent flag are reported as warnings. Secret redaction also covers the headers and body of a translated response template. See importing expectations

Fixed

  • A control-plane request that MockServer handles off its network threads, made while MockServer is stopping, now gets 503; before, it could get no response at all. These are OpenAPI imports, PUT /mockserver/contractTest, PUT /mockserver/trafficValidate, a retrieve with extractBodiesLargerThan, and any request that names an OpenAPI spec. A verification with a timeout made at that moment now answers at once, instead of possibly never
  • Event log entries written after a breakpoint resumes a paused exchange (for example the template output and the response returned) now always record the server's port; before, whether they did depended on which thread resumed the exchange
  • zstd now works in the Docker images and the mockserver-netty-no-dependencies jar, the only -no-dependencies jar that bundles zstd-jni (#2869). These jars rename the libraries they bundle, and they renamed zstd-jni, whose native library only works under its original class names. So a request with Content-Encoding: zstd, or a forwarded request or response in zstd, closed the connection with UnsatisfiedLinkError. zstd-jni now keeps its own names, as snappy-java and lz4-java already did. A check of everything else the renaming touches found three more problems in the same jars and images, now fixed: the bundled QUIC native library never loaded, so setting http3Port stopped MockServer starting from these jars (the -http3 image was not affected); the -no-dependencies jars other than mockserver-netty-no-dependencies never loaded their bundled OpenSSL library, so they always used the JDK's TLS; and system properties for the bundled libraries, such as -Dio.netty.allocator.type or -Dio.netty.leakDetection.level, were ignored when MockServer was started from the command line or in Docker. The build now runs the jars to check that their native libraries load and work. In these jars and images the bundled libraries' logger names start with shaded_package., such as shaded_package.io.netty.handler.ssl.SslHandler
  • HTTP/3: sseResponse expectations and streamed llmResponse expectations ("streaming": true) now work over HTTP/3. The client used to get no response at all: the stream was closed before any headers were sent. Each event is now sent as soon as it is due, and an event's delay does not hold up other HTTP/3 requests. A webSocketResponse expectation matched over HTTP/3 is now answered 501 Not Implemented with a message, and a warning is logged, instead of the stream being closed without a response: a WebSocket over HTTP/3 needs RFC 9220 extended CONNECT, which MockServer does not support, so send WebSocket requests over HTTP/1.1
  • HTTP/3: a delayed response no longer holds up every other HTTP/3 request. The delay used to be waited out on the thread that serves all HTTP/3 connections, so a fast request queued behind a slow one, on the same connection or another. Delays (an expectation's delay, globalResponseDelayMillis and chaos latency), forwarded responses, callbacks, templates and MCP requests are now handled off that thread, as they already were for HTTP/1.1 and HTTP/2. Request and response breakpoints now also pause HTTP/3 requests, and extension template helpers ($ext) work with HTTP/3 enabled. As on HTTP/1.1 and HTTP/2, an HTTP/3 request can now be refused with 503 when too many delayed responses (maxPendingDelayedResponses) or template renders (maxQueuedTemplateActions) are already waiting, counted in mock_server_overload_rejections
  • An HTTP request that matches an expectation whose action is a binary response (respondWithBinary) or a DNS response (respondWithDns) is now answered 501 Not Implemented, saying that action cannot answer an HTTP request, and a warning is logged; previously the client got no response and waited for ever, as did any request pipelined behind it on the same connection. Those expectations still answer binary and DNS traffic as before
  • HTTP/1.1 requests pipelined on one connection (sent without waiting for the previous response) are now answered in the order they were sent, as RFC 9112 requires. Before, a fast response could be written ahead of a slower one sent before it (a delayed, forwarded, templated, callback, file, static-file, breakpoint or control-plane response), so the client paired each response with the wrong request; a HEAD response could be framed against another request's body; and a request pipelined behind an SSE stream, a chunk-delayed response, or PUT /mockserver/bind with no body got no response at all. MockServer now handles one request on a connection at a time: a pipelined request waits, unprocessed and unrecorded, until the response to the request ahead of it has been written. What changes for a pipelining client: requests on one connection are no longer handled at the same time, so their total time is the sum of their delays; a request behind a paused breakpoint or a stream is recorded only at its turn; requests already waiting when the connection is closing (Connection: close, closeSocket, a drop or reset) are matched and recorded when it closes but not answered, where before they were often handled at the same time as the request ahead and answered before the close; a waiting request whose body is larger than maxRequestBodySize gets 413 and the connection is closed, because reads are paused while requests wait; waiting requests count as in flight, so a graceful stop waits for them too (and drops any still waiting when stopDrainMillis runs out) and the preemption simulator counts them; a waiting request whose body is already complete has its Expect header removed (it is not needed and would desynchronise the connection), so the recorded request has no Expect; a connection holding pipelined requests behind a response that makes no progress for inboundConnectionIdleTimeoutMillis is closed; and a client that pipelines an upload larger than maxRequestBodySize behind a large response without reading that response, from a single thread, is now closed by responseWriteStallTimeoutMillis, as MockServer stops reading the upload. Concurrent MCP requests on one connection no longer overwrite each other's CORS headers. HTTP/2, HTTP/3 and clients that do not pipeline (browsers, curl and most HTTP libraries by default) are unaffected, and the extra per-request work for them is a few field checks with no allocation. The new property http1PipeliningInOrder (default true) turns this off and restores the previous behaviour; it is read when a connection opens and will be removed in the next minor release. See http1PipeliningInOrder
  • TCP chaos faults resetPeer and resetMidResponse now reach the client as a connection reset over TLS and through a CONNECT or SOCKS tunnel. Over TLS the server sent a TLS close before the reset, and through a tunnel the client's own connection was closed cleanly, so in both cases the client saw a normal end of the connection instead of "connection reset". Through a tunnel, resetMidResponse still delivers the response before the reset
  • WAR deployment (mockserver-war and mockserver-proxy-war): forwarded and proxied requests now go through the upstream proxy set by forwardHttpProxy, forwardHttpsProxy or forwardSocksProxy, with forwardProxyAuthenticationUsername and forwardProxyAuthenticationPassword, as they do on the Netty server. The servlets ignored these settings and connected directly. As on the Netty server, configuring a SOCKS proxy together with an HTTP or HTTPS proxy now stops the servlet from starting
  • WAR deployment: a query string containing a literal ? or / (both legal in a query, and sent unencoded by curl, for example ?q=why?&page=2) lost every parameter before it, so matching saw the wrong query parameters and a control-plane call saw the wrong options. The servlet mapper now decodes the whole query string exactly as the Netty server does
  • Imports from WireMock, Mountebank and Mockoon: secret redaction (on by default) no longer breaks request body matchers. It turned JSON matchers into exact string matchers, so a WireMock equalToJson stub matched only byte-identical JSON, and it replaced JsonPath, XPath and regex matchers with their own description, so those matched nothing. JSON, substring, XML and binary matchers now keep their type. Redacted JSON fields inside them are masked, and an XML or non-JSON matcher that contains a redacted field name is replaced by a placeholder. XPath, JsonPath and regex expressions, and exact or substring matchers that start with a quoted string or a number followed by more text (such as "api_key":"abc"), are kept as written. A WireMock import warns about each masked matcher, which then matches only the placeholder, and about each kept expression or fragment that contains a redacted field name. A WireMock matchesXPath equalTo or contains value is masked when the expression names a redacted field. Redaction no longer drops after-actions or response templates, and no longer inlines file bodies. See sensitive data redaction
  • The response templates page's first example (echo a path parameter back) now works. In every language tab it matched no request, because /users/{id} did not declare pathParameters; its template also used id.[0], which Mustache renders as empty, and rendered a body rather than a response, which the server rejects with a 500
  • Multipart body matching no longer leaks memory when a request's multipart/form-data body is truncated or has more than 128 parts. The part being decoded when decoding stopped was never released.
  • Performance: a matched expectation whose response body is a FILE body no longer reads the file on a network thread, where a slow disk held up every other connection on that thread. The file is now read on the small pool that staticFilesDirectory already uses (created only when first needed, so a server that never serves a file starts no threads), at the cost of one hand-off between threads for these responses only; responses without a FILE body run exactly as before. Response bytes, templated bodies and paths, and the 500 for a missing or refused file are unchanged. With more than 1,000 file reads waiting, further FILE responses get a 503 with Retry-After: 1, counted as file_bodies in mock_server_overload_rejections. As with delayed, templated and forwarded responses, a pipelined HTTP/1.1 request sent after one may now be answered first. When MockServer runs as a WAR the file is still read on the servlet container's request thread
  • PUT /mockserver/bind with an empty body, or a body of null, is now answered 400 with invalid or missing port binding. It was not answered at all, so the client waited until its own timeout
  • A response with closeSocket set to true now says Connection: close instead of Connection: keep-alive when the request asked to keep the connection alive. A client told the connection would stay open could send its next request into the close and see it fail. Set keepAliveOverride to true to keep announcing keep-alive (for example, to test how a client handles a server that closes a connection it said it would keep), or suppressConnectionHeader to send no Connection header
  • An HTTP response that MockServer fails to write (for example one its encoder cannot encode, including the head of a response sent in delayed chunks) now closes the connection. Before, a keep-alive connection stayed open with nothing more to come, so the client waited until its own timeout. A connection whose client has reset it is still read to the end, as before, so a request the client sent before it left is still recorded
  • A connection whose reads were paused (by TCP chaos latency or bandwidth, pending WebSocket replies or relay backpressure) when MockServer started a lingering close or a connection reset stopped reading for good once the pause ended. The client's remaining request bytes were never read, so after 5 seconds the connection was reset, which can discard a response the client has not read yet. Reading now resumes when the pause ends
  • Dashboard: editing an expectation in the Mocks composer no longer loses settings the form cannot show. A response or error delay with a distribution or template is kept, and its fixed-delay field is disabled until you remove that delay under Other fields: MockServer uses a distribution instead of the fixed value, and uses the fixed value only as a template's fallback, so typing one replaced either. Setting a connection option keeps a response's chunkSize and chunkDelay. The Java and other code previews no longer show a Content-Type header for a loaded response that has none, which the save did not send. New / clear, and Add another after registering a mock, now start a blank mock of the selected kind (a static response for HTTP and MCP, a stream for gRPC, a DNS response for DNS), instead of keeping the previous expectation's action settings, such as Reset connection or template parameters. Switching the kind of a mock whose action you have not chosen now gives the new kind's default the same way, so a new gRPC mock starts as a stream rather than a static HTTP response; an action you picked, or one loaded from an existing mock, is kept when the new kind allows it