diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/handler/exception/RestExceptionHandler.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/handler/exception/RestExceptionHandler.java index fa94d6dcc..fdca73836 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/handler/exception/RestExceptionHandler.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/handler/exception/RestExceptionHandler.java @@ -12,6 +12,7 @@ import cn.daxpay.open.platform.core.rest.Res; import cn.daxpay.open.platform.core.rest.result.Result; import cn.daxpay.open.platform.capability.auth.entity.TwoFactorChallengeResult; import cn.daxpay.open.platform.capability.auth.exception.TwoFactorRequiredException; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.ValidationException; @@ -20,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.validation.BindException; @@ -32,6 +34,12 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; /// # 项目异常处理 /// +/// ## SSE 异步端点说明 +/// SSE 事件流端点(如 `/sse/connect`)基于 `SseEmitter`, 在客户端断连/超时/发送失败时, +/// Servlet 容器会做 ASYNC 重派发并抛出非 `SaTokenException` 的 `Throwable`(常为 `ClientAbortException`), +/// 最终落入本兜底处理. 此时响应 `Content-Type` 已锁为 `text/event-stream`, 无 JSON 转换器, +/// 写 body 会抛 `HttpMessageNotWritableException`. 故所有 handler 在 SSE 场景统一返回无 body 的 200 响应, +/// 并降级为 info 日志(多为客户端主动断连, 属常态). @Slf4j @RestControllerAdvice @EnableConfigurationProperties(PlatformCommonProperties.class) @@ -40,6 +48,17 @@ public class RestExceptionHandler { private final PlatformCommonProperties properties; + /// 判断当前响应是否为 SSE 事件流(Content-Type 已锁定为 text/event-stream) + private boolean isSseStream(HttpServletResponse response) { + String contentType = response.getContentType(); + return contentType != null && contentType.contains(MediaType.TEXT_EVENT_STREAM_VALUE); + } + + /// SSE 场景统一日志: 降级为 info, 仅记录异常类型与消息, 不打印完整堆栈 + private void logSse(Throwable ex) { + log.info("SSE 端点异常(通常为客户端断连), 类型={}, 消息={}", ex.getClass().getSimpleName(), ex.getMessage()); + } + /// 获取异常消息,支持国际化 private String getMessage(BizException ex) { String messageKey = ex.getMessageKey(); @@ -55,7 +74,11 @@ public class RestExceptionHandler { /// 普通业务异常, 不需要进行堆栈跟踪 @ExceptionHandler(BizInfoException.class) - public Result handleBizInfoException(BizInfoException ex) { + public Object handleBizInfoException(BizInfoException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage()); String message = getMessage(ex); return Res.response(ex.getCode(), message, MDC.get(CommonCode.TRACE_ID)); @@ -63,7 +86,11 @@ public class RestExceptionHandler { /// 双因素认证挑战: 密码通过但需二次验证, 返回预认证令牌, 不计入登录失败 @ExceptionHandler(TwoFactorRequiredException.class) - public Result handleTwoFactorRequiredException(TwoFactorRequiredException ex) { + public Object handleTwoFactorRequiredException(TwoFactorRequiredException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage()); String message = getMessage(ex); return Res.response(TwoFactorRequiredException.CODE, message, new TwoFactorChallengeResult(ex.getPreAuthToken())); @@ -71,7 +98,11 @@ public class RestExceptionHandler { /// 警告业务异常, 如果量多需要关注 @ExceptionHandler(BizWarnException.class) - public Result handleBizWarnException(BizWarnException ex) { + public Object handleBizWarnException(BizWarnException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.warn(ex.getMessage(), ex); String message = getMessage(ex); return Res.response(ex.getCode(), message, MDC.get(CommonCode.TRACE_ID)); @@ -79,7 +110,11 @@ public class RestExceptionHandler { /// 致命警告业务异常, 需要进行立即进入排查 @ExceptionHandler(BizErrorException.class) - public Result handleBizErrorException(BizErrorException ex) { + public Object handleBizErrorException(BizErrorException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.error(ex.getMessage(), ex); String message = getMessage(ex); return Res.response(ex.getCode(), message, MDC.get(CommonCode.TRACE_ID)); @@ -87,7 +122,11 @@ public class RestExceptionHandler { /// 业务异常 @ExceptionHandler(BizException.class) - public Result handleBusinessException(BizException ex) { + public Object handleBusinessException(BizException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage(), ex); String message = getMessage(ex); return Res.response(ex.getCode(), message, MDC.get(CommonCode.TRACE_ID)); @@ -95,7 +134,11 @@ public class RestExceptionHandler { /// 请求参数校验未通过 @ExceptionHandler({ ConstraintViolationException.class }) - public Result handleBusinessException(ConstraintViolationException ex) { + public Object handleBusinessException(ConstraintViolationException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage(), ex); StringBuilder message = new StringBuilder(); for (ConstraintViolation violation : ex.getConstraintViolations()) { @@ -106,7 +149,11 @@ public class RestExceptionHandler { /// 请求参数校验未通过 @ExceptionHandler({ MethodArgumentNotValidException.class }) - public Result handleBusinessException(MethodArgumentNotValidException ex) { + public Object handleBusinessException(MethodArgumentNotValidException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage(), ex); StringBuilder message = new StringBuilder(); for (var violation : ex.getAllErrors()) { @@ -117,22 +164,34 @@ public class RestExceptionHandler { /// 不支持 HTTP 请求方法异常 @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public Result HttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + public Object HttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } String message = I18nUtil.get("error.common.unsupportedOperate"); - log.info(message, e); + log.info(message, ex); return Res.error(message); } /// 请求参数校验未通过 @ExceptionHandler({ ValidationException.class }) - public Result handleBusinessException(ValidationException ex) { + public Object handleBusinessException(ValidationException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage(), ex); return Res.response(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, ex.getMessage(), MDC.get(CommonCode.TRACE_ID)); } /// 页面或资源不存在 @ExceptionHandler({ NoResourceFoundException.class }) - public ResponseEntity> handleBusinessException(NoResourceFoundException ex) { + public ResponseEntity handleBusinessException(NoResourceFoundException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage(), ex); String message = I18nUtil.get("error.common.notFound"); Result result = Res.response(CommonErrorCode.SOURCES_NOT_EXIST, message, MDC.get(CommonCode.TRACE_ID)); @@ -141,7 +200,11 @@ public class RestExceptionHandler { /// 处理 MissingServletRequestParameterException ( 缺少 必填的request param ) @ExceptionHandler(MissingServletRequestParameterException.class) - public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) { + public Object handleMissingServletRequestParameterException(MissingServletRequestParameterException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage(), ex); String message = I18nUtil.get("error.common.parseParams"); return Res.response(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, message, MDC.get(CommonCode.TRACE_ID)); @@ -149,20 +212,33 @@ public class RestExceptionHandler { /// 处理 HttpMessageConversionException @ExceptionHandler(HttpMessageConversionException.class) - public Result handleHttpMessageConversionException(HttpMessageConversionException ex) { + public Object handleHttpMessageConversionException(HttpMessageConversionException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info(ex.getMessage(), ex); return Res.response(CommonErrorCode.PARSE_PARAMETERS_ERROR, ex.getMessage(), MDC.get(CommonCode.TRACE_ID)); } + /// 处理 HttpMessageConversionException @ExceptionHandler(BindException.class) - public Result handleBindException(BindException ex) { + public Object handleBindException(BindException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.info("参数绑定失败 ", ex); return Res.response(CommonErrorCode.PARSE_PARAMETERS_ERROR, ex.getMessage(), MDC.get(CommonCode.TRACE_ID)); } /// 空指针异常 @ExceptionHandler(NullPointerException.class) - public Result handleNullPointerException(NullPointerException ex) { + public Object handleNullPointerException(NullPointerException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.warn("空指针 ", ex); String message = I18nUtil.get("error.common.system"); return Res.response(CommonErrorCode.SYSTEM_ERROR, message, MDC.get(CommonCode.TRACE_ID)); @@ -170,7 +246,11 @@ public class RestExceptionHandler { /// 处理运行时异常 @ExceptionHandler(RuntimeException.class) - public Result handleRuntimeException(RuntimeException ex) { + public Object handleRuntimeException(RuntimeException ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.error(ex.getMessage(), ex); String message = I18nUtil.get("error.common.system"); if (properties.getException().isShowFullMessage()) { @@ -182,7 +262,11 @@ public class RestExceptionHandler { /// 处理 OutOfMemoryError @ExceptionHandler(OutOfMemoryError.class) - public Result handleOomException(OutOfMemoryError ex) { + public Object handleOomException(OutOfMemoryError ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } log.error("内存不足错误 {}", ex.getMessage(), ex); String message = I18nUtil.get("error.common.system"); return Res.response(CommonErrorCode.SYSTEM_ERROR, message, MDC.get(CommonCode.TRACE_ID)); @@ -190,7 +274,11 @@ public class RestExceptionHandler { /// 处理 Throwable 异常 @ExceptionHandler(Throwable.class) - public Result handleThrowable(Throwable ex) { + public Object handleThrowable(Throwable ex, HttpServletResponse response) { + if (isSseStream(response)) { + logSse(ex); + return ResponseEntity.ok().build(); + } String message = I18nUtil.get("error.common.system"); if (properties.getException().isShowFullMessage()){ return Res.response(CommonErrorCode.SYSTEM_ERROR, ex.getMessage(), MDC.get(CommonCode.TRACE_ID));