fix(restapi): NoResource/NoHandler → 404, не 500
GlobalExceptionHandler catch-all Exception → 500 ловил NoResourceFoundException и NoHandlerFoundException. Симптомы на prod: - /swagger-ui/index.html при disabled springdoc возвращал 500 (UI всё равно не рендерился, но spring логировал 'Unhandled exception' на каждый запрос — спамил alerting и затруднял поиск настоящих ошибок). - GET /api/v1/typo от integrator'ов давал 500 вместо 404, осложняло debugging без traceId-correlation. - Любой stale frontend bundle с переименованным endpoint мог триггерить false-positive incident alerts. Fix: explicit @ExceptionHandler для обоих exception типов → 404 not_found. Логирование DEBUG (не WARN/ERROR) — это ожидаемый 404, не аномалия. Test: SmokeE2ETest.unknownPath_returns404WithNotFoundCode покрывает оба пути (/api/v1/* и /swagger-ui/*) → 404 + body.code='not_found'.
This commit is contained in:
@@ -127,4 +127,33 @@ class SmokeE2ETest {
|
||||
assertThat(newEntries).isNotEmpty();
|
||||
assertThat(newEntries.get(0).getBusinessKey()).isEqualTo(businessKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression: unrouted path должен возвращать 404 not_found, не 500
|
||||
* internal_error.
|
||||
*
|
||||
* <p>До MR-fix: NoResourceFoundException попадал в generic Exception handler
|
||||
* в GlobalExceptionHandler → 500. Симптомы на prod включали:
|
||||
* <ul>
|
||||
* <li>{@code /swagger-ui/index.html} при disabled springdoc → 500 вместо 404
|
||||
* (UI не рендерился но logs спамились "Unhandled exception").</li>
|
||||
* <li>Integrator'ы при typo в endpoint path получали 500 — затрудняло
|
||||
* debugging без traceId correlation.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Покрывает оба пути: путь похожий на API ({@code /api/v1/notexist}) и
|
||||
* generic non-API ({@code /random/path}).
|
||||
*/
|
||||
@Test
|
||||
void unknownPath_returns404WithNotFoundCode() throws Exception {
|
||||
mvc.perform(get("/api/v1/this-endpoint-does-not-exist"))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404))
|
||||
.andExpect(jsonPath("$.code").value("not_found"));
|
||||
|
||||
mvc.perform(get("/swagger-ui/index.html"))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.status").value(404))
|
||||
.andExpect(jsonPath("$.code").value("not_found"));
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -11,6 +11,8 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
@@ -73,6 +75,43 @@ public class GlobalExceptionHandler {
|
||||
return ResponseEntity.badRequest().body(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Path не имеет зарегистрированного controller'а ИЛИ static resource'а.
|
||||
*
|
||||
* <p>Spring MVC throws:
|
||||
* <ul>
|
||||
* <li>{@link NoResourceFoundException} — когда ResourceHttpRequestHandler не
|
||||
* нашёл static resource. Поднимается на любой запрос вида
|
||||
* {@code /api/v1/typo} к unrouted path, потому что dispatch fall'sback
|
||||
* на static resource resolver.</li>
|
||||
* <li>{@link NoHandlerFoundException} — когда
|
||||
* {@code spring.mvc.throw-exception-if-no-handler-found=true} и нет
|
||||
* static resource fallback. В нашем setup'е fallback включён, поэтому
|
||||
* приоритетно срабатывает NoResourceFoundException, но handler покрывает
|
||||
* обе ситуации для robustness.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>До этого handler'а оба exception'а попадали в generic {@link Exception}
|
||||
* catch ниже → возвращали 500 internal_error. Эффекты:
|
||||
* <ul>
|
||||
* <li>Disabled springdoc.swagger-ui (prod) — {@code /swagger-ui/index.html}
|
||||
* вместо чистого 404 давал 500 с записью "Unhandled exception" в logs,
|
||||
* спамил alerting.</li>
|
||||
* <li>{@code GET /api/v1/typo} — клиенты получали 500 вместо 404,
|
||||
* затрудняло debugging integrator'ам.</li>
|
||||
* <li>Любой stale frontend bundle с переименованным endpoint в новой версии
|
||||
* мог триггерить false-positive incident alerts.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Лог уровня DEBUG (не WARN/ERROR) — это ожидаемый 404, не аномалия.
|
||||
*/
|
||||
@ExceptionHandler({NoResourceFoundException.class, NoHandlerFoundException.class})
|
||||
public ResponseEntity<ApiErrorResponse> onNotFound(Exception e) {
|
||||
log.debug("Resource not found: {}", e.getMessage());
|
||||
var body = ApiErrorResponse.of(404, "not_found", "Resource not found", currentTraceId());
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiErrorResponse> onGeneric(Exception e) {
|
||||
log.error("Unhandled exception in REST API", e);
|
||||
|
||||
Reference in New Issue
Block a user