From 7db504cbc6d08e50666c86a3dbd6c7da5147b630 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期六, 17 十月 2026 13:28:09 +0800
Subject: [PATCH] fix(web): 修复客户端断开连接时的异常处理
---
yudao-server/src/main/resources/static/trace.html | 2
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java | 13 ++++++
yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerClientAbortTest.java | 40 ++++++++++++++++++++
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java | 10 +++++
4 files changed, 64 insertions(+), 1 deletions(-)
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
index 0eb7f5e..905697d 100644
--- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
@@ -17,6 +17,7 @@
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.google.common.util.concurrent.UncheckedExecutionException;
import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException;
@@ -34,6 +35,7 @@
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
@@ -67,6 +69,14 @@
private final ApiErrorLogCommonApi apiErrorLogApi;
+ /** 瀹㈡埛绔凡鏂紑鏃跺搷搴斾笉鍙啀鍐欙紱娉ㄥ叆 HttpServletResponse 浣� Spring MVC 鐩存帴缁撴潫澶勭悊銆� */
+ @ExceptionHandler(AsyncRequestNotUsableException.class)
+ public void asyncRequestNotUsableExceptionHandler(HttpServletRequest request,
+ HttpServletResponse response,
+ AsyncRequestNotUsableException ex) {
+ log.debug("[asyncRequestNotUsableExceptionHandler][瀹㈡埛绔凡鏂紑 uri={}]", request.getRequestURI());
+ }
+
/**
* 澶勭悊鎵�鏈夊紓甯革紝涓昏鏄彁渚涚粰 Filter 浣跨敤
* 鍥犱负 Filter 涓嶈蛋 SpringMVC 鐨勬祦绋嬶紝浣嗘槸鎴戜滑鍙堥渶瑕佸厹搴曞鐞嗗紓甯革紝鎵�浠ヨ繖閲屾彁渚涗竴涓叏閲忕殑寮傚父澶勭悊杩囩▼锛屼繚鎸侀�昏緫缁熶竴銆�
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerClientAbortTest.java b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerClientAbortTest.java
new file mode 100644
index 0000000..e38d829
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerClientAbortTest.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.framework.web.core.handler;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.io.IOException;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class GlobalExceptionHandlerClientAbortTest {
+
+ @Test
+ void doesNotSerializeJsonAfterZipResponseBecomesUnusable() throws Exception {
+ MockMvcBuilders.standaloneSetup(new ZipController())
+ .setControllerAdvice(new GlobalExceptionHandler("test", null))
+ .build()
+ .perform(get("/zip"))
+ .andExpect(status().isOk())
+ .andExpect(content().bytes(new byte[0]));
+ }
+
+ @RestController
+ static class ZipController {
+
+ @GetMapping("/zip")
+ public void download(HttpServletResponse response) throws IOException {
+ response.setContentType("application/zip");
+ response.flushBuffer();
+ throw new AsyncRequestNotUsableException("client disconnected");
+ }
+
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
index e6adab6..711cdda 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
@@ -15,6 +15,7 @@
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import java.io.BufferedInputStream;
import java.io.File;
@@ -193,6 +194,7 @@
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
}
response.setContentLengthLong(length);
+ long sent = 0;
try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
in.skipNBytes(start);
OutputStream out = response.getOutputStream();
@@ -205,8 +207,19 @@
}
out.write(buffer, 0, read);
remaining -= read;
+ sent += read;
}
out.flush();
+ } catch (AsyncRequestNotUsableException e) {
+ // 娴忚鍣ㄥ彇娑堛�佺綉缁滄柇寮�鎴栦唬鐞嗗叧闂繛鎺ユ椂锛屽搷搴斿凡缁忎笉鍙啓锛屼笉鑳藉啀杩斿洖 JSON 閿欒銆�
+ // ZIP 淇濈暀鍒版竻鐞嗘椂闂达紝鍓嶇鍙噸璇曞綋鍓嶅垎娈碉紱璁板綍鑼冨洿涓庤繘搴︿究浜庢帓鏌ラ绻佹柇寮�銆�
+ log.warn("[download][瀹㈡埛绔腑鏂� taskId={}, range={}, sent={}/{} bytes: {}]",
+ id, range, sent, length, e.getMessage());
+ } catch (IOException e) {
+ // ZIP 鍝嶅簲澶村凡缁忓啓鍏ワ紝涓嶈兘浜ょ粰鍏ㄥ眬寮傚父澶勭悊鍣ㄥ簭鍒楀寲 CommonResult銆�
+ // Content-Length 涓嶅尮閰嶄細璁╁鎴风鍒ゅ畾鏈澶辫触骞堕噸璇曘��
+ log.error("[download][浼犺緭澶辫触 taskId={}, range={}, sent={}/{} bytes]",
+ id, range, sent, length, e);
}
} finally {
activeDownloads.computeIfPresent(id, (key, count) -> count > 1 ? count - 1 : null);
diff --git a/yudao-server/src/main/resources/static/trace.html b/yudao-server/src/main/resources/static/trace.html
index d89fb73..fd796ac 100644
--- a/yudao-server/src/main/resources/static/trace.html
+++ b/yudao-server/src/main/resources/static/trace.html
@@ -327,7 +327,7 @@
</div>
<div class="contact-item">
<span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5 12 3l9 6.5V20a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9.5 22v-6h5v6"/></svg></span>
- <span class="txt"><span class="lbl">鍏徃缃戝潃</span><div class="val"><a href="https://www.talimu.net" target="_blank" rel="noopener">www.talimu.net</a></div></span>
+ <span class="txt"><span class="lbl">鍏徃缃戝潃</span><div class="val"><a href="https://www.tahezy.com" target="_blank" rel="noopener">www.tahezy.com</a></div></span>
</div>
<div class="contact-item">
<span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.4 8.4 0 0 1 3.8-.9h.5a8.5 8.5 0 0 1 8 8z"/></svg></span>
--
Gitblit v1.9.3