chore(deps): update dependency org.mock-server:mockserver-netty-no-dependencies to v8 - #2475
Merged
renovate[bot] merged 1 commit intoSep 15, 2026
Conversation
renovate
Bot
requested review from
dhoard,
fstab,
jaydeluca and
zeitlinger
as code owners
September 15, 2026 13:18
zeitlinger
approved these changes
Sep 15, 2026
renovate
Bot
deleted the
renovate/org.mock-server-mockserver-netty-no-dependencies-8.x
branch
September 15, 2026 13:36
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.6.0→8.0.0Release Notes
mock-server/mockserver-monorepo (org.mock-server:mockserver-netty-no-dependencies)
v8.0.0Added
org.mockserver.socket.PortFactorynormally finds a free port withbind(0), which draws from the sameephemeral range (on macOS
net.inet.ip.portrange.hifirst..hilast, typically 49152-65535) that every otherbind(0)on the machine uses — including unrelated applications, IDE helpers, and other JVMs. On a busydeveloper machine that causes two kinds of test flake: a foreign process can occupy a number a test is about to
choose, and a test can momentarily connect to that foreign listener instead of the server under test. Setting
both
mockserver.testPortRangeStartandmockserver.testPortRangeEnd(for example-Dmockserver.testPortRangeStart=20000 -Dmockserver.testPortRangeEnd=40000) makesPortFactorychoose ports byexplicitly binding inside that band — which the OS does not itself hand out to
bind(0)— retrying past anynumber already in use. Both properties are unset by default, so continuous integration and any machine that does
not set them keep the exact
bind(0)behaviour as before. This affects only test port selection; it does notchange how MockServer binds its own ports at runtime (starting on port
0and reading back the assigned portremains the fully race-free option and is unchanged). In a surefire/failsafe fork the two properties must reach
the fork, e.g. via
-Dmockserver.testArgLine="-Dmockserver.testPortRangeStart=20000 -Dmockserver.testPortRangeEnd=40000".Changed
h2(over TLS) andcleartext
h2c— now uses Netty's stream-multiplexing model for every connection, replacing the previousshared-connection HTTP/2 pipeline. Each HTTP/2 stream is processed on its own isolated child channel. This
removes a class of cross-stream interference on busy connections: concurrent streaming responses (Server-Sent
Events, NDJSON, AWS Bedrock event-stream, and therefore all streaming LLM responses) can no longer have a later
chunk of one stream mis-routed onto another, and each stream's end-of-stream is delivered independently, so a
slow or streaming response on one stream cannot stall another. This model was previously used only when
grpcBidiStreamingEnabledwas set; it is now the standard HTTP/2 pipeline and needs no configuration — so theHTTP/2 correctness fixes listed below apply to all HTTP/2 traffic, not only when that flag is enabled. There is
no change to the REST/Java API, to how expectations are written, or to HTTP/1.1 and HTTP/3 traffic, and HTTP/2
clients receive the same responses. Users driving very large numbers of concurrent streams over a single
connection may notice different memory and throughput characteristics, since each stream now has its own
lightweight channel. (GitHub issue #2669).
closeSocketnow ends the stream, not the connection. A per-expectationConnectionOptions.closeSocket/closeChannel(and theslowCloseDelayconnection-lifecycle chaos) appliedto an HTTP/2 request now closes only that request's stream; other requests in flight on the same connection
continue and complete normally. Previously it tore down the whole TCP connection, killing every concurrent
stream on it — so one expectation could destroy unrelated clients' in-flight requests. Ending a single stream
is the correct HTTP/2 semantic and is what the same expectation already did over HTTP/1.1, where a connection
carries one request at a time. If you rely on
closeSocketto tear down an HTTP/2 connection — for exampleto test how your client recovers from a dropped connection — use the
resetMidResponseconnection-lifecyclechaos fault instead, which still aborts the whole TCP connection (that is its purpose, and it now does so
properly rather than degrading into a single-stream reset). HTTP/1.1 and HTTP/3 behaviour is unchanged.
(GitHub issue #2669).
mockserver-bomnow manages only MockServer's ownorg.mock-servermodules — it no longer pinsthe third-party libraries MockServer uses internally. The published BOM previously baked in MockServer's
entire parent
dependencyManagement(~190 third-party entries — Jackson, Netty, Guava, Nimbus, Velocity, andmore), four of them at
testscope. Because a BOM's managed versions and scopes apply to whoever imports it,this silently overrode a consumer's own versions and scopes for those shared libraries. For example, a
project that declared
com.nimbusds:oauth2-oidc-sdkwithout a scope had it forced onto the test classpathby MockServer's internal test-scoped pin, so the dependency disappeared from compile/runtime and the
consumer's production code failed to build against it (GitHub issue #2684). The published BOM now contains
only the MockServer module entries (24, down from 221), so importing it never changes any third-party version
or scope in your build. What you need to do: if you imported
mockserver-bomto align MockServer'stransitive third-party versions — for instance to satisfy the Maven Enforcer
dependencyConvergencerule —those pins are gone and convergence errors may reappear; manage the affected third-party versions yourself in
your own
dependencyManagement, or import each upstream project's own BOM. Aligning the MockServer modulesthemselves is unchanged: keep importing the BOM and declare MockServer artifacts without a version.
(GitHub issue #2684).
Fixed
A SOCKS4a client (for example
curl -x socks4a://…) proxying through MockServer no longer hangs.SOCKS4a is the SOCKS4 extension where the client sends the destination as a hostname instead of an
IPv4 address. MockServer decoded the request correctly but then echoed that hostname back into the
DSTIPfield of the SOCKS4 grant reply, which must be an IPv4 literal; the resulting error was thrownwhile writing the reply, so the client never received one and blocked until it timed out (curl exit 28,
0 bytes) — for cleartext HTTP as well as for TLS. The reply's
DSTIP/DSTPORTare ignored by clients,so a SOCKS4a grant now carries
0.0.0.0:0; a classic SOCKS4 request (IPv4 literal) still echoes itsdestination back unchanged. Plain SOCKS4 and SOCKS5 (
socks5h://) were unaffected.The command-line server now exits with a non-zero status code when it fails to start. Previously, if
MockServer could not start — most commonly because the requested port was already in use — the CLI logged
the error but still exited
0, so a shell script or CI job that started MockServer got no failure signaland carried on as though the server was up (typically failing later with a confusing connection error).
mockserver run/-serverPort(and theui,demo,proxy, andopenapisubcommands, which start aserver the same way) now exit
1on a failed start. Usage errors that were already handled — an invalid ormissing port, an invalid host or log level — keep their existing exit code, so only the previously-silent
startup-failure case changes.
An HTTPS request that negotiates HTTP/2 (ALPN
h2) through MockServer's SOCKS proxy now works.Previously, using MockServer as a SOCKS4/SOCKS5 proxy for an
https://request that upgraded to HTTP/2failed completely — the client received nothing (curl reported
CURLE_HTTP2, 0 bytes) — because MockServerprovisioned its internal relay for HTTP/1.1 before the tunnelled TLS connection had negotiated its protocol,
so the forwarded HTTP/2 frames were unparseable. SOCKS over HTTP/1.1, SOCKS over cleartext, and the HTTP
CONNECTproxy over HTTP/2 were unaffected and continue to work. MockServer now terminates the tunnelled TLSin the relay and waits for its ALPN result before wiring up the connection, exactly as the
CONNECTproxyalready did. This works on any port: rather than guessing from the destination port number (the earlier
fix inferred TLS only for ports ending in
443, soh2to a TLS port such as993,465, or9999wasstill mis-provisioned as HTTP/1.1), MockServer now classifies the first bytes the client sends through the
tunnel — a TLS record versus a cleartext HTTP request — and provisions the connection to match. As a result,
HTTP/2 over TLS through a SOCKS tunnel succeeds on any port, and a cleartext tunnel to a port that happens to
end in
443is no longer mistaken for TLS. Cleartext HTTP/2 with prior knowledge (h2c) through a SOCKStunnel now works too — the last case left as HTTP/1.1 by the earlier fixes. The same first-bytes
classification now also recognises the HTTP/2 connection preface (
PRI * HTTP/2.0…) after ruling out a TLSrecord, and provisions cleartext HTTP/2 on both relay legs so the mocked response is served over
h2c(a curl
--http2-prior-knowledgerequest through asocks5h://proxy, for example); anything that is not apreface is still provisioned as HTTP/1.1, and when HTTP/2 is disabled (
http2Enabled=false) the preface isignored and the tunnel falls back to HTTP/1.1, exactly as MockServer's own listener does. The HTTP
CONNECTproxy is unchanged, so cleartext HTTP/2 with prior knowledge through
CONNECTis still served as HTTP/1.1 —that was never a working case and is unaffected either way. (GitHub issue #2685).
Starting the command-line server with port
0now reports and uses the actual OS-assignedephemeral port instead of
0. Previouslymockserver run -p 0(or-serverPort 0) bound a realephemeral port but recorded the requested
0, so the port a caller needs to reach the server wasnot discoverable, and
ui -p 0/demo -p 0printed a dashboard/getting-started URL pointing atlocalhost:0. MockServer now records the real bound port after startup and uses it for thedashboard and demo URLs. Starting on an explicit, non-zero port is unaffected.
HTTP/2 responses larger than the client's initial flow-control window no longer hang when fetched
through MockServer's HTTPS forward proxy (HTTP
CONNECT). On the CONNECT-tunnel path several handlersahead of the HTTP/2 codec overrode Netty's
channelReadCompleteto only flush, without propagating theevent down the pipeline. Netty's HTTP/2 connection handler relies on
channelReadCompleteto flushflow-control-pending writes (it is where a peer's
WINDOW_UPDATEis acted on), so swallowing the eventstalled any h2 response bigger than the peer's initial window (65,535 bytes by default) at exactly one
window until the client timed out. Direct HTTP/2 (
h2cand TLS+ALPN) and HTTP/1.1-through-CONNECTwere unaffected. The affected handlers now propagate the event. (GitHub issue #2683).
The S3 blob-store tests pull MinIO from quay.io instead of Docker Hub.
minio/miniois no longerpullable from Docker Hub, which broke these tests — and therefore the build — with a container-fetch
error unrelated to any code change. quay.io is MinIO's other official registry and serves the same
image and tag. Test-only; nothing MockServer ships is affected.
The Python client and the Python Testcontainers module now ship a PEP 561
py.typedmarker, so typecheckers use their annotations instead of ignoring them. Both packages are almost fully annotated, but
without the marker
mypyreportedSkipping analyzing "mockserver": module is installed, but missing library stubs or py.typed markerand treated every import asAny, so no call into the client waschecked at all. Nothing about the packages' behaviour changes —
mypy(and any PEP 561 checker) will nowtype-check your calls, which may surface genuine mistakes in existing code that were previously
invisible. (GitHub issue #2680).
With gRPC bidi-streaming enabled (
grpcBidiStreamingEnabled), streaming responses over HTTP/2 — Server-SentEvents, NDJSON, AWS Bedrock event-stream, and therefore all streaming LLM responses — now terminate correctly
instead of leaving the client hanging. Enabling that mode routes every HTTP/2 stream (not just gRPC) through
Netty's multiplex model, where each stream is a separate child channel. On that path MockServer's terminal
end-of-stream marker was being silently dropped by Netty's stream-frame codec, which hard-codes streaming data
frames as "not the end of the stream". The client received every event and then waited — receiving no
end-of-stream — until it timed out, with nothing failing or logged server-side. MockServer now translates the
terminal frame so the codec emits it with the end-of-stream flag set, closing the stream as expected. Streaming
over HTTP/1.1 and over the default (non-multiplex) HTTP/2 pipeline was already correct and is unaffected. (GitHub issue #2669).
With gRPC bidi-streaming enabled (
grpcBidiStreamingEnabled), plain HTTP requests sharing the same HTTP/2connection are no longer mis-handled. Enabling that mode switches the HTTP/2 pipeline to Netty's multiplex
model, giving every stream its own child channel — but those child channels do not inherit the parent
connection's attributes, and MockServer had only been copying one of them across. As a result, on any
non-gRPC request arriving over such a connection: it was not recognised as HTTP/2 (so
withProtocol(HTTP_2)matching failed and the protocol was mis-reported in the request log and HAR export); a request to the
dashboard or callback WebSocket endpoint tried to perform a WebSocket handshake over the HTTP/2 stream — which
is unsupported — instead of cleanly returning
501 Not Implemented; a client certificate presented on theconnection was not visible to control-plane authentication for MCP requests on that stream; and a proxied
request (SOCKS/CONNECT/transparent/port-forward) was mis-routed — treated as a direct request, or forwarded to
its
Hostheader instead of the proxy's actual remote target. The connection-scoped state a request depends on(negotiated protocol, TLS/client-certificate details, the proxying flag, the proxy remote-target address, and
the local-host set) is now propagated onto each HTTP/2 stream child channel, so these requests behave exactly as
they do on HTTP/1.1 and on the default (non-multiplex) HTTP/2 pipeline. HTTP/1.1 traffic is unaffected. (GitHub issue #2669).
With gRPC bidi-streaming enabled (
grpcBidiStreamingEnabled), a plain HTTP request sent over HTTP/2 with acompressed body (
content-encoding: gzip,deflate, and so on) is now decompressed before matching. Enablingthat mode routes every HTTP/2 stream — ordinary requests included, not just gRPC — through Netty's multiplex
model, and on that path the request body was left compressed: a
withBody(...)expectation then silentlyfailed to match (MockServer answered
404 Not Found) and the recorded request showed unreadable compressedbytes. Compressed request bodies are now decompressed on this path exactly as they are on HTTP/1.1 and on the
default (non-multiplex) HTTP/2 pipeline. gRPC's own message compression (carried by
grpc-encoding) is aseparate mechanism and is unaffected. (GitHub issue #2669).
Matching on the
content-encodingheader itself still works: the header is preserved for matching beforedecompression removes it, exactly as on HTTP/1.1. (An expectation written against
content-encoding: gzipbriefly stopped matching on this pipeline once decompression was added; that is fixed here.)
With gRPC bidi-streaming enabled (
grpcBidiStreamingEnabled), a plain HTTP request sent over HTTP/2 with anunusual header value — a leading space, an embedded
DEL(0x7F), or another control character — is nowreceived and matchable instead of being silently rejected. Enabling that mode routes every HTTP/2 stream
through Netty's multiplex model, where such a request was reset with
RST_STREAM(PROTOCOL_ERROR)before itever reached the matchers, so nothing matched and nothing was logged as received. Because MockServer is a mock
server that users deliberately drive with malformed traffic to test their own clients, these requests are now
accepted and recorded, matching the behaviour on HTTP/1.1 and on the default (non-multiplex) HTTP/2 pipeline.
This leniency applies to inbound requests only: response header names MockServer sends are still validated
as before, exactly as on the default HTTP/2 pipeline, so a malformed response header name is rejected rather
than put on the wire. (GitHub issue #2669).
Over HTTP/2 the two ways of ending a connection now behave distinctly instead of collapsing into a single
stream reset (see the behaviour-change note above). Every HTTP/2 stream is its own child of the shared TCP
connection.
A per-expectation
closeSocket/closeChannel(and theslowCloseDelayconnection-lifecycle chaos) nowcorrectly ends only that request's stream, leaving other concurrent requests on the same connection to
complete normally — whereas the
resetMidResponseconnection-lifecycle chaos fault, whose purpose is tosimulate a real server socket abort, now resets the whole TCP connection (aborting every concurrent stream on
it) rather than quietly degrading into a single-stream reset. Previously
resetMidResponseover multiplexHTTP/2 emitted only an ordinary stream reset — a fault meant to simulate a crashed socket silently became
something weaker. Behaviour over HTTP/1.1 and over the default (non-multiplex) HTTP/2 pipeline is unchanged.
(GitHub issue #2669).
The MockServer dashboard is no longer unreachable over HTTP/2 — a browser (or any client) requesting
/mockserver/dashboardand its assets over HTTP/2 hung indefinitely and never received a response. Thedashboard handler writes its response straight to the channel (it does not go through the normal response
writer), and it never copied the request's HTTP/2 stream id onto that response. On the shared HTTP/2
connection Netty then routed the response head onto a fresh server-initiated stream instead of the client's
own stream, so the response was never delivered and the client waited until it timed out. Nothing failed or
was logged server-side, which is why it went unnoticed. The handler now stamps the request's stream id onto
every response it writes (matching the metrics endpoint, which shares the same direct-write pattern); this is
a no-op on HTTP/1.1, where the dashboard already worked.
MockServerContainer(the Testcontainers integration) now waits until MockServer is actually servingbefore
start()returns, so a request issued immediately afterwards is no longer reset. The container waitedwith a listening-port strategy, which is satisfied the instant the mapped port accepts a TCP connection — but
MockServer's Netty listener binds the port early and then accepts-then-resets connections until initialisation
finishes.
start()therefore returned inside that window (measured at ~0.2–0.3s wide against the releasedimage, and wider on a loaded host), and the first request could fail with
SocketConnectionException: Channel handler removed before valid response has been received. The wait is now an HTTP readiness probe againstPUT /mockserver/statusreturning 200, which only happens once the request pipeline is fully initialised, sostart()returning now means "ready to serve". This most affects callers on busy/CI hosts, where CPUcontention widens the race. (
withServerPort(...)re-targets the probe at the chosen port.)A streaming
httpLlmResponseexpectation that setscompletion.streamingPhysics.timeToFirstTokenis nolonger rejected with an HTTP 400 and a confusing error naming
org.mockserver.model.Delay— a type the usernever wrote. The expectation serialised cleanly on the client (a raw
Delayserialises to exactly the samebytes its
DelayDTOproduces), so the invalid JSON was only rejected server-side on deserialisation, whereJackson tried to construct a raw
Delay(which has no default constructor and no creator) instead of goingthrough
DelayDTO. This serialise-succeeds / deserialise-fails asymmetry is why it was invisible inclient-side tests.
timeToFirstTokennow crosses the wire through the DTO layer like every otherDelay(via new
CompletionDTO/StreamingPhysicsDTOboundaries wrapping it inDelayDTO); the serialised JSON isbyte-for-byte unchanged, so existing stored expectations and clients keep working. (GitHub issue #2668).
Two (or more) concurrent streaming responses (
httpSseResponse, and streaminghttpLlmResponsewithcompletion.streaming:true) over a single HTTP/2 connection no longer cause one stream to hang forever. Allnon-gRPC HTTP/2 traffic is multiplexed over one shared
HttpToHttp2ConnectionHandler, which picked theoutbound target stream from a single mutable field updated only when a response head was written; a bare
data chunk carries no stream id, so once a second stream wrote its head every later chunk of the first
stream was mis-routed onto the second (by then often already-closed) stream — the server logged
IllegalArgumentException: Stream no longer exists, the terminal frame never reached the first stream, andits client waited on a stream that would never end. Streaming data frames now carry their originating stream
id out-of-band (a
StreamAddressedHttpContentwrapper) so each frame is written onto its own stream, andinterleaved concurrent streams each receive their full body and their own
END_STREAM; a streaming writethat fails for any reason now still ends its own stream so a client is never left hanging.
httpLlmResponsestreaming, which is served through the same handler, wasequally affected and is fixed by the same change. (GitHub issue #2667).
A JSON body expectation built through a client (e.g.
json("{\"amount\":275.0}", MatchType.ONLY_MATCHING_FIELDS))no longer fails to match a byte-identical request. A whole-number double such as
275.0was silently corruptedto the bare integer
275when the expectation was serialised, before any request even arrived: the JSON bodyserializers parsed the value with
USE_BIG_DECIMAL_FOR_FLOATSinto aBigDecimal, and Jackson'sSTRIP_TRAILING_BIGDECIMAL_ZEROES(on by default since 2.15) stripped the trailing zero at node construction,so re-serialisation emitted an integer literal. The server then parsed
275as an integer while the request's275.0stayed a double, and json-unit correctly reports those as different — so the expectation never matchedthe very request it was created for. The serializers now keep
BigDecimals exact(
JsonNodeFactory.withExactBigDecimals(true)), so275.0survives as275.0and genuine integers such as1are left untouched. This also completes the original intent of the earlier
USE_BIG_DECIMAL_FOR_FLOATSfix(#1740), which was meant to preserve decimals such as
0.00but — because the same stripping was already activeat that time — had never actually done so. (GitHub issue #2658).
Streaming responses (
httpSseResponse, and streaminghttpLlmResponsewithcompletion.streaming:true)no longer wrongly close the connection at end of stream.
finishStreamalways closed by default —closeConnectiondefaults to null and the oldnull || truetest made "unset" mean "always close" — whilethe response head unconditionally advertised
Connection: keep-alive. So an HTTP/1.1 client that reused theconnection the response had promised it could keep got a
RemoteDisconnectedon its next request, and onHTTP/2 (where every non-gRPC stream is multiplexed onto one connection channel) the
ctx.close()emittedGOAWAY and tore down the whole connection, killing sibling streams. The end-of-stream decision now mirrors
the non-streaming path: an explicit
closeConnectionstill wins on HTTP/1.1, otherwise the request'skeep-alive intent decides and
alwaysCloseSocketConnectionsstill forces a close; an HTTP/2 request nevercloses the shared parent connection (its terminal frame ends only that stream); and the
Connectionheadernow reports the decision that is actually taken instead of always claiming keep-alive. (GitHub issue #2641).
Note: fully-interleaved concurrent HTTP/2 streaming over the single-connection HTTP/2 path was a separate
limitation (bare content frames were routed by a single current stream id) and is now fixed too — see #2667.
Request bodies sent as
application/yaml,application/x-yamlorapplication/graphqlare no longercorrupted. None of those subtypes were in
MediaType.isString(), so the body was stored as aBinaryBodyand
getBodyAsString()handed back base64 — silently mangling every YAML specification and GraphQL SDLdocument sent with its natural content type, on the control plane (
PUT /mockserver/graphqlrejected itsown documented example this way, and
PUT /mockserver/openapidid the same for a YAML spec) and in userrequest matchers alike.
application/yamlis the media type registered by RFC 9512; all three are UTF-8text with neither a
texttype nor a+json/+xmlsuffix to fall back on.PUT /mockserver/contractTestno longer reports success for a run that verified nothing.allPassedwascomputed as
passed == results.size(), which is vacuously true for an empty result set, so a contract testwhose
operationIdfilter was mistyped or had gone stale against a renamed operation returned{"totalOperations": 0, "allPassed": true}. An empty run is nowallPassed: falseand carries anerrornaming why nothing ran.
The published OpenAPI examples for
PUT /mockserver/asyncapi/http,PUT /mockserver/loadScenario/generateFromOpenAPIand
PUT /mockserver/contractTestnow work as published. The AsyncAPI endpoint declared no request-bodyexample at all (so the generated Postman/Bruno collections sent an empty body and the endpoint rejected it),
and the other two fetched their specification from a remote URL, so the example failed anywhere without
egress. Both now carry an inline specification.
With gRPC bidi-streaming enabled (
grpcBidiStreamingEnabled), the HTTP/2GOAWAYdrain signal now actuallyreaches the client. Enabling that mode switches the HTTP/2 pipeline to Netty's multiplex model, where each
stream is handled on its own child channel. The code that emits a connection-level
GOAWAYlooked for theHTTP/2 connection handler only on the local channel — but on a multiplex child channel that handler lives on
the parent connection channel, so the lookup found nothing and the
GOAWAYwas silently dropped. Two"tell the client to drain" signals stopped working as a result: the graceful drain
GOAWAYsent while theserver is preempting/shutting down (so HTTP/2 clients stop opening new streams and retry elsewhere), and the
http2GoAwaychaos experiment. In both cases the server believed it had signalled while the client neverlearned, with nothing logged. The emitter now walks up to the parent connection channel when needed, so the
GOAWAYis written on the connection where it belongs. The default (non-multiplex) HTTP/2 path and HTTP/1.1are unaffected. (GitHub issue #2669).
Changed
PUT /mockserver/loadScenario/generateFromRecordinganswers 409 rather than 400 when there is norecorded traffic to convert; 400 now means only that the request is malformed (missing
name, an invalidmode, or a scenario that fails validation). The body was always well-formed in the no-traffic case — onlythe state was missing — so 400 told callers their request was wrong and gave them no way to tell that apart
from "record some traffic through the proxy first". The core throws a dedicated
LoadScenarioFromRecording.NoRecordedTrafficException, which extendsIllegalArgumentExceptionso existingcallers that catch it keep working. This was the last entry in the collection gate's
KNOWN_FAILINGratchet, which is now empty: every example the published Postman/Bruno collections carry is accepted by a
default container.
PUT /mockserver/verifySLOanswers 403 rather than 400 when SLO tracking is disabled(
sloTrackingEnabled=false); 400 now means only that the criteria are malformed. The two cases previouslyshared 400, so a caller could not tell a typo in its criteria from a server with the feature switched off —
every client library papered over it with a combined "invalid criteria (or SLO tracking disabled)" message.
403 matches
PUT /mockserver/loadScenario/start, which is the same situation, and the feature-disablederror the clients already model. All nine clients now distinguish the two: Go returns
FeatureDisabledErroron 403 and a criteria error on 400, PHP throws
FeatureNotEnabledExceptionon 403, Java throwsIllegalStateExceptionon 403 and keepsIllegalArgumentExceptionfor 400, and the Python, Ruby, Node,Rust, .NET and dashboard clients give distinct messages for each.
Added
watchInitializationJsonPollPeriodMillisproperty (system property-Dmockserver.watchInitializationJsonPollPeriodMillis,env var
MOCKSERVER_WATCH_INITIALIZATION_JSON_POLL_PERIOD_MILLIS, properties-file key,Configurationinstance setter, and
PUT /mockserver/configuration). It controls how often a watchedinitializationJsonPath/initializationOpenAPIPathfile (whenwatchInitializationJson=true) is polledfor changes; default
5000(5 seconds) — unchanged from the previously hard-coded value — lower it forfaster live reloads or raise it to reduce polling overhead. Previously this interval was a process-wide
mutable static with no supported configuration route, only reachable through internal test-only setters.
Those four accessors (
FileWatcher.get/setPollPeriodandget/setPollPeriodUnits) are removed — they werepublic staticon an internal persistence class, were never a documented configuration route, and had noconsumers outside MockServer's own tests; the property above replaces them. The shared mutable static was
also a real defect: two test classes shortened it and restored it concurrently, so whichever finished first
reinstated the 5-second default under the other, which is what reddened master builds 6914/6918 and PR #2655.
ORCAROUTERis now a supported LLM provider. OrcaRouter (api.orcarouter.ai) is an OpenAI-chat-compatibleAI gateway that fronts many upstream models with vendor-prefixed model ids, so it is wired exactly like
OPENROUTER: it produces the OpenAI Chat Completions wire format, is detected from its host on proxiedtraffic, and prices vendor-prefixed model ids through the underlying vendor's table. This completes community
contribution #2545 by adding
ORCAROUTERtothe two consumer-facing published contracts the server-side change had left out — the published OpenAPI
specification's
httpLlmResponse.providerenum and the Node client'sLlmProvidertype union — so theprovider is reachable from generated tooling and the TypeScript client, and by documenting it on the LLM
response mocking page.
PUT /mockserver/retrievenow accepts an expectation id ({"id": "..."}) in the request body instead of a requestmatcher, matching what
PUT /mockserver/clearandPUT /mockserver/verifyhave always accepted (GitHub issue #2591).For
?type=active_expectationsthis returns only the expectation with that id — filtering on the id itself, not onthe request definition it resolves to, so it neither returns every other expectation whose matcher matches the same
request nor misses expectations (OpenAPI, schema or regex matchers) whose own definition does not match their own
matcher. For
requests,request_responses,recorded_expectationsandlogsit returns the entries matching thatexpectation's request, exactly as verify by expectation id matches them. An unknown id is rejected with
400 No expectation found with id ...rather than silently matching everything. The expectation-id body and therequest-matcher body are unambiguous — both JSON schemas set
additionalProperties: falseand only the expectationid schema allows (and requires) an
idproperty — so existing request-matcher and empty bodies are unaffected. Allnine clients gained the corresponding calls: Java
retrieve*ById(...), Noderetrieve*ById(...), Python/Rubyretrieve_*_by_id(...), GoRetrieve*ByID(...), Rustretrieve_*_by_id(...), PHPretrieve*ById(...)and.NET
Retrieve*ById(...).(
jekyll-www.mock-server.com/mockserver-openapi.yaml, schemaHttpLlmResponse.provider) and the Node client'sLlmProvidertype union (mockserver-client-node/mockServer.d.ts) — is now pinned to the server'sProviderenumby
ProviderConsumerContractEnumParityTest. Previously a new provider could be added toProviderand everyserver-side registration point while both published contracts were left behind, and nothing failed: the existing
internal-schema parity test does not read these files, and the Node drift test compares the
.d.tsonly against theOpenAPI spec, so the two consumer copies could drift from the server in lock-step yet still agree with each other.
Both consumer artefacts are now driven from the one authority (the compiled
Providerenum), in both directions, andeach failure message names the missing provider, the exact file, and how to fix it.
(
.github/workflows/pr-tests.yml). Buildkite — the primary CI — deliberately does not build fork PRs(
build_pull_request_forks: false) because its EC2 agents carry the signing key and every publish credential, socommunity PRs previously got no test feedback at all. The new workflow runs the full Maven reactor
clean install(Surefire unit tests and Failsafe integration tests, including the Docker-gated Testcontainers suites, which execute
against local emulator containers on the Docker-enabled hosted runner — no cloud credentials) plus the standalone
examples build, and uploads the failing tests' reports as an artifact so the contributor can see which test failed
and why. It is secret-free by construction —
on: pull_request(neverpull_request_target), nosecrets.*reference, workflow-level
permissions: {}with the one job granted onlycontents: read, and GitHub-ownedSHA-pinned actions only — so it is useless to an attacker who controls the code it runs. It runs only on fork PRs
(where Buildkite adds nothing) and on manual
workflow_dispatch; in-repo branches keep their full Buildkitepipeline. The three privileged transparent-proxy end-to-end suites are excluded explicitly here — by name, via
-Dfailsafe.excludesFile, which preserves the POM includes and drops only those classes — because they build--cap-add=NET_ADMIN/--privilegedsibling containers a hosted runner cannot be relied on to support, and theironly code-level gate (
DockerCliTestSupport.isDockerAvailable()) is true onubuntu-latest(Maven has noRUN_TRANSPARENT_PROXY_E2Egate — that is a Buildkite shell-step switch only); they, and the client-language/UI/Helmpipelines, remain Buildkite's responsibility. The Docker-gated Testcontainers suites that do run are paired with a
fail-closed
assert-suite-ran.shcheck, so a runner without a usable Docker daemon reds the job rather than passinggreen on skipped coverage.
PR head commit has genuinely passed, via a new GitHub Actions workflow (
.github/workflows/dependabot-auto-merge.yml).It was chosen deliberately over GitHub's native auto-merge: native auto-merge waits only on the branch-protection
required-status-check list, and in this repo Buildkite and both Snyk scans are not required checks, so native
auto-merge would merge the instant the required checks were green while the build or the vulnerability scanner was
still pending or red — and a newly-added check stays invisible to it until someone edits the list. Instead the workflow
reads the checks that actually exist by aggregating both GitHub APIs — the Check Runs API
(
/commits/{sha}/check-runs, which on a Dependabot head carries CodeQL and the languageAnalyzeruns) and thelegacy Commit Status API (
/commits/{sha}/status, which carriesbuildkite/mockserverand both Snyk scans) — andrequires them to pass. Commit statuses must be
success— the legacy status API has onlyerror/failure/pending/success, so the security-critical contexts cannot report a non-blocking state. Check runs may beskippedorneutralwithout blocking (the fork-only test workflow is always skipped on an in-repo PR, so requiring literalsuccess from every run would merge nothing), but at least one genuine success is still required. It fails closed on
every other ambiguity: any pending, failed, cancelled or unrecognised result, zero checks found (the empty-set
trap), an API error, or unknown mergeability all refuse the merge with a logged reason. It merges only PRs authored by
dependabot[bot]whose head branch is in thisrepository (not a fork), and only two branch classes: minor/patch group updates and Docker digest bumps. For
the first,
.github/dependabot.ymlgroups minor+patch into*-minor-and-patchgroups and excludes majors, so a majorarrives as an ungrouped single-dependency branch that fails the group-branch check — a structural, spoof-resistant
signal because only Dependabot creates those in-repo branches. For the second, a Docker image-digest bump (a new SHA
on an unchanged tag) carries no semver and so joins no group; it is now accepted directly, but only for the
distroless runtime bases — the one image class that, being pinned on non-semver tags, has only ever moved by digest in
this repo's entire Dependabot history — and only when two independent Dependabot-generated signals agree: the
branch is a
.../distroless/<image>-<hex-sha>path and the PR title is Dependabot's digest-bump shape (bump … from <hex-sha> to <hex-sha>). A version or tag bump matches neither (its branch and title carry bare versions, not hexshas), and the two-signal AND fails closed, so a major or tag change can never be auto-merged. The semver-tagged
images (
alpine,ubuntu,eclipse-temurin,grafana/k6) are deliberately excluded from the digest path — safetyover completeness.
.github/dependabot.ymlis unchanged.The workflow runs on a
schedulesweep plusworkflow_dispatch(neverpull_request/pull_request_target), checksout no PR code, uses no third-party actions and no
secrets.*, and holds only job-levelcontents: write+pull-requests: writeunder a workflow-levelpermissions: {}. It ships withdry_rundefaulting ON so it can betrialled — logging each decision without merging — until the schedule default is flipped to live.
Changed
.github/workflows/dependabot-auto-merge.yml) is now live on the schedule:DEFAULT_DRY_RUNwas flipped from'true'to'false', so the scheduled sweep actually merges eligible PRs ratherthan only logging its decisions. The
workflow_dispatchdry_runinput still defaults totrue, so a manual runstays a safe dry-run probe unless the operator explicitly sets it false. Arming it is backed by a new standing guard
(Rule 6 in
.buildkite/scripts/steps/check-false-green-guards.sh) that fails the build if any of the fourload-bearing properties keeping majors and version bumps out of auto-merge is quietly weakened: it parses the three
acceptance regexes out of the workflow and asserts the digest-branch regex stays anchored, confined to
dependabot/docker/, keeps its literal/distroless/scope and a trailing hex-run floor of at least 7 (without thescope a date-tagged bump such as
bump ubuntu from 202401151200 to 202402201200, whose date tokens are all hex, would matchthe title regex); the digest-title regex requires a
[0-9a-f]{7,}run on both the from and to sides; path B stillcross-checks the title as a conjunct of the branch shape and fails closed on mismatch; and every group in
.github/dependabot.ymlwhose branch name path A accepts declaresupdate-typeswithin{minor, patch}, so a majorcan never ride a branch literally named
*-minor-and-patch. The guard fails closed if the workflow is missing, aregex cannot be extracted, path B cannot be located, or no group is accepted by path A.
typescript-eslint's peer range istypescript >=4.8.4 <6.1.0, so a TypeScript 7 bump failsnpm ciwithERESOLVEbefore lint or typecheck can run,and nothing else in the tree raises that ceiling — the upgrade is unmergeable until
typescript-eslintshipssupport for the new major. The
ignorecarries the reason and an explicit revisit condition, so it is a recordeddecision rather than silent suppression.
mockserver/pom.xmlare now mirrored into every Dependabot block whose module inherits that parent —
/examples/javaand/mockserver/mockserver-maven-plugin. Because those child directories resolve up into the parent pom, Dependabotscanning them could bump a parent-declared dependency and brand the PR with the child scope, sidestepping an ignore
that existed only in the
/mockserverblock. That is exactly how the un-passablecheckstyle 12.3.1 -> 13.10.0PR #2554 was generated from
/examples/java(checkstyle 13 ships Java 21 bytecode; the project floor is Java 17).The
checkstyle >= 13.0.0andgraphql-javaceilings are now present in all three parent-reaching blocks;the module-only
infinispan-coreignore is deliberately not mirrored because it is unreachable from those scopes.Each block carries a
SYNC-WITH-PARENTnote so future ignores are added in every parent-reaching block.typescript >= 7.0.0ignoreadded on 2026-08-17 was placed on the npm block shared by
/mockserver-ui,/mockserver-client-nodeand/mockserver-node, but its rationale (atypescript-eslintpeer conflict) applies only to the UI.mockserver-client-nodewas already on typescript~7.0.2— above the ceiling — and carries notypescript-eslint, so the ignore silently froze every typescript update for that published package./mockserver-uinow has its own block carrying the ignore, mirroring how/mockserver-vscodewas alreadysplit for the same reason: a directory-specific constraint must not leak onto directories it does not apply to.
graphql-javaupdate, including security patches.The ignore read
>= 25.0.0whilemockserver/pom.xmlhad already moved to26.0, so the in-use version was itselfabove the ceiling and no candidate could ever be proposed. This was found while mirroring the ignore into the two
parent-reaching blocks — the mirror would have propagated the fault rather than the control. Raised to
>= 27.0.0in all three blocks and documented the invariant inline: a ceiling must stay above the version in use, because
one at or below it is indistinguishable from a working ceiling — no pull requests appear either way. The stale
"22.x line" comments in
mockserver/pom.xmland the< 25.0.0row indocs/operations/security.mdare correctedto match the 26.x reality.
org.jspecify:jspecifyversion (1.0.1) is now pinned inmockserver/pom.xml<dependencyManagement>so
maven-enforcer'sDependencyConvergencestays green across Guava and graphql-java bumps. Guava33.7.0-jrepulls jspecify
1.0.1whilegraphql-java -> java-dataloader:6.0.0pulls1.0.0; with no managed version the twotransitive paths diverge the moment Guava moves, which is why Dependabot PR #2555 (
guava 33.6.0 -> 33.7.0) failedthe enforcer. jspecify is an annotation-only artifact and
1.0.1is a backward-compatible patch that neitherconsumer rejects, so the higher line is pinned.
Removed
org.mock-server:mockserver-examplesis no longer published to Maven Central, and theorg.mockserver.examplesJPMS automatic module is no longer resolvable. This artifact was only ever sample codedemonstrating client and proxy usage — not a supported consumer dependency — but it had been GPG-signed and deployed
to Maven Central on every release (it was a
<module>of themockserverreactor, and the release deploy runsmvn deploy -P releaseover that whole reactor with no-plrestriction). It has now been removed from themockserverreactor, so the release deploy no longer reaches it and no new versions will appear on Central
(
skipPublishing=truein its POM guards against a future re-add). Previously published versions (7.6.0 and earlier)remain available and resolvable forever — only new releases are affected. Anyone who declared a dependency on
mockserver-examplesorrequires org.mockserver.examplesshould pin the last published version or, preferably,copy the sample code (
examples/java/) into their own project. This is markedBREAKING BEHAVIOURrather than plainBREAKINGbecause it does not force a major version bump: the artifact was never a supported dependency, no shippedMockServer artifact depends on it, and every previously published version stays available.
Fixed
Fixed an order-dependent failure in the blob-store registrar tests, surfaced by running the suite on hardware
with a different class ordering.
S3ExpectationPersistenceReloadTeststarts a real server withblobStoreType("s3"), which makesStateBackendFactorydiscover and register the s3 factory in its JVM-globalregistry, and its teardown stopped the server and the MinIO container without resetting that registry. Every test
class in the module shares one reused fork under surefire's default
filesystemrun order, which is not stableacross machines — so whenever that class happened to run first,
S3BlobStoreRegistrarTest's "s3 should not beregistered before register()" precondition saw inherited state and failed. The leak is now cleaned up on
teardown, and all three registrar tests (s3, gcs, azure) scrub the registry on entry as well as exit, so the
precondition holds regardless of what ran before them. The gcs and azure tests had the identical latent shape and
passed only because their modules contain no equivalent leaker. Reproduced by forcing the losing order
(
-Dsurefire.runOrder=reversealphabetical) and confirmed fixed under it.The new fork-PR test workflow (
.github/workflows/pr-tests.yml) now reports one red per real failure instead ofthree. A latent race in
ThirdPartyStreamingClientConformanceIntegrationTestsurfaced on the slowerubuntu-latestrunner:shouldDeliverMessageThenCloseWhenCloseConnectionIsSetis the one case whose server isconfigured
withCloseConnection(true)(it delivers"bye"and closes immediately, by design), yet it asserted onJava-WebSocket's
connectBlocking()boolean, which returnsconnectLatch.await(...) && engine.isOpen()— and thatlatch is counted down by both
onWebsocketOpenandonWebsocketClose, so the deliberate close races theisOpen()read and the bare assert fails in ~12 ms. It now asserts the observable handshake outcome via theclient's
openedlatch (onOpenalways precedes any close on a successful handshake); the close, the"bye"payload, and the orderly close code were already verified race-free and are preserved. The product is unchanged —
withCloseConnection(true)did exactly what it promises. Two derivative reds in the same workflow are gone too:the standalone-examples step and the "assert Docker-gated suites actually ran" step are now gated on the reactor
step succeeding (
steps.reactor.outcome == 'success') rather than merely!cancelled(), so a reactor failure nolonger makes the examples build die with "Could not resolve dependencies for … mockserver-examples" (the reactor's
installnever completed) or makes the suite-ran check red onmockserver-blob-s3with "the suite did not run"(S3 was only skipped because of its test-scope reactor dependency on the failed
mockserver-netty, two modulesaway). The suite-ran check stays fail-closed for the case it exists for: a runner without Docker
assumeTrue-skipsthe suites, Maven still exits 0, so the reactor step succeeds, the check runs, finds no report, and reds loudly.
Dependabot now proposes grouped Maven updates for the core
mockserverreactor again. The reactor'smockserver/pom.xmldeclared<module>../examples/java</module>, a path that escaped Dependabot'sdirectory: "/mockserver"scope; with no repository-rootpom.xml, Dependabot re-parsed a scoped file subset,found no top-level pom and aborted every grouped-update run for the reactor with
No pom.xml!. As a result nogrouped Maven PR was ever created for the core modules (broken since the examples were unified in
d3cfa9aaf),while the self-contained
mockserver-maven-pluginsibling kept working, andexamples/java's own dependencies hadno coverage at all.
examples/javais now removed from the reactor so/mockserveris a self-contained Mavendirectory; the examples are kept compiled and tested by a second standalone Maven invocation in CI immediately after
the reactor
install(scripts/buildkite_quick_build.sh), and their own dependencies are covered by a new/examples/javablock in.github/dependabot.yml. (Vulnerability alerts were never affected — those come from thesubmitted dependency graph, not update proposal.)
The release pipeline can no longer silently overwrite the previous version's archived documentation site. Whether a
release creates a new versioned subdomain (
X-Y.mock-server.com) is fully determined by whether it is a major/minorrelease, but it was a free operator dropdown (
create-versioned-site) defaulting tono. On the 7.6.0 release thatdefault was left in place, so terraform's
latest_versionstill pointedmainat the previous version's bucket andthe docs publish (
aws s3 sync --delete) destroyed the7-5.mock-server.comarchive. The value is now derivedfrom
RELEASE_VERSIONvs the previous tag inrequire_release_inputs(the single input-validation chokepoint everyrelease script runs, before
prepare.shtags or pushes); the Buildkite input defaults toauto, and an explicityes/nois honoured only as a confirmation that must agree with the derived value or the run fails closed.This makes both destructive mistakes impossible: a major/minor release with
no(overwrites the previous archive)and a patch release with
yes(spurious subdomain).Removed a live-internet dependency from the
mockserver-coreunit tests.ExpectationSerializerTest(
shouldAllowSingleOpenAPIObjectForArrayandshouldAllowMixedExpectationTypesForArray) referenced the bundledpetstore OpenAPI spec via a
https://raw.githubusercontent.com/...URL; becausedeserializeArray()actually loadsthe spec to generate example bodies, every ordinary build fetched that URL over the internet. A transient HTTP 429
(rate limit) from
raw.githubusercontent.comfailed the test — and took down the 7.6.0 release build after it hadalready tagged and pushed. Both tests now resolve the byte-identical copy from the test classpath
(
org/mockserver/openapi/openapi_petstore_example.json, the same local reference the siblingHttpStateTestandJsonSchemaExpectationValidatorTestalready use), so the suite no longer depends on GitHub being reachable orun-rate-limited. The behaviour under test (a single OpenAPI object deserialising into an array of expectations with
the correct generated bodies) is unchanged and still fully asserted.
The Rust client's
cargo clippygate no longer fails the build on an unchanged source tree. Themockserver-rustpipeline lints inside a container pulled from the floatingrust:1tag, and that tag moved toclippy 1.98.0, whose
clippy::needless_late_initnow also flags late-initialisedletchains assigned across anif/else ifsequence.resolve_platform()inmockserver-client-rust/src/launcher.rshad two such chains(
os_name/ext, andarch), so with-D warningsthe lint became a hard error and the crate stopped compiling —on code nobody had touched. Master build #638 passed this exact source; build #639, fifteen hours later on a newer
rust:1, failed it, which also red-herring-failed an unrelated Dependabot pull request whose only change was to aJava dependency. Both chains are now initialising
if/elseexpressions (a tuple for the jointly-assignedos_name/extpair), exactly the rewrite clippy itself suggests; the divergingelsearms stillreturn Err(...)and coerce as
!. Behaviour is byte-for-byte identical — same platform tokens, same error messages, samePlatform— and was confirmed by running the fix under the real clippy 1.98.
Configuration
📅 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.