fix(api): return 400 on path-variable type mismatch instead of 500

This commit is contained in:
matevip 2026-05-24 22:57:51 +08:00
parent c49b4a21bb
commit 8030a41125
2 changed files with 79 additions and 0 deletions

View File

@ -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<R<Void>> 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<R<Void>> handleNoResourceFound(NoResourceFoundException e,
HttpServletRequest request) {

View File

@ -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<Long> probe(@PathVariable Long id) {
return R.ok(id);
}
}
}