Hi Spring-Team,

After upgrading to Spring Boot 3.4.0, we have encountered an issue with RestTemplate where random CancellationExceptions are thrown instead of the expected ResourceAccessException during timeout scenarios.

Observations: This behavior is inconsistent; sometimes a CancellationException is thrown, and other times a ResourceAccessException is thrown. This issue did not occur in previous versions of Spring Boot.

Reproduction: We have created a test case to demonstrate the issue: https://github.com/SimonPNorra/timeout-execption-case

Steps to Reproduce: Run the test case included in the project and observe the exceptions thrown. You will notice either a CancellationException or a ResourceAccessException occurring randomly.

Expected Behavior: Consistent behavior with ResourceAccessException thrown during timeout scenarios.

Actual Behavior: Random exceptions are thrown: either CancellationException or ResourceAccessException.

Please let us know if further information is required.

Thank you.

Regards

Comment From: wilkinsona

Thanks for the sample.

Unfortunately, on my machine, it doesn't reproduce the behavior that you've described. I've tried with both Java 21 and Java 23 and the exception was always a ResourceAccessException. I'm on macOS 14.7.1.

Can you please provide some more information about the OS you're running on and the version and distribution of Java that you're using? Also, please share the full stack trace of both the CancellationException and the ResourceAccessException that you're seeing.

Comment From: SimonPNorra

Hey @wilkinsona

Thank you for looking into this issue. Here is the information about my local machine, which I used to run the example:

Machine Details:

Device: MacBook Pro Processor: Apple M1 Pro Operating System: macOS 15.1.1 (24B91) JDK: azul-21.0.2 (aarch64)

Stack Traces from the Example: (Below are both stack traces as observed from running the test case.)

java.util.concurrent.CancellationException
    at java.base/java.util.concurrent.CompletableFuture.cancel(CompletableFuture.java:2510)
    at java.net.http/jdk.internal.net.http.common.MinimalFuture.cancel(MinimalFuture.java:109)
    at org.springframework.http.client.JdkClientHttpRequest$TimeoutHandler.lambda$new$0(JdkClientHttpRequest.java:234)
    at java.base/java.util.concurrent.CompletableFuture$UniRun.tryFire(CompletableFuture.java:787)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
    at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2179)
    at java.base/java.util.concurrent.CompletableFuture$DelayedCompleter.run(CompletableFuture.java:2931)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    at java.base/java.lang.Thread.run(Thread.java:1583)
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:49111/test": Request timed out
    at org.springframework.web.client.RestTemplate.createResourceAccessException(RestTemplate.java:926)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:906)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:801)
    at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:442)
    at com.example.timeoutexecptioncase.TimeoutExecptionCaseApplicationTests.contextLoads(TimeoutExecptionCaseApplicationTests.java:35)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:767)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$8(TestMethodTestDescriptor.java:217)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:156)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:146)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:144)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:100)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:160)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:146)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:144)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:100)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:160)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:146)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:144)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:100)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:198)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:169)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:93)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:58)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:141)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:57)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:103)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85)
    at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:63)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: java.net.http.HttpTimeoutException: Request timed out
    at org.springframework.http.client.JdkClientHttpRequest.executeInternal(JdkClientHttpRequest.java:124)
    at org.springframework.http.client.AbstractStreamingClientHttpRequest.executeInternal(AbstractStreamingClientHttpRequest.java:71)
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:81)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:900)
    ... 71 more

In addition, this issue first appeared in our application running as a GCP CloudRun service. The service operates using the following base image: FROM gcr.io/distroless/java21-debian12

Let me know if you need more details about the environment!

Comment From: wilkinsona

Thanks for the additional details and stack traces. The change in behavior that you've seen upon upgrading to Spring Boot 3.4 is due to this change in Spring Framework. We'll transfer this issue to the Framework team so that they can continue the investigation.

Comment From: SimonPNorra

Hi there,

I’ve updated my example project to include additional test scenarios:

  1. Unit Test (as included previously)
  2. Test from within the Spring application, which can be executed locally
  3. Test using a distroless Docker image based on gcr.io/distroless/java21-debian12, replicating the execution environment where this issue was originally observed

I hope these additions help with your investigation.

Thank you for your support!

Comment From: aldex32

I am having the same issue as @SimonPNorra

Comment From: jaceko

I'm having the same issue on Linux (JDK 21, Spring boot 3.4.3)

Comment From: cjrohrbach

Hi all I am facing the same Issue on windows with Spring boot 3.4.6.

@SimonPNorra did you by any chance find a workaround for this issue that you would be willing to share with us all?