Consider a Spring Web MVC controller method that calls some downstream http service:

@PostMapping("/hello")
public String hello() {
    return new RestTemplate().postForObject("http://localhost:8888", "hello", String.class);
}

Starting from Spring Boot 3.4.0 (Spring Web 6.2.0), if calling the downstream service produces a "Connection reset" error, it is silently ignored. This endpoint would return 200 OK in such case.

Before Spring Web 6.2.0, calling this endpoint http://localhost:8080/hello would result in HTTP error code 500, with the following logs:

2025-01-15T12:47:57.658+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : POST "/hello", parameters={}
2025-01-15T12:47:57.658+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication#hello()
2025-01-15T12:47:57.660+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.web.client.RestTemplate              : HTTP POST http://localhost:8888
2025-01-15T12:47:57.660+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.web.client.RestTemplate              : Accept=[text/plain, application/json, application/*+json, */*]
2025-01-15T12:47:57.660+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.web.client.RestTemplate              : Writing [hello] with org.springframework.http.converter.StringHttpMessageConverter
2025-01-15T12:47:57.662+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:8888": Connection reset
2025-01-15T12:47:57.662+01:00 ERROR 15980 --- [demo] [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:8888": Connection reset] with root cause

java.net.SocketException: Connection reset
    at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:318) ~[na:na]
        ...
2025-01-15T12:47:57.663+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for POST "/error", parameters={}
2025-01-15T12:47:57.664+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2025-01-15T12:47:57.664+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json]
2025-01-15T12:47:57.664+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Wed Jan 15 12:47:57 CET 2025, status=500, error=Internal Server Error, path=/hello}]
2025-01-15T12:47:57.665+01:00 DEBUG 15980 --- [demo] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500

After Spring Web 6.2.0 (Spring Boot 3.4.0), the logs look as follows, note the line "Looks like the client has gone away" — it's coming from DisconnectedClientHelper that assumes for some reason that the "Connection reset" exception comes from the client. Note that the exception is completely ignored, and the endpoint returns 200 OK:

2025-01-15T12:42:28.211+01:00 DEBUG 15887 --- [demo] [nio-8080-exec-7] o.s.web.servlet.DispatcherServlet        : POST "/hello", parameters={}
2025-01-15T12:42:28.213+01:00 DEBUG 15887 --- [demo] [nio-8080-exec-7] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication#hello()
2025-01-15T12:42:28.214+01:00 DEBUG 15887 --- [demo] [nio-8080-exec-7] o.s.web.client.RestTemplate              : HTTP POST http://localhost:8888
2025-01-15T12:42:28.215+01:00 DEBUG 15887 --- [demo] [nio-8080-exec-7] o.s.web.client.RestTemplate              : Accept=[text/plain, application/json, application/*+json, */*]
2025-01-15T12:42:28.215+01:00 DEBUG 15887 --- [demo] [nio-8080-exec-7] o.s.web.client.RestTemplate              : Writing [hello] with org.springframework.http.converter.StringHttpMessageConverter
2025-01-15T12:42:28.216+01:00 DEBUG 15887 --- [demo] [nio-8080-exec-7] o.s.w.s.handler.DisconnectedClient       : Looks like the client has gone away: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:8888": Connection reset (For a full stack trace, set the log category 'org.apache.commons.logging.LogAdapter$Slf4jLocationAwareLog@4d9f6662' to TRACE level.)
2025-01-15T12:42:28.216+01:00 DEBUG 15887 --- [demo] [nio-8080-exec-7] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

A simple reproducer follows.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

@SpringBootApplication
@Controller
public class DemoApplication {

    @PostMapping("/hello")
    public String hello() {
        return new RestTemplate().postForObject("http://localhost:8888", "hello", String.class);
    }

    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            try (ServerSocket socket = new ServerSocket(8888)) {
                while (true) {
                    try (Socket clientSocket = socket.accept()) {
                        // simulate TCP connection reset
                        clientSocket.getInputStream().read();
                        clientSocket.setSoLinger(true, 0);
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).start();

        Thread.sleep(2000);

        SpringApplication.run(DemoApplication.class, args);
    }

}

Comment From: quaff

It's a regression introduced by https://github.com/spring-projects/spring-framework/commit/203fa7519692a3d12a4eca73765ba6592b164fd3.

Comment From: forketyfork

@quaff thanks for the quick fix. But wouldn't it only fix the issue for the case when RestTemplate is used? Any other HTTP client might produce a completely different exception.

Comment From: quaff

@quaff thanks for the quick fix. But wouldn't it only fix the issue for the case when RestTemplate is used? Any other HTTP client might produce a completely different exception.

It should works for all http client provided by Spring Framework, like RestTemplate, RestClient, and WebClient.

Comment From: forketyfork

True, but what if I use any other client, e.g., Java HTTP client directly?

Comment From: forketyfork

The DisconnectedClientHelper docs say:

The utility methods help to log a single line message at DEBUG level, and a full stacktrace at TRACE level.

So the point here is to reduce the logging, and this heuristic makes sense in most of the cases. And even for false positive cases, not logging the exception might not be that critical. But the controller method also returns 200 OK in such case, and I think that's the real problem, as it breaks the expected behavior.

Comment From: quaff

The DisconnectedClientHelper docs say:

The utility methods help to log a single line message at DEBUG level, and a full stacktrace at TRACE level.

So the point here is to reduce the logging, and this heuristic makes sense in most of the cases. And even for false positive cases, not logging the exception might not be that critical. But the controller method also returns 200 OK in such case, and I think that's the real problem, as it breaks the expected behavior.

I agree with you, I appended another commit to fix it, let's wait for team member's response.

Comment From: rstoyanchev

The fix for #33064 changed "Connection reset by peer" to "Connection reset", which is too broad and matches server side client network issues in addition to the server losing its main connection.

Looking at the use case there, ClientAbortException is in the cause chain, but DisconnectedClientHelper doesn't check the type name of the outermost exception. I think we can change that, and restore "Connection reset by peer".