mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +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.http.ResponseEntity;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
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"));
|
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)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<R<Void>> handleException(Exception e,
|
public ResponseEntity<R<Void>> handleException(Exception e,
|
||||||
HttpServletRequest request,
|
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