mateclaw/mateclaw-server/src/test/java/vip/mate/exception/GlobalExceptionHandlerMethodNotSupportedTest.java
倪程伟 ffef9bab00 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).
2026-06-19 06:20:59 +08:00

68 lines
2.7 KiB
Java

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);
}
}
}