fix(oapi): 请求体反序列化失败给友好提示,避免误报 code=6 内部服务错误
build / build-push (push) Successful in 2m31s

GlobalExceptionHandler 新增 HttpMessageNotReadableException 处理:Jackson 在进 Controller 前抛的
解析失败(如 birthday 非 yyyy-MM-dd)此前无专属 handler,穿透到 api-std 兜底渲染成 code=6「内部服务错误」。
改为从异常 path 取失败字段名,birthday 点名「字段类型需为yyyy-MM-dd」、其余走通用文案,统一复用
paramError 返回 code=4。空串/null 由 Jackson 归一为 null 不抛,满足「空不管」。附单测 2 例。
This commit is contained in:
chenxing
2026-09-11 14:48:38 +08:00
parent 877e800f82
commit 0b67382076
2 changed files with 73 additions and 0 deletions
@@ -1,7 +1,9 @@
package com.yunhai.userbase.common;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -13,6 +15,7 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -66,6 +69,27 @@ public class GlobalExceptionHandler {
return paramError("参数类型不匹配: " + ex.getName());
}
/**
* 请求体反序列化失败(Jackson 在进 Controller 前抛出,@Valid 兜不到)。典型如 birthday 等非空字段格式非法:
* 从异常最内层 {@link JsonMappingException} 的 path 取失败字段名,birthday 给点名提示,其余走通用文案。
* 空串/null 由 Jackson JSR-310 归一为 null、不抛,故不会误入此分支(满足「空不管」)。
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public Map<String, Object> handleNotReadable(HttpMessageNotReadableException ex) {
String field = null;
Throwable cause = ex.getMostSpecificCause();
if (cause instanceof JsonMappingException) {
List<JsonMappingException.Reference> path = ((JsonMappingException) cause).getPath();
if (path != null && !path.isEmpty()) {
field = path.get(path.size() - 1).getFieldName();
}
}
if ("birthday".equals(field)) {
return paramError("birthday 字段类型需为yyyy-MM-dd");
}
return paramError("请求参数格式错误");
}
private String fieldErrorsMessage(java.util.List<FieldError> fieldErrors) {
if (fieldErrors == null || fieldErrors.isEmpty()) {
return "参数校验失败";
@@ -0,0 +1,49 @@
package com.yunhai.userbase.common;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link GlobalExceptionHandler#handleNotReadable} 单测:请求体反序列化失败的友好提示。
* 纯单元(无 DB/Spring 上下文),直接构造异常断言返回信封。
*/
class GlobalExceptionHandlerTest {
private final GlobalExceptionHandler handler = new GlobalExceptionHandler();
@Test
void birthdayParseErrorReturnsFriendlyParamError() throws Exception {
JsonParser p = new ObjectMapper().createParser("{}");
JsonMappingException jme = JsonMappingException.from(p, "cannot deserialize LocalDate");
jme.prependPath(new JsonMappingException.Reference(null, "birthday"));
HttpMessageNotReadableException ex =
new HttpMessageNotReadableException("JSON parse error", jme, (HttpInputMessage) null);
Map<String, Object> body = handler.handleNotReadable(ex);
assertEquals(4, body.get("code"));
String msg = (String) body.get("msg");
assertTrue(msg.contains("birthday"), "msg 应点名 birthday 字段: " + msg);
assertTrue(msg.contains("yyyy-MM-dd"), "msg 应含期望格式 yyyy-MM-dd: " + msg);
}
@Test
void otherParseErrorReturnsGenericMessage() {
HttpMessageNotReadableException ex =
new HttpMessageNotReadableException("malformed JSON", (HttpInputMessage) null);
Map<String, Object> body = handler.handleNotReadable(ex);
assertEquals(4, body.get("code"));
assertEquals("请求参数格式错误", body.get("msg"));
}
}