mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
Pr/fix sse timeout global exception handler (#178)
* feat : 调整本地开发环境
* feat : 调整sse 超时 GlobalExceptionHandler 拦截异常
See :
rg.springframework.web.context.request.async.AsyncRequestTimeoutException: null
at org.springframework.web.context.request.async.TimeoutDeferredResultProcessingInterceptor.handleTimeout(TimeoutDeferredResultProcessingInterceptor.java:42) ~[spring-web-6.1.4.jar:6.1.4]
at org.springframework.web.context.request.async.DeferredResultInterceptorChain.triggerAfterTimeout(DeferredResultInterceptorChain.java:81) ~[spring-web-6.1.4.jar:6.1.4]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startDeferredResultProcessing$5(WebAsyncManager.java:430) ~[spring-web-6.1.4.jar:6.1.4]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ~[na:na]
at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.onTimeout(StandardServletAsyncWebRequest.java:149) ~[spring-web-6.1.4.jar:6.1.4]
at org.apache.catalina.core.AsyncListenerWrapper.fireOnTimeout(AsyncListenerWrapper.java:44) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.catalina.core.AsyncContextImpl.timeout(AsyncContextImpl.java:136) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:135) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:243) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:57) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
* Revert "feat : 调整sse 超时 GlobalExceptionHandler 拦截异常"
This reverts commit 6f0cea85b5daac47025767efe1bd90565b5da469.
* Revert "feat : 调整本地开发环境"
This reverts commit 0695a3d7c823491ce5c83aabe3660ba9997f28c5.
* feat : 调整sse 超时 GlobalExceptionHandler 拦截异常
See :
rg.springframework.web.context.request.async.AsyncRequestTimeoutException: null
at org.springframework.web.context.request.async.TimeoutDeferredResultProcessingInterceptor.handleTimeout(TimeoutDeferredResultProcessingInterceptor.java:42) ~[spring-web-6.1.4.jar:6.1.4]
at org.springframework.web.context.request.async.DeferredResultInterceptorChain.triggerAfterTimeout(DeferredResultInterceptorChain.java:81) ~[spring-web-6.1.4.jar:6.1.4]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startDeferredResultProcessing$5(WebAsyncManager.java:430) ~[spring-web-6.1.4.jar:6.1.4]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ~[na:na]
at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.onTimeout(StandardServletAsyncWebRequest.java:149) ~[spring-web-6.1.4.jar:6.1.4]
at org.apache.catalina.core.AsyncListenerWrapper.fireOnTimeout(AsyncListenerWrapper.java:44) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.catalina.core.AsyncContextImpl.timeout(AsyncContextImpl.java:136) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:135) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:243) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:57) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
* fix(app): stabilize async timeout handling
---------
Co-authored-by: zhaieryuan <zhaieryuan@enn.cn>
Co-authored-by: wowo-zZ <zhenggui5228@126.com>
This commit is contained in:
parent
5842a02288
commit
5d7bee2c58
4 changed files with 90 additions and 1 deletions
|
|
@ -21,6 +21,7 @@ import org.springframework.validation.FieldError;
|
|||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
|
||||
/**
|
||||
* Translates application, domain, auth, and infrastructure exceptions into the platform's JSON API
|
||||
|
|
@ -111,6 +112,19 @@ public class GlobalExceptionHandler {
|
|||
apiResponseFactory.error(503, "error.storage.unavailable"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(AsyncRequestTimeoutException.class)
|
||||
public ResponseEntity<?> handleAsyncRequestTimeout(AsyncRequestTimeoutException ex, HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
if (path != null && path.endsWith("/sse")) {
|
||||
logger.debug("SSE timeout [requestId={}, path={}]", MDC.get("requestId"), path);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
logHandledException(HttpStatus.REQUEST_TIMEOUT, "error.request.timeout", request);
|
||||
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(
|
||||
apiResponseFactory.error(408, "error.request.timeout"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleGlobalException(Exception ex, HttpServletRequest request) {
|
||||
logger.error(
|
||||
|
|
@ -122,7 +136,7 @@ public class GlobalExceptionHandler {
|
|||
ex
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
|
||||
apiResponseFactory.error(500, "error.internal"));
|
||||
apiResponseFactory.error(500, "error.internal"));
|
||||
}
|
||||
|
||||
private void logHandledException(HttpStatus status, String messageCode, HttpServletRequest request) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ error.auth.sessionBootstrap.providerUnsupported=Unsupported session bootstrap pr
|
|||
error.auth.sessionBootstrap.notAuthenticated=No authenticated external session found
|
||||
error.badRequest=Invalid request
|
||||
error.forbidden=Forbidden
|
||||
error.request.timeout=Request timed out
|
||||
error.rateLimit.exceeded=Rate limit exceeded
|
||||
error.storage.unavailable=Object storage is temporarily unavailable. Please try again later.
|
||||
error.internal=An unexpected error occurred
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ error.auth.sessionBootstrap.providerUnsupported=不支持的会话引导提供
|
|||
error.auth.sessionBootstrap.notAuthenticated=未检测到已认证的外部会话
|
||||
error.badRequest=请求参数不合法
|
||||
error.forbidden=没有权限执行该操作
|
||||
error.request.timeout=请求超时
|
||||
error.rateLimit.exceeded=请求过于频繁,请稍后再试
|
||||
error.storage.unavailable=对象存储暂时不可用,请稍后再试
|
||||
error.internal=服务器内部错误
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package com.iflytek.skillhub.exception;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.iflytek.skillhub.dto.ApiResponse;
|
||||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.metrics.SkillHubMetrics;
|
||||
import com.iflytek.skillhub.security.SensitiveLogSanitizer;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.support.StaticMessageSource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GlobalExceptionHandlerTest {
|
||||
|
||||
@Mock
|
||||
private SensitiveLogSanitizer sensitiveLogSanitizer;
|
||||
|
||||
@Mock
|
||||
private SkillHubMetrics metrics;
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
private GlobalExceptionHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
StaticMessageSource messageSource = new StaticMessageSource();
|
||||
messageSource.addMessage("error.request.timeout", java.util.Locale.getDefault(), "Request timed out");
|
||||
ApiResponseFactory responseFactory = new ApiResponseFactory(
|
||||
messageSource,
|
||||
Clock.fixed(Instant.parse("2026-03-20T00:00:00Z"), ZoneOffset.UTC)
|
||||
);
|
||||
handler = new GlobalExceptionHandler(responseFactory, sensitiveLogSanitizer, metrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleAsyncRequestTimeout_shouldReturnNoContentForSseRequests() {
|
||||
when(request.getRequestURI()).thenReturn("/api/v1/notifications/sse");
|
||||
|
||||
ResponseEntity<?> response = handler.handleAsyncRequestTimeout(new AsyncRequestTimeoutException(), request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
|
||||
assertThat(response.getBody()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleAsyncRequestTimeout_shouldReturnApiEnvelopeForNonSseRequests() {
|
||||
when(request.getRequestURI()).thenReturn("/api/v1/publish");
|
||||
when(request.getMethod()).thenReturn("POST");
|
||||
when(sensitiveLogSanitizer.sanitizeRequestTarget(request)).thenReturn("/api/v1/publish");
|
||||
|
||||
ResponseEntity<?> response = handler.handleAsyncRequestTimeout(new AsyncRequestTimeoutException(), request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.REQUEST_TIMEOUT);
|
||||
assertThat(response.getBody()).isInstanceOf(ApiResponse.class);
|
||||
ApiResponse<?> body = (ApiResponse<?>) response.getBody();
|
||||
assertThat(body.code()).isEqualTo(408);
|
||||
assertThat(body.msg()).isEqualTo("Request timed out");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue