diff --git a/mateclaw-server/src/main/java/vip/mate/exception/GlobalExceptionHandler.java b/mateclaw-server/src/main/java/vip/mate/exception/GlobalExceptionHandler.java index 74d1889c..46d2c3b2 100644 --- a/mateclaw-server/src/main/java/vip/mate/exception/GlobalExceptionHandler.java +++ b/mateclaw-server/src/main/java/vip/mate/exception/GlobalExceptionHandler.java @@ -11,6 +11,7 @@ import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.resource.NoResourceFoundException; import vip.mate.common.result.R; import vip.mate.i18n.I18nService; @@ -97,6 +98,22 @@ public class GlobalExceptionHandler { return ResponseEntity.badRequest().body(R.fail(400, msg)); } + /** + * A path variable or request parameter could not be coerced into the + * handler's declared type (e.g. a non-numeric segment on an {@code /{id}} + * route bound to {@code Long}). This is a malformed client request, not a + * server fault, so it must surface as 400 — never a 500 with a full stack + * trace from the catch-all handler below. + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handleTypeMismatch(MethodArgumentTypeMismatchException e, + HttpServletRequest request) { + String required = e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "expected type"; + String msg = "Invalid value for parameter '" + e.getName() + "': expected " + required; + log.warn("Argument type mismatch: {} {} - {}", request.getMethod(), request.getRequestURI(), msg); + return ResponseEntity.badRequest().body(R.fail(400, msg)); + } + @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity> handleNoResourceFound(NoResourceFoundException e, HttpServletRequest request) { diff --git a/mateclaw-server/src/test/java/vip/mate/exception/GlobalExceptionHandlerTypeMismatchTest.java b/mateclaw-server/src/test/java/vip/mate/exception/GlobalExceptionHandlerTypeMismatchTest.java new file mode 100644 index 00000000..0f4efe81 --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/exception/GlobalExceptionHandlerTypeMismatchTest.java @@ -0,0 +1,62 @@ +package vip.mate.exception; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import vip.mate.common.result.R; +import vip.mate.i18n.I18nService; + +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Verifies that a non-coercible path variable on a typed route surfaces as a + * clean HTTP 400 (handled by {@link GlobalExceptionHandler}) instead of leaking + * a 500 with a full stack trace from the catch-all handler. + */ +class GlobalExceptionHandlerTypeMismatchTest { + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + I18nService i18n = mock(I18nService.class); + mockMvc = MockMvcBuilders.standaloneSetup(new ProbeController()) + .setControllerAdvice(new GlobalExceptionHandler(i18n)) + .build(); + } + + @Test + @DisplayName("Non-numeric segment on a Long {id} route returns 400, not 500.") + void nonNumericIdReturns400() throws Exception { + mockMvc.perform(get("/probe/status")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.msg").value("Invalid value for parameter 'id': expected Long")); + } + + @Test + @DisplayName("A valid numeric id still resolves the handler normally.") + void numericIdReturns200() throws Exception { + mockMvc.perform(get("/probe/123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").value(123)); + } + + /** Minimal stand-in for any controller with a {@code Long} path variable. */ + @RestController + static class ProbeController { + @GetMapping("/probe/{id}") + R probe(@PathVariable Long id) { + return R.ok(id); + } + } +}