Repository navigation
·
28 commits
to master
since this release
[9.2.0] - 2026-10-26
Security
- In the
mockserver-netty-no-dependenciesjar and the Docker images, the bundled Kafka client used for AsyncAPI mocking now refuses theJndiLoginModuleandLdapLoginModuleJAAS login modules in asaslJaasConfig, 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
dataPlaneAuthenticationRequiredon, 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/statusand/readyover HTTP/3), skipped the check and was answered by any matching expectation, a catch-all one included. Over HTTP/1.1, an expectation withrespondBeforeBodyanswered 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 theUNIMPLEMENTEDanswer for an unknown gRPC method, which told the caller which methods exist. All of these are now refused (401, or gRPC statusUNAUTHENTICATED) like any other mocked endpoint. A refused request does not use up the expectation'sTimes, 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 dataPlaneAuthenticationRequirednow applies to the WAR deployments (mockserver-war,mockserver-proxy-war) and toInProcessMockServer, 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 noTimesand 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,tlsMutualAuthenticationRequiredis now enforced too: a mocked request over plain HTTP gets426 Upgrade Required, and one over HTTPS without a client certificate verified by the servlet container gets401; 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
asyncKafkaSaslJaasConfigproperty (-Dmockserver.asyncKafkaSaslJaasConfigorMOCKSERVER_ASYNC_KAFKA_SASL_JAAS_CONFIG). A JAAS configuration names the Java login class the Kafka client loads, andPUT /mockserver/asyncapilet any control-plane caller choose it throughbrokerConfig.kafkaSecurity.saslJaasConfig. That request is now rejected with a400that names the startup property, and so is any other field inkafkaSecuritythat MockServer does not know, so no Kafka client setting that loads a class can be passed in;saslMechanismandsecurityProtocolmust be one of Kafka's own values. A request that selectsSASL_PLAINTEXTorSASL_SSLuses 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 startupasyncKafkaBootstrapServers: it must omitkafkaBootstrapServersor name the same brokers, or it gets a400and no credential is sent, and it gets a400while no startup bootstrap servers are set.asyncKafkaBootstrapServerscan now only be set at startup:PUT /mockserver/configurationignores it and logs a warning.PUT /mockserver/configurationignoresasyncKafkaSaslJaasConfigand logs a warning, andGET /mockserver/configurationnever returns it. To upgrade, move thesaslJaasConfigvalue from your requests toasyncKafkaSaslJaasConfig, setasyncKafkaBootstrapServersat startup to the brokers it logs in to, and keepsecurityProtocolandsaslMechanismin the request (see upgrading to 9) retrievewithforwardUnmatchedTonow 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 withcontrolPlaneAuthorizationEnableda principal with only theREADrole could redirect unmatched traffic. Such a principal now gets403, as it already did forextractBodiesLargerThan; aretrievewithout 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
undici7.30.1 (Dependabot alerts 610, 611, 612; the published package ships onlydist), and the VS Code extension's development dependency@vscode/vscenow resolves@azure/msal-node7.1 through an npmoverridesentry (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
protocexecutable it runs, can now only be set at startup:auditLogFile,persistedExpectationsPath,persistedRecordedExpectationsPath,persistedRecordedRequestsPath,memoryUsageCsvDirectoryanddirectoryToSaveDynamicSSLCertificate, likerecordedBodyFilesDirectory. Before,PUT /mockserver/configuration(or a client'supdateConfiguration) 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 aformat=recordingdisk import (which readspersistedRecordedRequestsPath) at another file.grpcProtocPath, which names theprotocexecutable MockServer runs, is now startup-only for the same reason. APUTthat sets a different value is still answered, but the value is ignored and a warning is logged; aPUTthat sends the value already in force logs nothing. Set them with a system property, environment variable, properties file orConfigurationinstead (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, thecontrolPlaneJWTAuthentication*andcontrolPlaneOidc*settings,controlPlaneAuthorizationEnabled,controlPlaneScopeMapping,controlPlaneAuditEnabledanddashboardAnalyticsEndpoint, like thecontrolPlaneBasicAuthentication*settings. Before, anyone allowed to change the configuration withPUT /mockserver/configurationcould 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. APUTthat sets a different value is still answered, but the value is ignored and a warning is logged, andGET /mockserver/configurationreports the value in force; the dashboard's Audit Trail view no longer has an on/off switch (see upgrading to 9) - A
PUT /mockserver/configurationthat changes where the default LLM backend sends requests (llmBaseUrl, orllmProviderwhilellmBaseUrlis empty) without also supplyingllmApiKeynow 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 pointingllmBaseUrlat their own server. APUTthat supplies both applies both; a JSONllmBackendsConfigvalue that changes a backend'sbaseUrlorproviderwhile 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.
clusterVerifyFanInPeersandclusterFanInPeerAuthTokencan now only be set at startup:PUT /mockserver/configurationcould 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. APUTthat supplies a different value is ignored and logs a warning. Thedetect_llm_driftMCP tool, which a read-only caller may use, now refuses a cassette whoseprovideris not the configured LLM backend's provider and sends nothing; before, it replayed the cassette with that provider's client, which, when nollmBaseUrlwas set, sent the heldllmApiKeyto that provider's default host. AnAZURE_OPENAIbackend with nollmBaseUrlnow sends nothing, instead of sending its key to the placeholder hostexample-resource.openai.azure.com. See upgrading to 9. noProxyHostscan now only be set at startup, like the upstream proxies it applies to (forwardHttpProxy,forwardHttpsProxy,forwardSocksProxy). Before, anyone allowed to change the configuration withPUT /mockserver/configurationcould add hosts to it and so route forwarded traffic around an upstream proxy relied on for logging or data-loss checks. APUTthat sets a different value is still answered, but the value is ignored and a warning is logged, andGET /mockserver/configurationreports 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,
Authorizationheader 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 itsAuthorization,Proxy-Authorization, cookie and API-key headers masked, whateverredactSecretsInLogsays; 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": trueanswers a matched request with a TCP reset instead of a response, so the client sees "connection reset by peer" (ECONNRESET), wheredropConnectioncloses 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 (usestreamErrorto reset one request); over HTTP/3, which has no TCP reset, the QUIC connection is closed withH3_INTERNAL_ERROR. Through a CONNECT or SOCKS tunnel the client's own connection is reset. WireMock and MountebankCONNECTION_RESET_BY_PEERfaults now import asresetConnection. Not available in the WAR deployment, like every error action. See error actions - Response templates: a
systemValuehelper reads an environment variable or a system property, like WireMock'ssystemValue: Velocity$systemValue.env('APP_REGION')and$systemValue.property('app.version'), JavaScriptsystemValue.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-onlytemplateSystemValueAllowListproperty, like WireMock's--permitted-system-keys: a comma-separated list of exact names or prefixes ending in*, such asAPP_*,HOSTNAME. The list is empty by default, sosystemValuereturns 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 withmockserver.orMOCKSERVER_, in any case) and thejavax.net.sslkey 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/configurationcannot 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 usesystemValuerun as before. See templateSystemValueAllowList - Kotlin DSL, like WireMock's
kotlin-wiremock: the neworg.mock-server:mockserver-client-kotlinartifact (also inmockserver-bom) adds Kotlin builders and extension functions toMockServerClientandClientAndServer, for examplemockServer.expect { request { method = "GET"; path = "/x" } respond { status = 200; body = "ok" } }. An expectation canrespond,forwardorerror, withtimes,timeToLive,priorityandid; requests match on method, path, headers, query and path parameters, cookies, an exact, JSON or any Java body matcher, andcustomize { }reaches every other Java builder method.verify(times) { }andverifySequence { }verify requests, andclear { },retrieveRecordedRequests { }andretrieveActiveExpectations { }take the same request block; standalonerequest { },response { }andexpectation { }builders return Java objects for any other client method. It brings inkotlin-stdlib2.4.20 and changes nothing for Java users. See Kotlin DSL - In-process mode with no HTTP server, like WireMock's
DirectCallHttpServer:new InProcessMockServer()(inmockserver-core) handles requests as Java calls, for hosts such as AWS Lambda or a test of an HTTP adapter.handle(HttpRequest)returns theHttpResponse, andhandleAsync(HttpRequest)returns aCompletableFuture. 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 configuredforwardHttpProxy,forwardHttpsProxyorforwardSocksProxy), class callbacks, verification and the log behave the same. Most of the REST control plane works throughhandletoo, with control-plane authentication as configured; routes only the Netty server serves (/mockserver/configuration,/mockserver/ready,/mockserver/metrics, the dashboard) answer404.upsert,verify,retrieveRecordedRequests,resetandgetHttpState()give Java access to the control plane; they are in-JVM calls and are not authenticated, so ifhandlereceives untrusted traffic, enable control-plane authentication or drop/mockserver/paths first.handleblocks the caller for a response delay or a forward;handleAsyncdoes not. An unmatched request is answered404and never proxied to the host in itsHostheader. 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 throwsIllegalStateException. 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 withlogRawNetworkTraffic=trueto log every read and write on each connection to MockServer, and on the HTTP requests it sends itself (forwards, proxied requests, webhooks), atINFOon theorg.mockserver.logging.RawNetworkTrafficLoglogger. 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/configurationignores 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,afterandequalToDateTime. 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 isnow,nowwith 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 asdd/MM/yyyy, oriso,rfc1123,epochSeconds,epochMillis), truncate either side (truncateExpected,truncateActual, such asfirst day of monthorstartOfDay), truncatenowafter its offset (applyTruncationLast), set the time zone used for values without one, truncation and calendar offsets (zone, default UTC) and negate the match (not).nowfollows 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 readbefore:2026-01-01still matches that text, and expectations without date-time matchers do no extra work. Every client library models the object form and round-trips it: Gomockserver.After("now -1 hours")withHeaderMatcherand the other*Matcherbuilders; RustMatcherValue::after(..)withheader_matcherand the others; .NETMatcherValue.After(..); PythonDateTimeMatcher.after(..); RubyMockServer::DateTimeMatcher.after(..); PHPDateTimeMatcher::after(..)with the newheaderMatcher,queryStringParameterMatcher,cookieMatcherandpathParameterbuilders; and Node typings (DateTimeMatcher). Each haswith...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 mapsbefore,afterandequalToDateTimeheader and query predicates (withexpectedOffset,actualFormatand 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
matchesJsonPathandmatchesXPathwithbefore,afterorequalToDateTime. AJSON_PATHorXPATHbody matcher takes an optionaldateTimefield 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"))orxpath("/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 withoutdateTimerun exactly as before, with no extra allocation. Every client library supports it: GoJSONPathBody(..).WithDateTime(mockserver.After(..)), RustBody::json_path(..).with_date_time(MatcherValue::after(..)), .NETBody.OfJsonPath(..).WithDateTime(MatcherValue.After(..)), PythonBody.json_path(.., DateTimeMatcher.after(..))andXPathBody(date_time=..), RubyBody.json_path(.., date_time: ..), PHPHttpRequest::jsonPathBody(.., DateTimeMatcher::after(..)), the newxpathBodyandmatcherBody, 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 mapsmatchesJsonPathandmatchesXPathwith abefore,afterorequalToDateTimeresult 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
httpRequestTemplatein place ofhttpRequest(Java client:afterAction().withHttpRequestTemplate(template(...)); also modelled by the .NET (AfterAction.HttpRequestTemplate), Go (AfterAction.HttpRequestTemplate), Node (AfterAction.httpRequestTemplatetype), 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 byforwardProxyBlockPrivateNetworkswhen it is enabled (it is off by default), and a template that fails is logged and not sent (a blockingFAIL_FASTbefore-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 usinghttpRequestand{$request...}runtime expressions are unchanged. - Response templates: the
dateshelper now works on any date and in any time zone, not only on the current time in UTC.dates.parsereads ISO-8601, RFC-1123, epoch seconds or millis, or text in a given pattern;dates.format(date, pattern, zone)formats any date;dates.offsetmoves a date by amounts such as"-2 hours"or an ISO-8601 period such asP1M;dates.truncate(and methods such asstartOfDay(),firstDayOfMonth(),lastDayOfMonth()andfirstDayOfNextYear()) moves it to the start of a period in its own time zone;dates.now(zone)gives the current time in a zone; anddates.rfc1123()writes the HTTP date format for headers such asExpiresandLast-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 existingdatesmethods 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
FILEresponse body can now pick its file from the request, like WireMock's templatedbodyFileName. Set"templatedFilePath": truewith atemplateTypeofMUSTACHEorVELOCITY, andfilePathis 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 offilePathbefore 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 same500as a file that cannot be read. This is opt-in: withouttemplatedFilePath, or when the path has no template syntax, the path is used exactly as before. Every client library supports it: JavaFileBody.withTemplatedFilePath(true), Node typingstemplatedFilePath, PythonBody.file(..., templated_file_path=True), RubyBody.file(..., templated_file_path: true), GoFileBody.TemplatedFilePathandResponseBuilder.BodyFromTemplatedFile, .NETFileBody.TemplatedFilePathandWithTemplatedFileBody, RustBody::with_templated_file_path(true)and PHPfileBody(..., 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
requestmodel in Velocity, Mustache and JavaScript now hasformData(the fields of anapplication/x-www-form-urlencodedbody),parts(the parts of amultipart/form-databody, each withname,filename,contentType,headers,bodyandbodyAsBase64),host,port,schemeandbaseUrl(from theHostheader or HTTP/2 and HTTP/3:authority, else the local address),bodyAsBase64, andid(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'sbodyandbodyAsBase64are built only when read, so reading an upload'sfilenamedoes not copy its content. Form and multipart decoding is bounded (1,000 fields; 100 parts; 10 MiB, separate frommaxRequestBodySize), 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,portandbaseUrlcome from the client-setHostheader;X-Forwarded-*headers are not used. In JavaScript these fields are not enumerated, soObject.keys(request)andJSON.stringify(request)are unchanged. - Recording: three new options on retrieving recorded expectations (
PUT /mockserver/retrieve?type=RECORDED_EXPECTATIONS), matching WireMock's recording options.repeatsAsScenarios=trueturns 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,Acceptkeeps 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 aFILEbody that points at it; files are written only to the directory set by the newrecordedBodyFilesDirectoryproperty (empty by default, which disables this and makes such a retrieve a400; 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 andmaxFutureTimeoutInMillisof 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 (redactSecretsInRecordedExpectationsdoes 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 asretrieveRecordedExpectations(request, recordedExpectationsOptions().withRepeatsAsScenarios(true)...), which also carries the existingconsolidateandparameterizeoptions. See recording options - Response templates: new helpers matching WireMock's
pickRandom,array,arrayJoin,range,formatXml,hostnameandtransformerParameters, and DataFaker locales. Thelistshelper haspickRandom,array,arrayJoinandrange(Velocity$lists.pickRandom('a', 'b'), JavaScriptlists.range(1, 5), Mustache{{#lists.pickRandom}}a|b{{/lists.pickRandom}}and{{#lists.range.1.5}}{{.}}{{/lists.range.1.5}});pickRandomfollowstemplateFakerSeed, 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).hostnameis the server's host name: theHOSTNAMEenvironment 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 asAccept-Languagecannot grow memory.arrayJoinoutput is capped at 64 KiB or four times its values. A template can now carryparameters, a JSON object it reads asparameters.*($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: Javatemplate(...).withParameter("greeting", "hello")/withParameters(map), .NETHttpTemplate.OfType(...).WithParameter(...)/WithParameters(...), GoTemplateBuilder.Parameter(...)/Parameters(...)(HttpTemplate.Parameters), Node typingsparameters, PythonHttpTemplate(parameters=...)/with_parameter(...), RubyHttpTemplate.new(parameters: ...)/with_parameter(...), PHPHttpTemplate::withParameter(...)/withParameters(...), and RustExpectation::respond_template_with_parameters(...)/forward_template_with_parameters(...)andHttpOverrideForwardedRequest::response_template_with_parameters(...), read back withresponse_template_parameters()(the RustHttpTemplatestruct is unchanged, so code that builds it as a struct literal still compiles; the parameters are kept in the owning struct'sextramap, 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:
@MockServerInstancestarts several named MockServers for one test class, like WireMock's@ConfigureWireMock. Repeat it once per service; each instance gets its own free port and setsmockserver.<name>.portandmockserver.<name>.baseUrlin the SpringEnvironment, plus any property names you list inportPropertiesorbaseUrlProperties(these overrideapplication.propertiesand@TestPropertySource). AMockServerClientorClientAndServerfield annotated@InjectMockServer("<name>")receives that instance's client.initializationJsonPathloads expectations into one instance from a file, classpath resource, glob or file-system directory, andpropertiestakes per-instancemockserver.*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.@MockServerTestis 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.
forwardProxyAllowedHostslists the only hosts that forward actions, proxied requests, replays and webhooks may go to, andforwardProxyDeniedHostslists hosts they must not go to; each is a comma-separated list of host names (api.example.com), domains (*.example.com, which also matchesexample.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.0and::), 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 aforwardProxyBlockPrivateNetworksrefusal is (502for HTTP, a closed connection for binary and WebSocket proxying, an unsent webhook, and a warning). Both are fixed at startup:PUT /mockserver/configurationignores 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 (JavawithClientIp, .NETWithClientIp/ClientIp, GoClientIP, Pythonwith_client_ip/client_ip, Rubywith_client_ip/client_ip:, Rustclient_ip(held inextra, so no public field changed), PHPclientIp, 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 fromX-Forwarded-For. With transparent proxying on, any client can send a PROXY header, soclientIpis 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 firstclientIpis applied in the JVM (the switch covers every MockServer instance in the process and is never cleared, even byreset), 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'sInetAddress(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 aclientIprun 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,totalFieldCountandmatchedFieldCountindebugMismatch,explainand 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
metadataobject 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_expectationsandPUT /mockserver/clearacceptmetadataKey(optionally withmetadataValue) andmetadataJsonPathto select expectations by their metadata; a text value is compared as text and any other value as JSON. In the Java client, usewithMetadata(...)when creating an expectation,retrieveActiveExpectationsByMetadata(...),clearByMetadata(...),clearByMetadataKey(...), and the newExpectationFilter. 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 exampleclear_by_metadatain Python and Ruby,ClearByMetadatain 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=trueonPUT /mockserver/retrieve?type=active_expectationsreturns only the expectations that have never matched a request since they were created or last changed, and onPUT /mockserver/clearremoves them. Each MockServer instance keeps its own count. Java client:retrieveUnmatchedExpectations()andclearUnmatchedExpectations(). 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 asmetadataKey=),unmatched=falseon its own, a filter given twice, or a name that starts withmetadataorunmatchedbut is not a filter (such asmetadata_key) is rejected with400instead 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 with400rather than read as having no parameters, and so is a clear with an emptynamespace=. See clearing and resetting - Paging:
offsetandlimitonPUT /mockserver/retrieveforactive_expectations(highest priority first, then oldest first) andrecorded_expectations(in the order received), applied after every other filter and for every format. Java client:retrieveActiveExpectations(request, ExpectationFilter)andretrieveRecordedExpectations(request, offset, limit). An invalid value, or one of these parameters on a type it does not apply to, is rejected with400. See retrieving active expectations - Static files: the new
staticFilesDirectoryproperty serves a directory of files forGETandHEADrequests that match no expectation and are not proxied, like WireMock's__filesdirectory. A request for/assets/site.cssgetsassets/site.cssfrom that directory, with aContent-Typefrom the file extension; a path ending in/gets that folder'sindex.html, and aHEADrequest gets the file'sContent-Lengthwithout 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 usual404. 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 a503(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/configurationignores it with a warning. See staticFilesDirectory - Request matching with logical OR, like WireMock's
or: anhttpRequestholding ananyOflist matches a request when any one of its request matchers does, tried in order and stopping at the first match;"not": truebeside it matches when none does. Each entry can be the usual request fields, an OpenAPI matcher, anif/then/elseconditional or anotheranyOf, and a conditional's branches can now be ananyOf. It works for expectations, verification and retrieving recorded requests, and an emptyanyOfis rejected. Every client builds it: JavarequestAnyOf(...), .NETHttpRequest.RequestAnyOf/WithAnyOf, GoAnyOfRequest/RequestBuilder.AnyOf, PythonAnyOfRequestDefinition, RubyAnyOfRequestDefinition, RustHttpRequest::any_of(held inextra, so no public field changed), PHPHttpRequest::anyOf, and the Node typings'AnyOfRequestDefinition. When a request matches none of the entries,explain,explainUnmatchedanddebugMismatchgive the expectation's differences for the closest entry and add ananyOfobject 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 ananyOf(and anif/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 withoutanyOfare matched and indexed exactly as before; ananyOfexpectation 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 implementingorg.mockserver.extension.Extensionthat MockServer loads when it starts. Name them in the newextensionsproperty (--extensionson the command line,MOCKSERVER_EXTENSIONS,-Dmockserver.extensions,@MockServerTest/ per-instance@MockServerInstanceproperties) or pass classes or suppliers toConfiguration.extensions(...)when embedded;extensionScanning=truealso loads those listed inMETA-INF/servicesfiles. Each extension starts before MockServer builds anything else, is told the bound ports once they are bound (/mockserver/readywaits for it), and is stopped in reverse order when MockServer stops; an extension that also implementsExpectationInitializerseeds expectations at startup. A class that cannot be loaded, a duplicate or invalid name, or astartoronStartedthat throws or exceedsextensionStartTimeoutMillis(default 30 seconds) refuses start-up and stops the extensions already started. The three properties are fixed at startup:PUT /mockserver/configurationignores 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 (whereonStartedgets no ports) and embedded use. See extensions - Extension admin routes, like WireMock's
AdminApiExtension: an extension that also implementsorg.mockserver.extension.AdminApiExtensionanswers 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 withcontrolPlaneAuthorizationEnabledevery route needs the role that may change state (GETincluded) unless the extension'sisReadOnlysays it only reads. The extension's code never runs on a network thread or MockServer's shared scheduler: at mostextensionAdminMaxInFlightcalls (default 32) run at once on a pool of their own, and a call beyond that, including one an extension makes back into MockServer, gets503(counted asextension_admininmock_server_overload_rejections); a call that has not answered withinextensionAdminTimeoutMillis(default 30 seconds) gets one500and is interrupted, and its late result is discarded. A call made while MockServer is stopping gets503and never reaches the extension. A thrown exception gets500with 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,StubLifecycleListenerandGlobalSettingsListener: an extension that implementsServeEventListeneris told about each request MockServer answered (mocked, forwarded or not matched, with the request, the response, the expectation id and when; bodies cut tomaxLoggedBodyBytes); one that implementsExpectationLifecycleListeneris told about each expectation added, updated or removed and why (API,FILE_INITIALISER,CLASS_INITIALISER,FILE_WATCHER,EXHAUSTED,EXPIRED,EVICTEDorCLUSTER; an expectation used up across a cluster isEXHAUSTEDand marked fleet-wide); and one that implementsSettingsListeneris told about eachPUT /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 newextensionListenerQueueSize(default 10,000) and, for served requests and expectation changes, byextensionListenerMaxBytes(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 inmock_server_extension_listener_dropped_totalwithout affecting the event log or verification. Both properties are fixed at startup. When MockServer stops, queued events are delivered for up tostopDrainMillisbefore 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 implementsorg.mockserver.extension.TemplateHelperProvideradds its own helpers to response and forward templates underext.<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 newextensionHelperTimeoutMillis(default 1 second), after which a running call is interrupted and the template fails as any failing template does; at mostextensionHelperMaxInFlight(default 32) calls run at once, and one over the limit fails at once. OnlyhttpResponseTemplateandhttpForwardTemplatecan 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.RequestFiltersees every request that reaches expectation matching, over HTTP/1.1, HTTP/2 and HTTP/3 (plain and gRPC), in the WAR deployments and inInProcessMockServer(handleandhandleAsync), 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 arespondBeforeBodyexpectation, gRPC bidirectional streams answered by agrpcBidiResponseexpectation, or the built-in gRPC health, reflection and chaos-profile answers, so a filter is not an access control: usedataPlaneAuthenticationRequiredfor that. It does see a browser's CORS preflight for a mocked endpoint (FilterContext.isCorsPreflight()), an unserved/mockserver/...path, and, over HTTP/3,/mockserver/statusand/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'sgrpc-timeoutincludes the time its filter takes. A filter that throws, times out, returnsnull, moves a request onto the control plane or changes a gRPC request's method gets a500naming the extension unless itsfailOpen()istrue(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), anINFOentry when a filter changed it, and aWARNfor each failure. Pipelined HTTP/1.1 responses stay in order. A filter may change where a proxied request goes;forwardProxyBlockPrivateNetworksand 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 answered503at once over HTTP/3, when a WAR is undeployed and when anInProcessMockServeris closed; over HTTP/1.1 and HTTP/2 the graceful stop drain (stopDrainMillis) lets it finish first, and it gets the503only if it outlasts the drain. Either way the filter threads end. The website's Extensions page shows how to write and load one, andexamples/javahas a runnable filter. With no such extension loaded nothing changes InProcessMockServernow loads extensions configured withextensions(orconfiguration().extensions(...)); before, it ignored them. They start when the instance is created and stop in reverse order onclose()or a refused start;onStartedgets an empty list of ports. Admin routes are reached by passing/mockserver/extensions/<name>/...requests tohandle, with the same control-plane authentication as other REST requests, and run on the calling thread with noextensionAdminTimeoutMillis. Served-request and expectation listeners and$exttemplate helpers work as on a running server; a settings listener is never called, becausePUT /mockserver/configurationis 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, sodocker run -v $(pwd)/ext:/extensions:ro -e MOCKSERVER_EXTENSIONS=com.example.AuditExtension mockserver/mockserveradds extensions without building an image, in every variant including-graaljs,-clusteredand-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 newextensions.volumevalue mounts any volume (a ConfigMap, Secret, PersistentVolumeClaim, ...) read-only at/extensions, andextensions.classesandextensions.scanningset 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 implementsorg.mockserver.extension.ResponseTransformersees each response MockServer sends over HTTP/1.1, HTTP/2 or HTTP/3, from the WAR deployments and fromInProcessMockServer(mocked, forwarded and proxied responses, and the responses MockServer makes itself, such as its404, overload503, breakpoint aborts, rate-limit429, chaos errors, failed templates and failed forwards, which it can tell apart withTransformContext.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, aContent-Lengthit 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 removeContent-Encoding. CORS anddefaultResponseHeadersare 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 a500. A streamed response is shown head only. A transformer that throws, returnsnullor takes longer thanextensionDataPlaneTimeoutMillis(1 s by default) has its response replaced by a500, unless itsfailOpen()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 asrespondBeforeBodyexpectations). A gRPC unary response is shown as JSON, before it is encoded, and over HTTP/3 agrpc-timeoutthat elapses while the transformer runs still ends the call withDEADLINE_EXCEEDED. In a WAR and inInProcessMockServer.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, andexamples/javahas a runnable transformer. A server without such an extension is unchanged - Control-plane HTTP Basic authentication, alongside mTLS, JWT and OIDC. Set
controlPlaneBasicAuthenticationRequired=trueand configure users withcontrolPlaneBasicAuthenticationCredentials(username:passwordHash[:scopes], separated by;) orcontrolPlaneBasicAuthenticationUsersFile(one per line, the formathtpasswd -Bwrites). 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 unlesscontrolPlaneBasicAuthenticationAllowInsecureis 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 bycontrolPlaneScopeMapping, a request refused over HTTPS gets aWWW-Authenticate: Basicchallenge (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 maskAuthorization,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 byGET /mockserver/configurationand 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: JavawithControlPlaneBasicAuthentication(username, password), NodebasicAuthUsername/basicAuthPasswordoptions, Python and Rubycontrol_plane_basic_auth, GoWithControlPlaneBasicAuthentication, .NETWithControlPlaneBasicAuthentication, Rustcontrol_plane_basic_authenticationand PHPwithControlPlaneBasicAuthentication(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 theAuthorizationheader in aClientExceptionmessage. 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,captureHeadersandextractBodiesLargerThan, plus the existingconsolidateandparameterize. Node passes an options object,retrieveRecordedExpectations(path, {repeatsAsScenarios: true, captureHeaders: ['X-Tenant'], extractBodiesLargerThan: 1024}), typed asRecordedExpectationsOptions; Pythonretrieve_recorded_expectations(options=RecordedExpectationsOptions().with_repeats_as_scenarios()), sync and async, and Rubyretrieve_recorded_expectations(options: RecordedExpectationsOptions.new...)combine it withoffsetandlimit; GoRetrieveRecordedExpectationsWithOptions, .NETRetrieveRecordedExpectations(request, RecordedExpectationsOptions.Options()...)and its async form, Rustretrieve_recorded_expectations_with_optionsand PHPretrieveRecordedExpectationsWithOptionsare 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 ascaptureHeaders=(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-dependenciesjars: sethttp3Portand 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-aotimages serve HTTP/3 too, but need a writable/tmp. The-http3image tags (<version>-http3,latest-http3,snapshot-http3) and the Helm valueimage.variant=http3are 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:
Bodyhas a newcopy()method, andHttpRequestandHttpResponsea newdeepCopy(), which request filter and response transformer extensions use to give each extension its own copy of a request or response. Every built-in body implementscopy(). A custom subclass ofBodymust overridecopy()to be used with those extensions; otherwise copying it throwsUnsupportedOperationException. Nothing else calls them, so nothing changes for a server without such an extension. forwardProxyBlockPrivateNetworks(SSRF protection) can now only be set at startup, likeforwardProxyAllowedHostsandforwardProxyDeniedHosts. Before,PUT /mockserver/configuration(or a client'supdateConfiguration) could switch it off on a running server, so anyone who could reach the control plane could turn the protection off. APUTthat 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 orConfigurationinstead (see upgrading to 9)- Java client:
clear(expectationId),clear(expectationId, type)andclearByNamespace(namespace)now throwIllegalArgumentExceptionfor 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(withxPathNamespacesand anequalToorcontainsresult matcher) andequalToXmlbecome XPath and XML body matchers;binaryEqualToa binary body matcher;urlPathTemplatea{name}path whose names match any one segment unlesspathParametersconstrains them;delayDistribution(uniform,lognormalwith the same median and 99th percentile,fixed) a delay distribution, withfixedDelayMillisecondsadded to it as WireMock does;chunkedDribbleDelaya chunked response with a delay between chunks;bodyFileNamea file body, read from the WireMock__filesdirectory given by the newfilesDirectoryquery parameter (a path on the machine running MockServer; without it the stub still gets a warning); aresponse-templatetransformer whose template uses only simple request references (request.path,request.query.x,request.headers.X,request.pathSegments.[n]and similar) a Mustache response template; andpostServeActions/serveEventListenerswebhooks after-actions. TheEMPTY_RESPONSEfault closes the connection, andMALFORMED_RESPONSE_CHUNKandRANDOM_DATA_THEN_CLOSEnow send WireMock's bytes before closing it, without a warning;CONNECTION_RESET_BY_PEERresets the connection with a TCP reset (the newresetConnectionerror action), without a warning. Handlebars helpers, triple-brace output, custom transformers, stubmetadataand the per-stubpersistentflag 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 withextractBodiesLargerThan, and any request that names an OpenAPI spec. A verification with atimeoutmade 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-dependenciesjar, the only-no-dependenciesjar 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 withContent-Encoding: zstd, or a forwarded request or response in zstd, closed the connection withUnsatisfiedLinkError. 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 settinghttp3Portstopped MockServer starting from these jars (the-http3image was not affected); the-no-dependenciesjars other thanmockserver-netty-no-dependenciesnever 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.typeor-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 withshaded_package., such asshaded_package.io.netty.handler.ssl.SslHandler - HTTP/3:
sseResponseexpectations and streamedllmResponseexpectations ("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. AwebSocketResponseexpectation matched over HTTP/3 is now answered501 Not Implementedwith 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,globalResponseDelayMillisand 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 with503when too many delayed responses (maxPendingDelayedResponses) or template renders (maxQueuedTemplateActions) are already waiting, counted inmock_server_overload_rejections - An HTTP request that matches an expectation whose action is a binary response (
respondWithBinary) or a DNS response (respondWithDns) is now answered501 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
HEADresponse could be framed against another request's body; and a request pipelined behind an SSE stream, a chunk-delayed response, orPUT /mockserver/bindwith 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 thanmaxRequestBodySizegets413and 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 whenstopDrainMillisruns out) and the preemption simulator counts them; a waiting request whose body is already complete has itsExpectheader removed (it is not needed and would desynchronise the connection), so the recorded request has noExpect; a connection holding pipelined requests behind a response that makes no progress forinboundConnectionIdleTimeoutMillisis closed; and a client that pipelines an upload larger thanmaxRequestBodySizebehind a large response without reading that response, from a single thread, is now closed byresponseWriteStallTimeoutMillis, 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 propertyhttp1PipeliningInOrder(defaulttrue) 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
resetPeerandresetMidResponsenow 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,resetMidResponsestill delivers the response before the reset - WAR deployment (
mockserver-warandmockserver-proxy-war): forwarded and proxied requests now go through the upstream proxy set byforwardHttpProxy,forwardHttpsProxyorforwardSocksProxy, withforwardProxyAuthenticationUsernameandforwardProxyAuthenticationPassword, 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
equalToJsonstub 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 WireMockmatchesXPathequalToorcontainsvalue 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 declarepathParameters; its template also usedid.[0], which Mustache renders as empty, and rendered a body rather than a response, which the server rejects with a500 - Multipart body matching no longer leaks memory when a request's
multipart/form-databody 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
FILEbody 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 thatstaticFilesDirectoryalready 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 aFILEbody run exactly as before. Response bytes, templated bodies and paths, and the500for a missing or refused file are unchanged. With more than 1,000 file reads waiting, furtherFILEresponses get a503withRetry-After: 1, counted asfile_bodiesinmock_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/bindwith an empty body, or a body ofnull, is now answered400withinvalid or missing port binding. It was not answered at all, so the client waited until its own timeout- A response with
closeSocketset totruenow saysConnection: closeinstead ofConnection: keep-alivewhen 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. SetkeepAliveOverridetotrueto keep announcingkeep-alive(for example, to test how a client handles a server that closes a connection it said it would keep), orsuppressConnectionHeaderto send noConnectionheader - 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
distributionortemplateis 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'schunkSizeandchunkDelay. The Java and other code previews no longer show aContent-Typeheader 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