From 5d9f5b8ab7996ad7b5057ee30cd5d760aa204214 Mon Sep 17 00:00:00 2001 From: Azure <983547216@qq.com> Date: Wed, 24 Jun 2026 12:00:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20HTTP=E7=8A=B6=E6=80=81=E7=A0=81=20+=20?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GlobalExceptionHandler.java | 55 +++++++++++++------ .../webgamebackend/sse/SseController.java | 2 - 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java b/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java index 7f93ffe..34d7fd0 100644 --- a/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java @@ -4,11 +4,14 @@ import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; import cn.dev33.satoken.exception.SaTokenContextException; +import com.alibaba.fastjson2.JSON; import com.webgame.webgamebackend.common.dto.ApiResponse; import com.webgame.webgamebackend.common.exception.BusinessException; import com.webgame.webgamebackend.common.exception.ErrorCode; +import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -21,9 +24,14 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.servlet.resource.NoResourceFoundException; +import java.io.IOException; + /** - * 全局异常处理器,返回 ResponseEntity<ApiResponse> 实现双层错误表达: + * 全局异常处理器,双层错误表达: * HTTP 状态码表达错误大类 + ApiResponse.code 表达具体业务错误码 + *

+ * 业务异常(仅来自 REST JSON 请求)返回 ResponseEntity,经过 Spring 内容协商。 + * 框架级异常(可能来自 SSE/浏览器等任意请求类型)直接写 HttpServletResponse 绕过内容协商。 */ @Slf4j @RestControllerAdvice @@ -47,7 +55,7 @@ public class GlobalExceptionHandler { // SSE 响应已被客户端关闭,不写响应体 } - // ==================== 业务异常 ==================== + // ==================== 业务异常(REST JSON 请求) ==================== /** * 业务异常 — HTTP 状态码从 ErrorCode 枚举自动派生 @@ -115,31 +123,46 @@ public class GlobalExceptionHandler { .body(ApiResponse.fail(403, "权限不足")); } - // ==================== 框架级异常 ==================== + // ==================== 框架级异常(直接写响应,绕过内容协商) ==================== + // 这类异常可能来自任意请求类型(SSE Accept: text/event-stream、 + // 浏览器 Accept: text/html 等),若走 ResponseEntity 会因 Accept 头 + // 与 JSON 响应体不匹配而抛出 HttpMediaTypeNotAcceptableException。 @ExceptionHandler(NoResourceFoundException.class) - public ResponseEntity> handleNoResourceFound(NoResourceFoundException ex) { + public void handleNoResourceFound(NoResourceFoundException ex, HttpServletResponse response) throws IOException { log.warn("资源未找到: {}", ex.getMessage()); - return ResponseEntity - .status(HttpStatus.NOT_FOUND) - .body(ApiResponse.fail(404, "资源未找到")); + writeJsonError(response, HttpStatus.NOT_FOUND, 404, "资源未找到"); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public ResponseEntity> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) { + public void handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, + HttpServletResponse response) throws IOException { log.warn("请求方法不支持: {}", ex.getMessage()); - return ResponseEntity - .status(HttpStatus.METHOD_NOT_ALLOWED) - .body(ApiResponse.fail(405, "请求方法不支持")); + writeJsonError(response, HttpStatus.METHOD_NOT_ALLOWED, 405, "请求方法不支持"); } - // ==================== 兜底 → HTTP 500 ==================== + // ==================== 兜底 → HTTP 500(直接写响应,绕过内容协商) ==================== @ExceptionHandler(Exception.class) - public ResponseEntity> handleException(Exception ex) { + public void handleException(Exception ex, HttpServletResponse response) throws IOException { log.error("服务器内部错误", ex); - return ResponseEntity - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.fail(ErrorCode.INTERNAL_ERROR.getCode(), ErrorCode.INTERNAL_ERROR.getDefaultMessage())); + writeJsonError(response, HttpStatus.INTERNAL_SERVER_ERROR, + ErrorCode.INTERNAL_ERROR.getCode(), ErrorCode.INTERNAL_ERROR.getDefaultMessage()); + } + + // ==================== 工具方法 ==================== + + /** + * 直接向 HttpServletResponse 写入 JSON 格式的错误响应 + *

+ * 不依赖 Spring 的 MessageConverter 和内容协商机制, + * 确保 SSE/浏览器等非 JSON Accept 头的请求也能收到可读的错误信息。 + */ + private static void writeJsonError(HttpServletResponse response, HttpStatus status, + int code, String message) throws IOException { + response.setStatus(status.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(JSON.toJSONString(ApiResponse.fail(code, message))); } } diff --git a/src/main/java/com/webgame/webgamebackend/sse/SseController.java b/src/main/java/com/webgame/webgamebackend/sse/SseController.java index 22a4f5d..9a43f27 100644 --- a/src/main/java/com/webgame/webgamebackend/sse/SseController.java +++ b/src/main/java/com/webgame/webgamebackend/sse/SseController.java @@ -71,8 +71,6 @@ public class SseController { lastEventId, userId, connectionId); } - log.info("[SSE] 连接建立成功, userId={}, connectionId={}, onlineConnections={}", - userId, connectionId, connectionManager.getOnlineCount()); return emitter; }