chore(deps): update dependency org.mock-server:mockserver-netty-no-dependencies to v7.5.0 - #2354
Merged
renovate[bot] merged 1 commit intoJul 29, 2026
Conversation
…pendencies to v7.5.0
renovate
Bot
requested review from
dhoard,
fstab,
jaydeluca and
zeitlinger
as code owners
July 29, 2026 22:39
Contributor
Benchmark resultsBenchmark run finished with conclusion
Benchmark summary artifact was not found; see the workflow run for details. |
jaydeluca
approved these changes
Jul 29, 2026
renovate
Bot
deleted the
renovate/org.mock-server-mockserver-netty-no-dependencies-7.x
branch
July 29, 2026 23:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
7.4.0→7.5.0Release Notes
mock-server/mockserver-monorepo (org.mock-server:mockserver-netty-no-dependencies)
v7.5.0Security
template remote-code-execution path reported as
GHSA-7pwj-xvc2-hfpc.
A caller who can reach the management API can register an expectation, and a response template was able
to load
java.lang.Runtimeand execute OS commands in the MockServer process. Both engines that coulddo this are now sandboxed out of the box:
velocityDisallowClassLoadingnow defaults totrue(wasfalse), installing Velocity'sSecureUberspectorso a template cannot reach classes through$request.class.classLoader.loadClass(...).This is the more exposed half of the issue, and the half the report did not cover: Velocity ships in
the DEFAULT distribution, whereas the JavaScript engine does not.
empty
javascriptAllowedClassesand emptyjavascriptDisallowedClassesmeant unrestrictedJava.type(...)access; that combination — the out-of-the-box state — now denies every class.java.lang.Classorjava.lang.ClassLoader. Denying classes atJava.type(...)alone was not sufficient: real hostobjects are bound into the context (
fakerand the other built-in helpers), and under the previousHostAccess.ALLa template could walk from one of them to a classloader —faker.getClass().getClassLoader().loadClass('java.lang.Runtime')— reachingRuntimewithout theclass filter ever being consulted. That walk is now closed, so host-class lookup is the single complete
gate; a regression test drives four such walks (including through
request) and fails if any resolves.Velocity's
SecureUberspectoralready blocked the equivalent walk through its own bound helpers, whichis now covered by a test too.
Both flips are fully reversible with one property and remove no functionality: set
mockserver.velocityDisallowClassLoading=false, or list the classes your templates need inmockserver.javascriptAllowedClasses(the single entry*lets any class resolve again). Templates thatdo not touch Java classes are unaffected, which is the overwhelming majority — JavaScript templates have
the full ES2023 standard library available regardless of this setting. A refused class is logged once at
WARN naming the class and the property to set, because GraalJS otherwise surfaces a refusal only as the
class being undefined ("... is not a function"); the log is bounded and de-duplicated so a hostile
template cannot flood it.
mockserver.javascriptAllowedClassesis now also settable through the Springtest listener's
@MockServerTestproperties, which it was not before — it was a nice-to-have while thedefault was unrestricted, and is the only way to grant a class now that it is not. The insecure-mode WARN
now fires when an operator has explicitly opened the
sandbox rather than when it is closed. Proven end-to-end by a Netty integration test that registers the
reported payload through the real management API and asserts the OS command creates no marker file, with
a negative control on a deliberately unsandboxed server that DOES create it — so a regression cannot pass
as an inert payload. This lands DEF-2 and DEF-3 of
docs/plans/later/security-defaults.mdahead of theother default flips listed there; JavaScript went further than that plan proposed (deny everything, not a
built-in "safe types" allow-list) because deny-by-default is the only form that stays safe as the JDK
grows new reachable classes.
Fixed
(#2358). When a
mockserver.propertyFilean operator had explicitly configured could not be read, MockServer appliednone of its properties and said nothing about it — at any log level. The only symptom was that every
property in the file appeared to be at its default, which surfaces far downstream as unexplained
behaviour: in the reported case an unreadable (but present) mounted file meant
initializationJsonPathwas never set, so no expectations loaded, no
loading JSON initialization file:line appeared, and noerror was logged either. The message existed but was unreachable in practice — gated at DEBUG and
emitted during static initialisation, before any log level has been applied, so neither
-Dmockserver.logLevel=DEBUGnor a-logLevelargument could surface it. Such a file is now logged atWARN, naming the path and the underlying reason verbatim; because
FileNotFoundExceptioncovers "notthere" and "not allowed to read it" alike, that reason is usually the whole answer (
Permission deniedin the reported case, typically SELinux labelling or a rootless/user-namespace UID mismatch). A property
file that is merely absent at its default location stays quiet, as does the Docker image's built-in
-Dmockserver.propertyFile=/config/mockserver.properties, which the entrypoint always passes and whichtherefore expresses no intent — otherwise every container started without a mounted config would warn.
Inside the image, only
MOCKSERVER_PROPERTY_FILEcan express that intent, and it does.mockserver-nodelauncher suite no longer fails intermittently on a TLS handshake reset. Thetwo tests that exercise
jvmOptionsdid so over HTTPS against a server started withdynamicallyCreateCertificateAuthorityCertificate=true, and issued that HTTPS request as soon asstart_mockserverresolved.start_mockserveronly proves the HTTP control plane is answering — itpolls
PUT /mockserver/retrieveover plain HTTP — but with a dynamically created certificateauthority the server still has to generate a CA key pair and a leaf certificate before it can serve
TLS on that same (port-unified) port. A handshake arriving in that window was closed mid-negotiation
and surfaced as
ECONNRESET"Client network socket disconnected before secure TLS connection wasestablished", failing whichever of the two tests lost the race. This accounted for every
mockserver-nodefailure onmasterover the preceding 40 builds (5 of 40, ~12%), so it was the solecause of the pipeline's intermittent red. Both tests now wait for an actual TLS handshake to complete
before asserting, which gates them on the condition they really depend on rather than retrying the
assertions. The new
waitForTlsReadyhelper is verified to reject — not resolve — both when nothingis listening and when a listener accepts the TCP connection then destroys it mid-handshake, which is
exactly the failure signature it exists to absorb. The readiness budget is deliberately generous
(120s): waiting costs nothing when the server is healthy, since a ready server completes the
handshake on the first attempt in milliseconds, so the limit only decides how much CI contention is
tolerated before a slow start is misreported as a fault. An earlier 30s budget went green five builds
running and then expired on a loaded agent — the same flake wearing a clearer error message. A start
that takes over 5s is now reported even when it passes, because readiness creeping towards the limit
is the signal that the next run will not make it.
archiver.glob()works again in@mockserver/testcontainers(Node), and CVE-2026-14257 staysclosed. The previous remedy for the
brace-expansiondenial of service (GHSA-mh99-v99m-4gvg,patched only in 5.0.8) was a blanket
"brace-expansion": "^5.0.8"override. That resolved the wholetree to a single hoisted 5.0.8 and
npm auditreported zero vulnerabilities — but 5.x changed theCommonJS export from a callable function to an object (
{ expand, EXPANSION_MAX, ... }), while theminimatch copies actually installed (3.1.5, 5.1.9, 9.0.9) all call it as
expand(pattern). Everyglob containing a brace therefore threw
TypeError: expand is not a function, crashingarchiver.glob(). The blast radius is narrower than it first looks —testcontainerscopies fileswith
archiver.directory()/.append(), which pass no brace pattern and still work — so what brokeis brace globbing for anything in this module's runtime tree that does use it. The failure was
invisible because minimatch short-circuits patterns with no
{, so plain globs kept working and theunit suite stayed green. The override is now targeted:
readdir-globandarchiver-utils'globtake
minimatch@^10.2.5, which depends onbrace-expansion@^5.0.5and is written against the newAPI, so both runtime copies land on the patched 5.0.8 with a matching minimatch. jest keeps its own
minimatch@3.1.5+brace-expansion@1.1.16pairing and is untouched.npm audit --omit=devstillreports 0 vulnerabilities, and a new
dependency-integrityunit test drives a brace pattern throughboth runtime minimatch copies and through a real
archiver.glob()tar, plus asserts expansion staysbounded — it fails against the blanket override, so the silent half of this cannot return.
responseOverridethat replaces the body no longer inherits the upstream response'sContent-Length, which truncated the response on the wire. The override swapped the body but left theupstream header in place, so the client read only as many bytes as the body it replaced — a 34-byte
override behind an upstream
Content-Length: 13arrived as 13 bytes — or hung waiting for bytes thatnever came. The stale header is now dropped so the encoder recomputes it from what is actually written;
a
Content-Lengthset by the override itself, andconnectionOptions.contentLengthHeaderOverride, arestill honoured, and a header-only override (one that sets no body) is untouched. This affects every body
override, and it was the remaining reason a
FILEresponse body returned from aresponseOverridestill reached the client wrong after
#2450: the file was materialised
correctly and then cut short by the stale length. Covered by a Netty integration test that drives a real
forward-with-override through a real upstream and asserts the bytes the client receives.
(#2455). "Load into Server" was
rejected with
400 incorrect expectation json formatbecause the builder emitted a shape that neverexisted on the server: a flat
completionstring, a top-levelfinishReason,stream, andusage,and a
providerofOPEN_AI. The completion text, streaming flag, stop reason, and token usagebelong INSIDE the
completionobject (text,streaming,stopReason,usage.inputTokens/usage.outputTokens), and providers are theProviderenum names (OPENAI,AZURE_OPENAI, …). Theprovider and field catalogues shared with the VS Code extension are corrected the same way — they
offered
OPEN_AI,VERTEX_AI,messages,stream,finishReasonand a top-levelusage, none ofwhich the server accepts — and completion inside a
completionobject now offers the nested fields.The plugin has always bundled the correct schema; it simply never validated its own output against it,
and the previous tests asserted the builder matched the same invented shape it produced. Both editors
now validate against the bundled schema in their test suites.
httpLlmResponse.providernow accepts every provider MockServer implements. The JSON Schema enumlisted 9 of the 14
org.mockserver.model.Providerconstants, soMISTRAL,XAI,DEEPSEEK,GROQ,and
OPENROUTERwere rejected with400 incorrect expectation json formateven though each has afully registered response codec. The five missing values are added to the core schema, the generated
VS Code and JetBrains schemas, and both copies of the OpenAPI specification, and a new parity test
fails if the enum and
Providerever diverge again in either direction. The provider list on theLLM response mocking documentation and in the Rust client's field docs is updated to match.
before any test runs. Each ran its Docker container with
--memory=4g, butmockserver/.mvn/jvm.configpins the Maven JVM to
-Xmx6144mand the wrapper prepends it toMAVEN_OPTS, so the-amdependencybuild was permitted a 6 GB heap inside a 4 GB cgroup and the kernel intermittently killed it with exit 137
— losing the very coverage those fail-closed steps exist to guarantee. Raised each to
--memory=7g, thevalue every other
./mvnwstep already uses and which fits the single-agentc5.2xlarge/m5.2xlargedefault-queue instances with margin.
Added
A cassette is now auto-registered when a fixture is loaded or recorded via the MCP tools, so it
appears under
GET /mockserver/cassetteswithout a separatePUT /mockserver/cassettescall.Previously the server-side cassette registry was populated only by an explicit
PUT /mockserver/cassettes, so a fixture loaded with theload_expectations_from_fileMCP tool, orwritten with
record_llm_fixtures, never showed up in the dashboard's Cassettes tab unless thecaller also registered it by hand. Both MCP tool handlers now register the fixture in
CassetteRegistryat the point the file is loaded/written — the file path as the key, the loaded/written expectation count, and an
originofloadedorrecordedrespectively — soGET /mockserver/cassettes(which serialises that registry) lists it automatically. Re-loading orre-recording the same path updates the existing entry in place rather than duplicating it.
Clustered (Infinispan) expectation reload-on-startup is now proven end-to-end. A new test
(
ClusteredExpectationPersistenceReloadTestinmockserver-state-infinispan) forms an in-JVMJGroups cluster consisting of a bare "fleet keeper"
InfinispanStateBackendthat stays up for thewhole test plus a full MockServer node started with
stateBackend=infinispan,clusterEnabled=trueandpersistExpectations=true. An expectation is created on that node overthe wire, the persisted document is polled for through the keeper's backend (proving it really
replicated across the REPL_SYNC blob cache), the node is then stopped completely, and a fresh node
is started against the same cluster and the same
persistedExpectationsPath— which must restorethe expectation and MATCH a real HTTP request with it. The local persisted file is asserted to be
empty first, so the restore cannot be coming from the filesystem-initializer route. The reload path
in
ExpectationFileSystemPersistencewas already covered at unit level inmockserver-core(
ExpectationBlobStoreRestoreTest, against anInMemoryBlobStore, with no server and no cluster)and end-to-end only against S3/MinIO behind a Docker gate; what no test proved is that a clustered
node's
InfinispanBlobStoreis the storeHttpStatewires into that restore, nor that a realrestarted member of a live cluster recovers the fleet's shared expectations. A second test sets
blobStoreRestoreTimeoutSeconds=0(the documented way to skip the restore) and asserts the freshnode does NOT serve the expectation, which permanently pins the fact that no other mechanism —
JGroups state transfer of the expectations cache, a stray invalidation event, or the local file —
restores expectations when a node starts. Verified by a positive control: disabling the reload
path in production makes the restarted node answer with an empty body and turns the test red.
The response-aware arm of the eviction false-green guard is now proven end-to-end over HTTP. A new
Netty integration test (
EvictedResponseVerificationIntegrationTest) boots a real server withmaxLogEntries=2andfailVerificationOnEvictedLog=true, registers an expectation so aGET /was-respondedexchange is recorded as a realEXPECTATION_RESPONSErequest-response pair, then floodsthe bounded event log with further unmatched traffic so that pair is evicted. A subsequent
verify(request("/was-responded"), response().withStatusCode(418), never())through the Java client mustthrow an
AssertionErrorsaying the response "could not be verified" because entries were discardedafter reaching
maxLogEntries.MockServerEventLogimplements this guard twice — once inverifyRequestand once, through a completely separate counting path over recorded pairs, in
verifyResponse— and onlythe request arm had an
*IntegrationTest; the response arm was covered solely by an engine-level testagainst an in-process event log. The test uses
never()because it is the simplest shape that reachesthe guard: the guard sits on the PASS branch behind any asserted upper bound (
getAtMost() != -1— soatMost(n),between(0,n)andexactly(0)reach it too), whereas anatLeast(1)/once()verificationof an evicted pair fails earlier with an ordinary "Response not found" message and proves nothing.
never()is exactly the case a guard-less server would answer with a silent false green. The assertion pins the message toResponse could not be verifiedso it cannot besatisfied by the request-side arm. Verified by a positive control (disabling only the response-side guard
in production makes the verification pass silently and turns the test red).
The eviction false-green guard is now proven end-to-end over HTTP. A new Netty integration test
(
EvictedLogVerificationIntegrationTest) boots a real server withmaxLogEntries=2andfailVerificationOnEvictedLog=true, records aGET /was-calledrequest, then floods the boundedrequest-log ring with further traffic so the
/was-calledentry is evicted. A subsequentverify(request("/was-called"), never())through the Java client must throw anAssertionErrorwhosemessage says the log "could not be verified" because entries were discarded after reaching
maxLogEntries— proving the guard refuses to certify absence it can no longer see, rather thansilently passing. Previously the guard was only covered by an engine-level test against an in-process
MockServerEventLogand no*IntegrationTestexercised it across the wire. Verified by a positivecontrol (disabling the guard in production makes
verify(never())pass silently and turns the testred).
Custom gRPC response metadata and trailing metadata are now proven against a real
grpc-javaclient. Two new tests in
GrpcUnaryClientIntegrationTestregister an expectation whose gRPCresponse carries both custom response metadata authored with
withHeader(...)and custom trailingmetadata authored with
withTrailer(...), drive it with a livegrpc-javaclient, and read thevalues back off the real
io.grpc.Metadataobjects the client receives (via a capturingClientInterceptor, and viaStatusRuntimeException.getTrailers()on the error path). Theassertions are deliberately discriminating: the response metadata must arrive in the initial
headers and not in the trailers, the trailing metadata must arrive in the trailers and not be
folded into the initial headers, and both values must round-trip byte-for-byte including a value
carrying
=,;,,and spaces. Previously this behaviour was exercised only structurally(
EmbeddedChannel/ model-level assertions, which cannot tell a trailer emitted as a trailer fromone folded into the headers) and by the existing
-binmetadata tests, which deliberately acceptthe value from either side because a body-less unary response may legitimately collapse to
Trailers-Only. Verified by positive controls: dropping the user-authored trailers turns both tests
red, and dropping the user-authored response headers turns the header assertion red.
The
maxResponseBodySizelimit is now proven behaviourally against a real upstream. A newintegration test (
MaxResponseBodySizeIntegrationTest) boots a forwarding MockServer configured with a4KB
maxResponseBodySize, points it at a raw upstream socket that returns a 64KB body, and drives itover a plain client socket: the oversized body fails the forward and the client receives 502 Bad
Gateway with none of the payload relayed, while a control request whose body sits under the limit is
forwarded intact. A third case repeats the oversized body with
Transfer-Encoding: chunkedand noContent-Length, proving the cap is enforced against the bytes actually accumulated by the forwardclient's aggregator rather than merely against a declared header. Previously this documented,
memory-protecting bound — read whenever a forward-client pipeline is built — had no behavioural
coverage at all, so a regression that dropped the wiring (or passed an unbounded value) would have
removed the limit silently; only the inbound analogue
maxRequestBodySizewas verified. The new testcovers the HTTP/1.1 forward aggregator; the HTTP/2 forward path reads the same property (for the
per-stream aggregator and to derive the client's
maxFrameSize) and remains uncovered.maxResponseBodySizeaccordingly moves fromENFORCEMENT_EXEMPTtoENFORCEMENT_VERIFIEDinConfigurationEnforcementClassificationTest. Verified by a positive control (restoring an unboundedaggregator lets the oversized body through with a 200 and turns both over-limit assertions red).
The Ruby client now proves live SSE stream consumption over the wire. New integration examples
(
spec/integration_spec.rb→SSE streaming) register anhttpSseResponseexpectation via the Rubyclient against a running MockServer, then open a real streaming HTTP consumer and assert every
data:frame arrives in order, that the reconstructed multi-delta message matches, and that a multi-line
data:payload survives the framing intact (Content-Type: text/event-stream). Previously the Rubysuite only asserted the JSON keys of a built streaming expectation (
a2a_spec) and never consumed alive SSE stream, so a silent server-emission or client-parsing drop would have gone uncaught. Verified
by a positive control (dropping events from the emitted stream turns the received-frames assertion red).
The
assumeAllRequestsAreHttpprotocol-detection fallback now has direct unit coverage. Twopaired
EmbeddedChanneltests inDirectProxyUnificationHandlerTestdrivePortUnificationHandler.decode()with an HTTP request using a non-standard method (PURGE, which isnot one of GET/POST/PUT/HEAD/OPTIONS/PATCH/DELETE/TRACE/CONNECT): with
assumeAllRequestsAreHttp=truethe full HTTP pipeline is added (rather than falling to binary requestproxying), and with the flag disabled the HTTP codec is not added — proving the flag is the only
difference. Previously the fallback branch was exercised only by a live-socket integration test and
the config getter's own unit test, so the
EmbeddedChannelprotocol-detection path for the flag wasunexercised.
HTTP/3 streaming response bodies are now proven end-to-end through the action pipeline with a real
QUIC client. A new integration test (
Http3StreamingForwardIntegrationTest) registers aforwardexpectation on the HTTP/3 port (with
streamingResponsesEnabled) pointing at an upstream Server-SentEvents stream that serves an early event immediately and withholds the late event for 1.5s, then drives
it with a live Netty QUIC client and asserts both events arrive as SEPARATE DATA frames spread across
that delay — proving the streaming relay funnels through
HttpActionHandler->ResponseWriter.writeResponse->Http3ResponseWriter.writeStreamingResponseand emits chunksincrementally. Previously
Http3StreamingIntegrationTestdroveHttp3ResponseWriterdirectly from ahand-built QUIC server (bypassing expectation matching), and
Http3MockingMatrixIntegrationTestexercised the real pipeline over QUIC but only with non-streaming actions, so incremental delivery of a
streamed body through the full pipeline was untested. QUIC-gated like the sibling HTTP/3 tests so it
skips cleanly where the native transport is unavailable.
The dashboard's Monaco code editor is now proven in a real browser end-to-end. A new Playwright
e2e test (
mockserver-ui/e2e/dashboard.spec.ts) drives the actual bundled Monaco editor in theserved dashboard's composer against a live MockServer: it asserts Monaco's own DOM
(
.monaco-editor/.view-lines) renders, authors a JSON response body via real editor input,raises and clears a live validation marker from Monaco's JSON language web worker, then registers
the mock and confirms the Monaco-authored body round-trips to the server (present in
PUT /mockserver/retrieveand served verbatim on the matching request). Previously the 178jsdom/vitest specs globally replaced Monaco with a bare
<textarea>, so nothing exercised the realeditor's tokenisation, web-worker validation, or DOM.
SOCKS4 CONNECT tunnelling is now proven end-to-end over a real socket. A new socket-level
integration test (
NettyHttpProxySOCKS4IntegrationTest) performs a raw SOCKS4 CONNECT handshakeagainst a bound MockServer targeting a loopback
EchoServer, then sends an HTTP GET through thegranted tunnel and asserts the EchoServer received the request and returned 200 (bytes relayed by
Socks4ConnectHandler). Previously SOCKS4 was only exercised by anEmbeddedChannelunit test(
Socks4ProxyHandlerTest, which asserts handler removal) while every real-socket proxy integrationtest used SOCKS5, so nothing drove the SOCKS4 relay path over the wire.
Per-host forward-proxy client-certificate selection is now proven at a real TLS handshake. A new
integration test (
ForwardWithCustomClientCertificateByHostIntegrationTest) configuresforwardProxyClientCertificatesByHostto present two independent client certificates (each backed byits own CA) keyed by host, then forwards through MockServer to two secure upstream
EchoServers thateach
REQUIREclient auth and trust only ONE of the two client CAs. Because the host string is thecert-mapping key while the connection target is fixed independently by the forward port, the same
upstream is reached under both host keys: the mapped cert is accepted (200) and the mismatched cert is
rejected at the handshake (502). This makes the presented client certificate a load-bearing assertion,
verified by a positive control (degrading the mapping to always present cert A flips host B's accepted
case to 502). Previously
NettySslContextFactoryTestasserted onlySslContextidentity/distinctnessand the pure resolver -- nothing drove the per-host cert to an actual mTLS handshake.
LLM streaming physics for Gemini, Ollama, and Bedrock are now proven over a real socket. Streaming
physics over the wire was previously e2e-tested only for Anthropic and OpenAI (both SSE); Gemini, Ollama,
and Bedrock rested on self-derived golden JSONL plus codec unit tests, with no socket streaming e2e. Three
new tests in
LlmAgentLoopE2eTestserve a streaminghttpLlmResponsefor each provider, connect a realsocket client, and assert both that the wire
Content-Typeis the provider's streaming media type(
text/event-streamfor Gemini SSE,application/x-ndjsonfor Ollama NDJSON,application/vnd.amazon.eventstreamfor Bedrock AWS event-stream binary framing) and that the textreconstructed by concatenating the streamed deltas — Gemini
candidates[].content.parts[].text, Ollamamessage.content, and Bedrock's base64-wrapped Anthropictext_deltafragments decoded from theCRC32-validated binary event-stream messages — equals the completion text exactly. The Bedrock case
de-chunks the HTTP/1.1 chunked body and decodes the binary framing end to end.
The PHP client's fidelity harness now gates the TYPED model, not just raw replay. The existing
RoundTripFidelityTestdeserialises each shared fixture withExpectation::fromArray(), which storesthe decoded array verbatim in
rawDataand replays it unchanged -- so it records zero gaps for everyfixture BY CONSTRUCTION and can never detect a field the typed builders (
HttpResponse,HttpForward,HttpError,HttpRequest) fail to model. A newTypedRoundTripFidelityTestcloses that tautology:for every shared fixture it reconstructs each action/matcher block THROUGH the typed model (reflection
copies across only the properties each class declares, recursing into declared nested typed objects,
then serialises via the class's own
toArray()) and diffs the rebuilt structure back against thefixture -- the server-schema side of the contract -- so any server field the typed model drops surfaces
as a concrete diff path derived from the corpus, never from the client's own key list. This immediately
documented five real request-matcher gaps the raw harness hid (
dnsClass/dnsName/dnsType,pathParameters,protocol, plus NottableStringmethod/path), each pinned in a per-field gapledger with a stale-entry ratchet, while the
httpResponse/httpForward/httpErrormodels are provento cover their entire fixture surface. A positive-control test proves the gate fires when a typed
builder drops a field it is supposed to carry (removing
statusCodefromHttpResponse::toArray()turns the suite red for every fixture carrying it, and green again on restore) -- the exact regression
the raw-replay harness cannot catch. Shared comparator logic is extracted to
FidelityComparator.The Go client's FORWARD and ERROR response actions are now proven over the wire. New integration
tests (
response_action_integration_test.go) register ahttpForwardand ahttpError(
dropConnection) action via the Go client and drive real requests that assert the SERVER actuallyperforms them: the forwarded request loops back through a match-once, higher-priority self-forward to
a distinct upstream response body (needing no externally-reachable upstream, so it runs identically in
the CI sibling-container harness and against a locally port-mapped server), and the error endpoint
drops the connection at the transport level (no HTTP response) while a control endpoint on the same
server still answers cleanly. Previously the Go client's response-action coverage was builder/JSON
only -- no test drove a forward or error action to completion over a socket.
Rust client wire tests proving the server actually enforces negation matchers and performs
response actions. The Rust integration suite previously only asserted control-plane serialization
(
matcher_value_tests.rs) and registered a forward expectation it never drove (test_forward_expectation"won't actually forward"), so nothing proved a running MockServer acted on either. Three new
#[ignore]d integration tests drive a live server over the data plane via a dependency-free rawsocket:
test_negation_matcher_enforced_over_wireregisters aNottableStringnegation (bare"!foo", explicitMatcherValue::not_literal, and an escaped literal"!foo") and asserts theserver matches a non-
foovalue (200) while excludingfoo(404) — and that an escaped"!foo"matches literally rather than as a negation;
test_forward_action_actually_forwardsregisters ahigher-priority run-once FORWARD that loops back to the server's own port plus a lower-priority
fall-through RESPOND, and asserts the distinctive fall-through body is returned only if the forward
genuinely executed (topology-independent — no external upstream); and
test_error_action_actually_returns_raw_bytesregisters an ERROR action and asserts the serverwrites the configured raw bytes back. Run in CI by the existing
rust-integration-teststep.Wire-level .NET client test coverage for NottableString header negation and for
forward/error response actions. The .NET integration suite asserted expectation creation but
never proved the server honours what the client sends: there was no test that a
!fooheadermatcher (
MatcherValue.NotLiteral) is transmitted and enforced over the wire, nor that aregistered forward or error action is actually performed. A new
WireBehaviorTestsdrives realrequests through a running MockServer (reached via the existing
MOCKSERVER_URLharness) to prove:a "not foo" header matcher matches a non-
foorequest (200) and rejects afoorequest (404); theescaped literal
MatcherValue.Literal("!foo")matches only a header whose value really is!foo;a forward action is genuinely forwarded (the path is received twice — original plus the re-entered
forward — not merely served directly); and an error action actually drops the connection (a
transport failure, not an HTTP status). Each assertion was confirmed to redden when the
corresponding client behaviour is degraded.
Live-broker test coverage proving Kafka SASL credentials reach and are enforced by a real
broker. Kafka SASL/SSL security was only asserted at the property-map level
(
KafkaMessagePublisherSecurityTest) and every live-broker Kafka integration test used a plaintextbootstrap, so nothing proved the configured credentials actually authenticate against a broker. A
new
KafkaSecurityLiveBrokerIntegrationTeststarts a Testcontainers Kafka whose external listeneris
SASL_PLAINTEXT/PLAINwith a broker-side JAAS config that knows a single credential, thendrives MockServer's
KafkaMessagePublisherwith aKafkaSecurity: a correctly-credentialedpublisher publishes successfully and the message is read back by a matching credentialed consumer,
while a wrong-password publisher is rejected with an authentication exception (proving enforcement,
not merely that plaintext works). Docker-gated so it SKIPS cleanly when Docker is unavailable.
Credential-enforcement test coverage for MQTT security against a secured live broker. MQTT
MqttSecuritycredentials were only asserted at the options-carrier unit level(
MqttSecurityOptionsTest), while the sole live Mosquitto integration test ran anallow_anonymousplaintext broker — so nothing proved credentials are actually applied andenforced on the wire. A new
MqttTlsLiveBrokerIntegrationTestdrives MockServer's MQTT publisheragainst a Testcontainers Mosquitto broker configured with a
password_fileandallow_anonymous false: it asserts that a publisher wired with the correctMqttSecurityusername/password authenticates and delivers a message to an authenticated subscriber, and that a
publisher with the wrong password (and one with no credentials) is rejected by the broker at
CONNECT. Docker-gated so it skips cleanly when Docker is unavailable.
Live-broker test coverage for the AsyncAPI control-plane
load()→ publish-on-load path. Thecontrol-plane's broker-connecting path (
createBrokerConnections/publishOnLoad) was untestedagainst a real broker —
AsyncApiControlPlaneImplTestloads without a reachable broker (assertingpublishers=0), and the endpoint IT covers only the broker-less endpoints. A new Docker-gatedAsyncApiControlPlaneLiveBrokerIntegrationTest(Testcontainers Kafka) drivesload()with a realbrokerConfig,publishOnLoad:trueandconsume:true, then proves the control-plane genuinelyconnected and published by consuming the on-load message with a plain third-party Kafka client and
asserting
status()reportspublishers>0, a subscriber, and the recorded round-tripped message.Over-the-wire test coverage for an LLM refusal preset served with a rate-limit quota. The LLM
refusal presets, provider-specific rate-limit headers, and stateful request-count quota were only
asserted at the body-builder / handler-unit level; nothing drove them through a running server. A
new
LlmRefusalQuotaRateLimitIntegrationTestserves anhttpLlmResponseconfigured with anAnthropic refusal preset and a 2-request quota, then asserts on the raw socket response that the
first two requests return a
200refusal envelope (stop_reason:"refusal") carrying theanthropic-ratelimit-requests-*headers, and that the third (over-quota) request flips to a429rate_limit_errorenvelope with the exhausted rate-limit headers andRetry-After.Fixed
testcontainers-mockserver(Python) port assertions no longer break against testcontainers4.15.0, which keys
DockerContainer.portsbystr(port)rather thanint.with_exposed_portsnow stores
self.ports[str(port)] = None(4.15.0 types the attribute asdict[str, Optional[int]]), so the suite'sassert 1080 in container.portsstarted failing withassert 1080 in {'1080': None}and took five tests — and the wholeMockServer Pythonpipeline,and with it the umbrella
MockServerbuild — red onmaster. The tests now read the exposed portsthrough a normaliser that parses each key to an
int(tolerating a"1080/tcp"protocol suffix),so they assert the same thing under either key style. This also closes a latent false green: the
assert MOCKSERVER_PORT not in container.portscheck intest_replaces_default_portpassedtrivially once the keys became strings, and so would no longer have caught
with_server_portfailing to drop the previously exposed port. Only the tests changed —
MockServerContaineritselfwas already correct, as
get_exposed_porttakes anintand normalises internally.<blobStoreKeyPrefix>/<file name>instead of under the writing machine's absolute local path, and ablobStoreKeyPrefixthat does not end in a separator is now treated as a folder-style prefix insteadof being glued straight onto the key (
mockserver+x.jsonwasmockserverx.jsonand is nowmockserver/x.json). Anything persisted by an earlier version is stored under the OLD name and willNOT be restored after upgrading — the one-line migration is below.** The blob key was the ABSOLUTE
local
persistedExpectationsPath(for example/var/folders/.../persistedExpectations.json) and theconfigured
blobStoreKeyPrefixwas concatenated onto it with plain string addition. With the prefixshape the documentation recommends —
blobStoreKeyPrefix="mockserver/", with a trailing separator —that composed
mockserver//var/folders/.../persistedExpectations.json, and MinIO rejects the doubled//outright with HTTP 400, "Object name contains unsupported characters", so under that oneconfiguration nothing was ever persisted and consequently nothing could be restored on restart. Under
every other prefix shape the write SUCCEEDED and restore worked — a leading
/is a legal byte in anS3 object name, and the read composed exactly the same name back — but the object was then named after
the writing container's local filesystem layout, so a second instance that resolved
persistedExpectationsPathto a different absolute path (started from a different working directory,or in a different container) looked under a different name and silently restored nothing. The key is
now derived by the new shared
org.mockserver.state.BlobKeyshelper inmockserver-core: for everystore other than
FilesystemBlobStorethe key is the FILE NAME ofpersistedExpectationsPathalone,and prefix and key are joined with exactly one separator, with any leading separator dropped and any
repeated separators collapsed, so every prefix shape a user can configure — unset,
mockserver,mockserver/,/mockserver/— now produces the same valid object name(
mockserver/persistedExpectations.json). That normalisation is applied for allput/get/list/deleteoperations whereverblobStoreKeyPrefixis applied, so it renames EVERYblob key, not only the persisted-expectations document.
FilesystemBlobStoreis unaffected: itinterprets the key as a file path, so it keeps the absolute path and writes exactly the file it always
did. Upgrading: because the old key always embedded the absolute local path and the new key is the
bare file name, the object name changes for every non-filesystem persistence user, under every prefix
shape and on every platform — there is no configuration in which the old name is preserved. On the
first start after upgrading, the restore looks under the new name, misses (logged at
INFOwith thename it looked for), and the instance starts with no restored expectations; the next expectation change
then writes a fresh object under the new name and leaves the old one behind. To carry existing state
across the upgrade, copy or rename the object once before starting the new version, for example
aws s3 mv s3://<bucket>/<prefix>/<old absolute path> s3://<bucket>/<prefix>/<file name>— otherwiseaccept the miss and let the first expectation change re-create it. One long-standing footgun goes away
with the change: restoring after a restart no longer requires the two instances to resolve
persistedExpectationsPathto the same absolute path, only to the same file name. Deployments thatmust NOT share state within one bucket should give each its own
blobStoreKeyPrefix(or its own filename). Proven by a Docker-gated MinIO round trip that writes and then reads back an expectation with a
trailing-slash
blobStoreKeyPrefix(the exact configuration that returned HTTP 400 before), a MinIOput/get/list/delete round trip across all four prefix shapes, and Docker-free unit coverage of the key
composition and of the key the persistence layer derives.
ConfigurationEnforcementClassificationTestrecords, for every risky configuration property, theClass#methodtest that proves an instance-set value changes observable behaviour. It validated thosepointers by loading the class — but it runs in
mockserver-core, so any pointer naming a test in asibling module was silently skipped on
ClassNotFoundException. That exempted precisely the mostvaluable evidence, the end-to-end Layer C pointers: renaming, moving or deleting the referenced test
left a dangling pointer and the guard still passed green, for
maxRequestBodySize,maxResponseBodySize,wasmEnabled,redactSecretsInLog,clusterEnabled,dnsEnabled,grpcBidiStreamingEnabled,http3ConnectUdpEnabledandtransparentProxyEnabled. A class thatcannot be loaded is now resolved
by locating its
.javasource under any module'ssrc/test/javaand asserting the file declares boththe class and the referenced method, so cross-module pointers are checked in a full reactor build and
when only some modules are built. The guard fails closed: a pointer resolvable by neither route is now
a failure naming the dangling pointer, never a silent skip. Anti-vacuity assertions in the spirit of
the sibling
ConfigurationCallSiteGuardTestkeep the scan honest — the set of pointers resolved bysource scan must match the declared cross-module ratchet exactly,
mockserver-nettyandmockserver-state-infinispanmust both have contributed, and classpath resolution must still coverthe bulk of the pointers — so a scan that resolves nothing cannot pass. Verified by degrading a real
mockserver-nettytest method name: the guard now fails with a message naming the dangling pointer,where the previous version passed green with the identical defect in place.
authored —
response().withTrailer("x-request-cost", "42"), and the gRPC chaos profile'scustomTrailers— reached an HTTP/1.1 or HTTP/2 client but never reached an HTTP/3 client atall, in every branch of the HTTP/3 gRPC response path (with a body and body-less, and both with
and without proto descriptors loaded). The same expectation therefore produced different trailing
metadata depending only on which transport the client happened to use, and there was no error or
warning anywhere to indicate the loss — a test asserting on trailing metadata over HTTP/3 simply
saw nothing. The cause was that
Http3GrpcResponseWriterbuilds its HTTP/3 frames by hand ratherthan through
MockServerHttpResponseToFullHttpResponse.mapResponseWithTrailers(which is whatcarries trailers on the other transports), and
GrpcHttp3Adapter.buildTrailingHeadersFrame/buildTrailersOnlyFramepopulated onlygrpc-statusandgrpc-message; the response's owntrailers were never read. They are now emitted on the terminal frame: on the trailing HEADERS
frame when the response has a body, and folded into the Trailers-Only frame when it does not,
which is the shape gRPC defines for that form (
HTTP-Status Content-Type Trailers, whereTrailersincludes custom metadata) and leaves the framing unchanged — there is still exactly oneterminal frame, written with
SHUTDOWN_OUTPUT, so an added trailer cannot cost the response itsend-of-stream marker the way it did on HTTP/2 before the fix above. A user-authored trailer cannot
override or spoof the transport's own status: the new shared
GrpcResponseStatusResolver.passThroughTrailers(the trailer twin ofpassThroughHeaders)excludes
grpc-status,grpc-messageandgrpc-status-name, mirroring the exclusion the HTTP/2path makes in
remainingTrailers, and also excludes the connection-specific fields,content-length/content-typeand pseudo-header names that RFC 9114 forbids in a trailer section.Trailer field names are lower-cased and CR/LF stripped from values before reaching the frame,
because HTTP/3 field names must be lower-case and Netty's
DefaultHttp3Headersrejects anupper-case one by throwing — so an expectation authoring
withTrailer("X-Request-Cost", …)wouldotherwise have taken the client's entire response down rather than dropping a single field.
Verified over the wire by two new tests in
Http3GrpcIntegrationTestthat drive a live in-JVMNetty QUIC client and read the metadata off the HEADERS frames it actually received, asserting
which side each value arrived on (a trailer must not be folded into the initial headers, a
response header must not be repeated as a trailer) and asserting the HEADERS-frame count so the
metadata cannot arrive at the cost of correct framing, plus adapter-level coverage in
GrpcHttp3AdapterTest. Positive control: neutering the trailer pass-through in production turnsall seven new assertions red with the trailer absent.
request body is now actually matched (#2450). A response whose body is a
FILE(afilePathwithno template engine) was previously read on the static response action only; the same FILE body
returned from an object callback, a class callback, a response template, or a forward
responseOverridereached the wire unread, emitting the file path string instead of the filecontents. Materialisation now lives in a single shared
FileBodyMaterialiserinvoked from the tworesponse-write funnels (
writeResponseActionResponse, covering the static, object-callback,class-callback, response-template and SSE paths, and
writeForwardActionResponse, covering theforward
responseOverride), so all five producers — and the shared WAR/servlet path — serve the filecontents. Templated FILE bodies (a
FileBodycarrying a Velocity/MustachetemplateType) are renderedagainst the request on these paths too, not only verbatim ones; a text content type yields the decoded
string and a binary or absent content type yields the raw bytes intact. A missing or unreadable file
now produces a clean, logged
500whose body does not leak the path, instead of a broken connection orthe path string. Separately, a
FILEbody used for request matching had no case inBodyMatcherBuilder, so the body constraint was silently ignored (it matched any body); it now matchesthe request body against the exact file contents (string or binary). This resolves the earlier
"static response only" caveat.
responseOverridethat replaces the body is no longer truncated to the upstreamContent-Length. When a forwarded request's response is overridden with a new body,HttpResponse.update()replaced the body but kept theContent-Lengthinherited from the upstreamresponse. A replacement body longer than the upstream body was therefore truncated on the wire (and a
shorter one could over-run) — visible only to a real client, since every layer above the encoder held
the full, correct response.
update()now drops the inheritedContent-Lengthwhenever the overridesupplies a new body (or a
generateFromSchema), unless the override itself sets an explicitContent-Length(which is still honoured verbatim), so the encoder recomputes the length from theactual body. This closes the residual on the forward-override path left by the FILE-body fix above.
body-less gRPC response is collapsed into the gRPC Trailers-Only form, which moves
grpc-statusinto the initial HEADERS frame and relies on that frame being end-of-stream. When the expectation
also authored a custom trailer with
withTrailer(...), that trailer kept a separate trailingHEADERS frame alive, so the initial frame was no longer end-of-stream: a real client read it as
ordinary headers (where
grpc-statusis ignored) and then found no status at all in the terminalframe, failing the call with
UNKNOWN: missing GRPC status. In other words, adding a singletrailer to an error response destroyed the error — the caller lost both the status code and the
message.
GrpcToHttpResponseHandler.asTrailersOnlyIfHttp2now skips the Trailers-Only collapsewhenever any user-authored trailer remains, keeping
grpc-status/grpc-messagein the trailingHEADERS frame alongside the custom metadata, which is the correct shape in that case. The same fix
covers the gRPC chaos fault path, which produced the byte-identical broken shape: a fault response
configured with
customTrailersemits them as real trailers alongsidegrpc-status/grpc-messageon a body-less response, so a chaos-injected error over HTTP/2 also reached the client as
UNKNOWN: missing GRPC statusinstead of the configured status. Found by the new real-clienttrailing-metadata coverage described under Added.
is covered by an over-the-wire integration test. Every previous gRPC-Web test drove the handler
through an
EmbeddedChanneland setx-grpc-web-content-typedirectly on the response, so noneexercised the actual mock-matching path: there the marker lives on the request only and was lost,
and a matched expectation went back to a browser client as
application/grpcwithgrpc-statusinHTTP trailers a gRPC-Web client cannot read. The original request content-type is now retained in
the per-stream
GrpcPendingRequestsrecord alongside the resolved service/method, soGrpcToHttpResponseHandlerre-frames the response as gRPC-Web (length-prefixed message frame + a0x80trailer frame carryinggrpc-statusin the body, base64-encoded for the-textvariant).A new
GrpcWebOverTheWireIntegrationTestposts a realapplication/grpc-webandapplication/grpc-web-textframed request to a running server over a raw socket and asserts on theexact bytes a browser client would receive.
PUT /mockserver/asyncapi,GET /mockserver/asyncapiandPUT /mockserver/asyncapi/verifywereonly exercised at the orchestrator/control-plane level, so a regression in the Netty →
HttpState→AsyncApiControlPlaneRegistryrouting or response wiring would not have been caught.A new
AsyncApiControlPlaneIntegrationTestboots a real MockServer and drives all three endpointsover a raw socket without any live broker: it asserts the load response (
201,loaded:true,channel count, zero publishers/subscribers), the status response (
200, channels, counts, and theempty/unloaded case), and the broker-less verify verdict (
406with the "at least 1 … found 0"failure detail) plus the blank-body
400.StreamingBodyresponse delivered to a real HTTP/2 inbound client is now covered end-to-end.NettyResponseWriter.writeStreamingResponsere-stamps the request's HTTP/2 stream id onto thestreaming response head (the field is not part of the copied header multimap) and flushes each chunk
as it arrives, but no test drove that path with a real HTTP/2 client —
Http2SseStreamingIntegrationTestcovered only the SSE sibling and explicitly noted the
StreamingBodycase was untested, while theexisting streaming-relay tests drive an HTTP/1.1 inbound socket. A new
Http2StreamingBodyIntegrationTestdrives a real prior-knowledge h2c multiplex client through a
streamingResponsesEnabledforwardMockServer to an SSE upstream and asserts on the frames the client receives on its OWN request stream:
both events arrive (proving the stream-id stamp — the #2419 class), and the early event's DATA frame
arrives promptly as one of at least two separate, in-order DATA frames rather than being buffered into
one. Degrading the write path to buffer chunks until stream completion was verified to make the
incremental-delivery assertion go red.
socket.
ProxyProtocolOriginalDestinationHandlerwas only exercised viaEmbeddedChannel, which assertsthe handler sets the
REMOTE_SOCKETchannel attribute but never that this attribute actually chooses theforward target end-to-end. A new non-privileged loopback
ProxyProtocolForwardingIntegrationTestrunsMockServer with
transparentProxyEnabled=trueand no fixed remote, opens a raw socket, writes a validPROXY v1
TCP4header naming a loopbackEchoServeras the destination followed by a plain GET whoseHostheader points at an unrelated (closed) decoy port, and asserts the EchoServer reflects the requestback — proving the PROXY-protocol
REMOTE_SOCKET, and not theHostheader, drives forwarding. The testneeds no
NET_ADMIN/privileged capability because the PROXY-protocol header is an application-level byteprefix. Verified as a genuine regression guard by a positive control: ignoring the PROXY-header
destination turns the forwarding assertions RED, restoring it returns them GREEN.
A static response with a body of type
FILEand a `filePatConfiguration
📅 Schedule: (UTC)
🚦 Automerge: Enabled.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.