fix(I): HTTP 语义类异常返回真实 4xx 状态码, 不再兜底成 500
问题:GlobalExceptionHandler 只对业务异常/参数校验做了分类,其余异常一律被兜底的
Exception handler 处理成 500「系统繁忙」。例如以 application/octet-stream 发送
JSON 请求体本应返回 415 Unsupported Media Type,却得到 500,误导调用方与排障。
改动(iot-common,四个服务共用):
- ResultCode 新增:UNSUPPORTED_MEDIA_TYPE(415) / NOT_ACCEPTABLE(406) / PAYLOAD_TOO_LARGE(413)
- GlobalExceptionHandler 新增 HTTP 语义类异常处理:
HttpMediaTypeNotSupportedException -> 415
HttpMediaTypeNotAcceptableException -> 406
HttpRequestMethodNotSupportedException-> 405
MissingServletRequestParameterException / MethodArgumentTypeMismatchException
/ HttpMessageNotReadableException -> 400
MaxUploadSizeExceededException -> 413
- 响应约定:真实 HTTP 状态码 + body.code 与状态码保持一致(沿用 JwtAuthenticationEntryPoint 的
401 约定);业务/参数异常维持 HTTP 200 + body.code 的历史约定,前端无需改动
- 这类异常属客户端错误,日志级别由 error 降为 warn,避免污染错误日志
- 新增单测 iot-common/src/test/java/com/iot/common/exception/GlobalExceptionHandlerHttpStatusTest.java
(2 个用例全部通过)
生产验证:
错误 Content-Type -> HTTP 415 body.code=415
错误请求方法 -> HTTP 405
参数类型不匹配 -> HTTP 400
请求体 JSON 非法 -> HTTP 400
正常列表/物模型 -> HTTP 200
前端 axios 拦截器对非 401/403/500 的 4xx 走 data?.message 分支,提示更准确,无回归。
This commit is contained in:
parent
0ff9103089
commit
4d8d38909d
@ -3,14 +3,29 @@ package com.iot.common.exception;
|
||||
import com.iot.common.result.Result;
|
||||
import com.iot.common.result.ResultCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* 响应约定:
|
||||
* 业务/参数类异常 -> HTTP 200 + body.code 表达语义(沿用历史约定,前端按 body.code 判定)
|
||||
* HTTP 语义类异常 -> 真实 HTTP 4xx 状态码 + body.code 与状态码一致
|
||||
* (如 415 Unsupported Media Type,与 JwtAuthenticationEntryPoint 的 401 约定一致)
|
||||
* 未预期异常 -> HTTP 200 + body.code 500
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
@ -36,6 +51,85 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.PARAM_ERROR.getCode(), message);
|
||||
}
|
||||
|
||||
// ===== HTTP 语义类异常:返回真实 4xx 状态码 =====
|
||||
|
||||
/**
|
||||
* 请求的 Content-Type 不被支持(如以 application/octet-stream 发送 JSON 请求体)
|
||||
*/
|
||||
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
|
||||
public ResponseEntity<Result<Void>> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException e) {
|
||||
String message = "不支持的请求媒体类型: " + e.getContentType();
|
||||
log.warn("请求媒体类型不支持: {}", message);
|
||||
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
.body(Result.failed(ResultCode.UNSUPPORTED_MEDIA_TYPE.getCode(), message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求的 Accept 头与可返回的类型不匹配
|
||||
*/
|
||||
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
|
||||
public ResponseEntity<Result<Void>> handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException e) {
|
||||
String message = "无法满足请求的 Accept 类型";
|
||||
log.warn("请求 Accept 类型不可接受: {}", message);
|
||||
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE)
|
||||
.body(Result.failed(ResultCode.NOT_ACCEPTABLE.getCode(), message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求方法不被支持
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public ResponseEntity<Result<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
|
||||
String message = "不支持的请求方法: " + e.getMethod();
|
||||
log.warn("请求方法不支持: {}", message);
|
||||
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.body(Result.failed(ResultCode.METHOD_NOT_ALLOWED.getCode(), message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺少必填请求参数
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public ResponseEntity<Result<Void>> handleMissingParameter(MissingServletRequestParameterException e) {
|
||||
String message = "缺少必填参数: " + e.getParameterName();
|
||||
log.warn("缺少必填请求参数: {}", message);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Result.failed(ResultCode.PARAM_ERROR.getCode(), message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数类型不匹配
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public ResponseEntity<Result<Void>> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
|
||||
String message = "参数类型不匹配: " + e.getName();
|
||||
log.warn("请求参数类型不匹配: {}", message);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Result.failed(ResultCode.PARAM_ERROR.getCode(), message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求体无法解析(JSON 格式错误等)
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<Result<Void>> handleMessageNotReadable(HttpMessageNotReadableException e) {
|
||||
String message = "请求体格式错误,无法解析";
|
||||
log.warn("请求体无法解析: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Result.failed(ResultCode.PARAM_ERROR.getCode(), message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传内容超过限制
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<Result<Void>> handleMaxUploadSize(MaxUploadSizeExceededException e) {
|
||||
String message = "上传内容超过大小限制";
|
||||
log.warn("上传内容超限: {}", message);
|
||||
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
|
||||
.body(Result.failed(ResultCode.PAYLOAD_TOO_LARGE.getCode(), message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数绑定异常
|
||||
*/
|
||||
|
||||
@ -43,6 +43,21 @@ public enum ResultCode {
|
||||
*/
|
||||
METHOD_NOT_ALLOWED(405, "方法不允许"),
|
||||
|
||||
/**
|
||||
* 请求的媒体类型不被支持(Content-Type 不匹配)
|
||||
*/
|
||||
UNSUPPORTED_MEDIA_TYPE(415, "不支持的媒体类型"),
|
||||
|
||||
/**
|
||||
* 无法接受(Accept 头与可返回类型不匹配)
|
||||
*/
|
||||
NOT_ACCEPTABLE(406, "无法满足请求的 Accept 类型"),
|
||||
|
||||
/**
|
||||
* 请求体过大
|
||||
*/
|
||||
PAYLOAD_TOO_LARGE(413, "上传内容过大"),
|
||||
|
||||
/**
|
||||
* 请求超时
|
||||
*/
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package com.iot.common.exception;
|
||||
|
||||
import com.iot.common.result.Result;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* HTTP 语义类异常的全局处理器单测:
|
||||
* 确认真实 HTTP 状态码与 body.code 保持一致(415 / 405 等),
|
||||
* 不再被兜底的 Exception handler 统一当成 500「系统繁忙」。
|
||||
*/
|
||||
class GlobalExceptionHandlerHttpStatusTest {
|
||||
|
||||
private final GlobalExceptionHandler handler = new GlobalExceptionHandler();
|
||||
|
||||
@Test
|
||||
void unsupportedMediaType_shouldReturn415() {
|
||||
HttpMediaTypeNotSupportedException ex = new HttpMediaTypeNotSupportedException(
|
||||
"application/octet-stream", List.of(MediaType.APPLICATION_JSON));
|
||||
ResponseEntity<Result<Void>> resp = handler.handleHttpMediaTypeNotSupported(ex);
|
||||
assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, resp.getStatusCode());
|
||||
assertEquals(415, resp.getBody().getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodNotSupported_shouldReturn405() {
|
||||
HttpRequestMethodNotSupportedException ex = new HttpRequestMethodNotSupportedException("TRACE");
|
||||
ResponseEntity<Result<Void>> resp = handler.handleMethodNotSupported(ex);
|
||||
assertEquals(HttpStatus.METHOD_NOT_ALLOWED, resp.getStatusCode());
|
||||
assertEquals(405, resp.getBody().getCode());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user