While working on https://github.com/spring-projects/spring-framework/issues/35530, I figured out that with Jetty and Java 25, MultipartWebClientIntegrationTests#transferTo
is always broken.
MultipartWebClientIntegrationTests
is known to be flaky, so there may be something to fix in the client multipart support, but that's now straightforward, so this issue is a reminder for this test that will be skipped for now.
Comment From: Lublanski
Hi @sdeleuze , I’d like to take a look at this issue.
My plan is:
1. Switch to the java25
branch and run the MultipartWebClientIntegrationTests
with JDK 25.
2. Reproduce the failure of #transferTo
and investigate the root cause (whether it’s in WebClient’s multipart support or related to Jetty/Java 25).
3. Propose a fix or at least narrow down the problem with a reproducible case.
Is it ok if I work on this?
Comment From: sdeleuze
Hi, yes feel free to have a deeper look. Please share your findings and potential link to your branch here initially, not in a PR.
What java25
branch are you referring to?
Comment From: Lublanski
@sdeleuze Thanks for the clarification!
I’ll start from the main
branch in my fork, create a dedicated branch for investigation, and run the MultipartWebClientIntegrationTests
with JDK 25 + Jetty to reproduce the failure.
I’ll share my findings and a link to my branch here before opening any PR.
Comment From: Lublanski
Hi @sdeleuze I was able to reproduce the failure locally.
With java 24, for example, the test passes, but with java 25 it consistently fails.
As you already described, the failure is on MultipartWebClientIntegrationTests#transferTo when using Jetty, with the following error:
WebClientResponseException: 200 OK ... but response failed with cause:
reactor.netty.http.client.PrematureCloseException: Connection prematurely closed DURING response
This suggests that the response starts correctly (200 OK), but the connection is closed before the body is fully consumed.
I’ll continue investigating whether this comes from Jetty on JDK 25, or from Reactor Netty’s handling of the response, and will share further findings.
Comment From: Lublanski
Hi @sdeleuze , a quick update on my investigation:
With JDK 25, I noticed the following debug log when running MultipartWebClientIntegrationTests#transferTo with Jetty:
13:46:02.707 [InnocuousThread-2] DEBUG o.s.h.s.r.ServletHttpHandlerAdapter - [4048aa2] AsyncEvent onError: java.lang.SecurityException: setContextClassLoader
This seems to happen on InnocuousThread-1, and may explain why the connection is being closed prematurely on the client side (PrematureCloseException). With JDK 24, this does not occur.
I’ll keep digging to confirm whether this is caused by Jetty’s use of setContextClassLoader under JDK 25 restrictions.
Comment From: Lublanski
Hi again @sdeleuze, quick update on this.
I was able to capture the full stacktrace of the failure when running MultipartWebClientIntegrationTests#transferTo with JDK 25 and Jetty:
java.lang.SecurityException: setContextClassLoader
at java.base/jdk.internal.misc.InnocuousThread.setContextClassLoader(InnocuousThread.java:130)
at org.eclipse.jetty.server.handler.ContextHandler.exitScope(ContextHandler.java:754)
at org.eclipse.jetty.server.handler.ContextHandler$ScopedContext.run(ContextHandler.java:1647)
at org.eclipse.jetty.server.handler.ContextResponse$1.succeeded(ContextResponse.java:41)
at org.eclipse.jetty.util.thread.SerializedInvoker$Link.run(SerializedInvoker.java:274)
at org.eclipse.jetty.util.thread.SerializedInvoker.run(SerializedInvoker.java:174)
at org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.succeeded(HttpChannelState.java:1359)
at org.eclipse.jetty.server.internal.HttpConnection$SendCallback.onCompleteSuccess(HttpConnection.java:1022)
at org.eclipse.jetty.util.IteratingCallback.onCompleted(IteratingCallback.java:264)
at org.eclipse.jetty.server.internal.HttpConnection$SendCallback.onCompleted(HttpConnection.java:797)
at org.eclipse.jetty.util.IteratingCallback.doCompleteSuccess(IteratingCallback.java:276)
at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:501)
at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:354)
at org.eclipse.jetty.server.internal.HttpConnection$HttpStreamOverHTTP1.send(HttpConnection.java:1578)
at org.eclipse.jetty.server.HttpStream$Wrapper.send(HttpStream.java:197)
at org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1333)
at org.eclipse.jetty.server.Response$Wrapper.write(Response.java:836)
at org.eclipse.jetty.server.handler.ContextResponse.write(ContextResponse.java:56)
at org.eclipse.jetty.ee11.servlet.ServletContextResponse.write(ServletContextResponse.java:225)
at org.eclipse.jetty.ee11.servlet.HttpOutput.channelWrite(HttpOutput.java:248)
at org.eclipse.jetty.ee11.servlet.HttpOutput$AsyncWrite.process(HttpOutput.java:1643)
at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:377)
at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:354)
at org.eclipse.jetty.ee11.servlet.HttpOutput.write(HttpOutput.java:997)
at org.springframework.http.server.reactive.ServletServerHttpResponse.writeToOutputStream(ServletServerHttpResponse.java:217)
at org.springframework.http.server.reactive.ServletServerHttpResponse$ResponseBodyProcessor.write(ServletServerHttpResponse.java:383)
at org.springframework.http.server.reactive.ServletServerHttpResponse$ResponseBodyProcessor.write(ServletServerHttpResponse.java:353)
at org.springframework.http.server.reactive.AbstractListenerWriteProcessor$State$3.onWritePossible(AbstractListenerWriteProcessor.java:392)
at org.springframework.http.server.reactive.AbstractListenerWriteProcessor.onWritePossible(AbstractListenerWriteProcessor.java:157)
at org.springframework.http.server.reactive.AbstractListenerWriteProcessor.writeIfPossible(AbstractListenerWriteProcessor.java:311)
at org.springframework.http.server.reactive.AbstractListenerWriteProcessor.changeStateToReceived(AbstractListenerWriteProcessor.java:290)
at org.springframework.http.server.reactive.AbstractListenerWriteProcessor$State$2.onNext(AbstractListenerWriteProcessor.java:371)
at org.springframework.http.server.reactive.AbstractListenerWriteProcessor.onNext(AbstractListenerWriteProcessor.java:118)
at org.springframework.http.server.reactive.ChannelSendOperator$WriteBarrier.emitCachedSignals(ChannelSendOperator.java:312)
at org.springframework.http.server.reactive.ChannelSendOperator$WriteBarrier.request(ChannelSendOperator.java:282)
at org.springframework.http.server.reactive.AbstractListenerWriteProcessor$State$1.onSubscribe(AbstractListenerWriteProcessor.java:348)
at org.springframework.http.server.reactive.AbstractListenerWriteProcessor.onSubscribe(AbstractListenerWriteProcessor.java:110)
at org.springframework.http.server.reactive.ChannelSendOperator$WriteBarrier.subscribe(ChannelSendOperator.java:358)
at org.springframework.http.server.reactive.AbstractListenerWriteFlushProcessor$State$2.onNext(AbstractListenerWriteFlushProcessor.java:292)
at org.springframework.http.server.reactive.AbstractListenerWriteFlushProcessor.onNext(AbstractListenerWriteFlushProcessor.java:119)
at org.springframework.http.server.reactive.AbstractListenerWriteFlushProcessor.onNext(AbstractListenerWriteFlushProcessor.java:43)
at reactor.core.publisher.StrictSubscriber.onNext(StrictSubscriber.java:89)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2570)
at reactor.core.publisher.StrictSubscriber.onSubscribe(StrictSubscriber.java:77)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55)
at reactor.core.publisher.Mono.subscribe(Mono.java:4571)
at org.springframework.http.server.reactive.AbstractListenerServerHttpResponse.lambda$writeAndFlushWithInternal$0(AbstractListenerServerHttpResponse.java:64)
at reactor.core.publisher.MonoFromPublisher.subscribe(MonoFromPublisher.java:64)
at reactor.core.publisher.Mono.subscribe(Mono.java:4571)
at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:238)
at reactor.core.publisher.FluxConcatArray.subscribe(FluxConcatArray.java:79)
at reactor.core.publisher.Mono.subscribe(Mono.java:4571)
at org.springframework.http.server.reactive.ChannelSendOperator$WriteBarrier.onNext(ChannelSendOperator.java:187)
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:200)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.innerNext(FluxConcatMapNoPrefetch.java:259)
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:865)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondComplete(MonoFlatMap.java:245)
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:305)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.complete(MonoIgnoreThen.java:294)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onNext(MonoIgnoreThen.java:188)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:237)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:204)
at reactor.core.publisher.MonoCreate$DefaultMonoSink.success(MonoCreate.java:144)
at reactor.core.publisher.LambdaSubscriber.onComplete(LambdaSubscriber.java:132)
at reactor.core.publisher.FluxCreate$BaseSink.complete(FluxCreate.java:465)
at reactor.core.publisher.FluxCreate$BufferAsyncSink.drain(FluxCreate.java:871)
at reactor.core.publisher.FluxCreate$BufferAsyncSink.complete(FluxCreate.java:819)
at reactor.core.publisher.FluxCreate$SerializedFluxSink.drainLoop(FluxCreate.java:249)
at reactor.core.publisher.FluxCreate$SerializedFluxSink.drain(FluxCreate.java:215)
at reactor.core.publisher.FluxCreate$SerializedFluxSink.complete(FluxCreate.java:206)
at org.springframework.core.io.buffer.DataBufferUtils$WriteCompletionHandler.completed(DataBufferUtils.java:1259)
at org.springframework.core.io.buffer.DataBufferUtils$WriteCompletionHandler.completed(DataBufferUtils.java:1177)
at java.base/sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:121)
at java.base/sun.nio.ch.SimpleAsynchronousFileChannelImpl$3.run(SimpleAsynchronousFileChannelImpl.java:410)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
at java.base/java.lang.Thread.run(Thread.java:1474)
at java.base/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:148)
14:07:26.035 [reactor-http-epoll-2] DEBUG o.s.w.r.f.c.ExchangeFunctions - [48d31d25] [4d59a85f-1, L:/127.0.0.1:41416 - R:localhost/127.0.0.1:39673] Response 200 OK
14:07:26.083 [reactor-http-epoll-2] DEBUG o.s.c.c.StringDecoder - [48d31d25] [4d59a85f-1, L:/127.0.0.1:41416 - R:localhost/127.0.0.1:39673] Decoded "/tmp/MultipartIntegrationTests12650419040079562514foo.txt"
14:07:26.281 [Test worker] DEBUG o.s.w.t.h.s.r.b.JettyHttpServer - Stopping JettyHttpServer...
14:07:26.287 [Test worker] DEBUG o.s.w.t.h.s.r.b.JettyHttpServer - Server stopped (0 millis).
So the root cause is not in Spring, but in Jetty.
* Jetty calls Thread.setContextClassLoader inside an InnocuousThread.
* With JDK 25 this is no longer permitted and results in a SecurityException.
* This aborts the async response, which is why the client side sees a PrematureCloseException.
This seems to be a Jetty + JDK 25 incompatibility. The Spring test just exposes it. Next step would likely be to raise this upstream with Jetty so they can adjust their use of setContextClassLoader for JDK 25.