You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot+Tomcat服务端如何感知客户端请求中断?

Spring Boot 1.5.12 + Tomcat 8.5.29: Client Connection Drop Behavior & Server-Side Detection

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 DEBUG level (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:

  1. Logs the error at DEBUG level (check the org.apache.coyote logger if you want to verify this)
  2. Cleans up socket and connection resources
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 09:06:58