mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(exception): return 405 for HttpRequestMethodNotSupportedException
Spring's default lets HttpRequestMethodNotSupportedException escape to the catch-all @ExceptionHandler(Exception.class), surfacing as a 500 with a full stack trace. Return a clean 405 so the client gets a structured R body and the log stays at WARN. This is also the second line of defence against malformed path segments that confuse reverse proxies — e.g. a conversationId ending in ":" can make a proxy strip the trailing path, landing a GET /api/v1/conversations/<id> on the @DeleteMapping variant and triggering exactly this exception (issue #369).
This commit is contained in:
parent
5893d4b33d
commit
ffef9bab00
@ -9,6 +9,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
@ -121,6 +122,21 @@ public class GlobalExceptionHandler {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(R.fail(404, "Resource not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring's default lets this escape to the catch-all below, surfacing as a
|
||||
* 500 with a stack trace. Return a clean 405 so the client gets a
|
||||
* structured body and the log stays at WARN. Doubles as a defence when a
|
||||
* malformed path segment makes a reverse proxy strip the trailing path
|
||||
* (e.g. a conversationId ending in ":" lands a GET on a @DeleteMapping).
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public ResponseEntity<R<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException e,
|
||||
HttpServletRequest request) {
|
||||
log.warn("Method not supported: {} {} (supported: {})",
|
||||
request.getMethod(), request.getRequestURI(), e.getSupportedHttpMethods());
|
||||
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(R.fail(405, "Method not allowed"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<R<Void>> handleException(Exception e,
|
||||
HttpServletRequest request,
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
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.DeleteMapping;
|
||||
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.delete;
|
||||
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 path matched but the HTTP method did not surfaces as a clean
|
||||
* HTTP 405 (handled by {@link GlobalExceptionHandler}) instead of leaking a 500
|
||||
* with a full stack trace from the catch-all handler.
|
||||
*
|
||||
* <p>This is the second line of defence for malformed path segments that make
|
||||
* a reverse proxy strip the trailing path — e.g. a conversationId ending in
|
||||
* ":" landing a GET on a @DeleteMapping route (upstream issue #369).
|
||||
*/
|
||||
class GlobalExceptionHandlerMethodNotSupportedTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
I18nService i18n = mock(I18nService.class);
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(new ProbeController())
|
||||
.setControllerAdvice(new GlobalExceptionHandler(i18n))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET on a @DeleteMapping-only route returns 405, not 500.")
|
||||
void getOnDeleteOnlyRouteReturns405() throws Exception {
|
||||
mockMvc.perform(get("/probe/abc"))
|
||||
.andExpect(status().isMethodNotAllowed())
|
||||
.andExpect(jsonPath("$.code").value(405))
|
||||
.andExpect(jsonPath("$.msg").value("Method not allowed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DELETE on the same route still resolves the handler normally.")
|
||||
void deleteOnDeleteRouteReturns200() throws Exception {
|
||||
mockMvc.perform(delete("/probe/abc"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data").value("abc"));
|
||||
}
|
||||
|
||||
/** Minimal stand-in for a controller whose path is mapped to DELETE only. */
|
||||
@RestController
|
||||
static class ProbeController {
|
||||
@DeleteMapping("/probe/{id}")
|
||||
R<String> probe(@PathVariable String id) {
|
||||
return R.ok(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user