Spring Boot+Tomcat服务端如何感知客户端请求中断?
Let's break down your scenario and how to handle client disconnect detection on the server, since I've dealt with this exact Tomcat + Spring Boot behavior before:
Default Behavior When Client Disconnects
Your observation is spot-on: when the client hits a read timeout (like your 500ms setting) and drops the connection, Tomcat silently catches the IOException/ClientAbortException at the low level (in AbstractProcessor.action()) and doesn't propagate it up to your Spring controller.
Here's the reasoning behind this design:
- Tomcat is built to handle thousands of concurrent connections gracefully. A client disconnecting mid-request is a common network edge case, and Tomcat doesn't want to clutter your application logs or interrupt business logic with exceptions that aren't the server's fault.
- In your server code, you're sleeping for 1000ms while the client times out after 500ms. By the time your server tries to send the response, the client connection is already closed. Tomcat catches this write failure, logs it at
DEBUGlevel (you won't see it unless you enable debug logging for Tomcat's connector classes), cleans up the connection, and moves on—no exception reaches your controller method.
Why the Exception Doesn't Reach Spring
Tomcat's AbstractProcessor class handles raw socket communication. When it encounters an IOException (like a broken pipe from a dropped client), it:
- Logs the error at DEBUG level (check the
org.apache.coyotelogger if you want to verify this) - Cleans up socket and connection resources
- Does not throw the exception up the call stack to Spring's dispatcher servlet or your controller.
This is intentional behavior to prevent application code from having to handle network-level exceptions that are out of its control.
How to Detect Client Disconnects on the Server
If your business logic needs to know when a client has disconnected (e.g., to stop a long-running task), you have a few Tomcat-specific options:
1. Check Tomcat's Request.isClosed() (Synchronous Logic)
Tomcat's implementation of HttpServletRequest includes an isClosed() method to check if the client connection is still open. You'll need to cast the request to Tomcat's specific class:
@PostMapping("/sampleUri") public ResponseEntity<SampleResponse> sample(@Valid @RequestBody SampleRequest request, HttpServletRequest servletRequest) { try { // Simulate long-running work with periodic connection checks for (int i = 0; i < 10; i++) { Thread.sleep(100); // Check if client is still connected if (servletRequest instanceof org.apache.catalina.connector.Request) { org.apache.catalina.connector.Request tomcatRequest = (org.apache.catalina.connector.Request) servletRequest; if (tomcatRequest.isClosed()) { System.out.println("Client disconnected mid-processing—aborting task"); // Add cleanup logic here (e.g., cancel database queries, release resources) throw new RuntimeException("Client disconnected"); } } } SampleResponse response = service.sample(request); return ResponseEntity.ok(response); } catch (InterruptedException e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } }
Note: This couples your code to Tomcat. If you ever switch to a different servlet container (like Jetty), you'll need to adjust this logic.
2. Use Async Context + AsyncListener (Asynchronous Logic)
For async endpoints, register an AsyncListener to listen for connection errors (including client disconnects):
@PostMapping("/sampleUri") public void sample(@Valid @RequestBody SampleRequest request, HttpServletRequest servletRequest) { AsyncContext asyncContext = servletRequest.startAsync(); asyncContext.setTimeout(0); // Disable default async timeout for custom handling // Add listener to detect disconnects/errors asyncContext.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent event) {} @Override public void onTimeout(AsyncEvent event) {} @Override public void onError(AsyncEvent event) throws IOException { // Triggered when client disconnects or a network error occurs System.out.println("Client disconnected: " + event.getThrowable().getMessage()); // Clean up long-running tasks here } @Override public void onStartAsync(AsyncEvent event) {} }); // Run your long-running task in a separate thread asyncContext.start(() -> { try { Thread.sleep(1000); SampleResponse response = service.sample(request); // Only send response if client is still connected if (!asyncContext.getResponse().isCommitted()) { asyncContext.getResponse().getWriter().write(new ObjectMapper().writeValueAsString(response)); } } catch (Exception e) { e.printStackTrace(); } finally { asyncContext.complete(); } }); }
3. Check AsyncContext Response Status
In async scenarios, you can also check if the response has been committed (a strong sign the client has disconnected):
if (asyncContext.getResponse().isCommitted()) { // Client is gone—stop processing }
Quick Note on the Client-Side Exception
Your client's expected ResourceAccessException (with nested SocketTimeoutException) is normal behavior. The client correctly throws this when it hits the read timeout, as your provided stack trace shows. This is independent of the server-side behavior we've covered.
内容的提问来源于stack exchange,提问作者DG94

