From f2adc37e2ef6d22a09759dd6943b0b586d66edce Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 13 May 2026 00:00:36 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(restapi):=20NoResource/NoHandler=20?= =?UTF-8?q?=E2=86=92=20404,=20=D0=BD=D0=B5=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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'. --- .../ordinis/app/e2e/SmokeE2ETest.java | 29 ++++++++++++++ .../restapi/error/GlobalExceptionHandler.java | 39 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java b/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java index 13f71fd..4669ce8 100644 --- a/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java +++ b/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java @@ -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. + * + *

До MR-fix: NoResourceFoundException попадал в generic Exception handler + * в GlobalExceptionHandler → 500. Симптомы на prod включали: + *

+ * + *

Покрывает оба пути: путь похожий на 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")); + } } diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/error/GlobalExceptionHandler.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/error/GlobalExceptionHandler.java index 9cb6373..1c3ebc6 100644 --- a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/error/GlobalExceptionHandler.java +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/error/GlobalExceptionHandler.java @@ -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'а. + * + *

Spring MVC throws: + *

+ * + *

До этого handler'а оба exception'а попадали в generic {@link Exception} + * catch ниже → возвращали 500 internal_error. Эффекты: + *

+ * + *

Лог уровня DEBUG (не WARN/ERROR) — это ожидаемый 404, не аномалия. + */ + @ExceptionHandler({NoResourceFoundException.class, NoHandlerFoundException.class}) + public ResponseEntity 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 onGeneric(Exception e) { log.error("Unhandled exception in REST API", e); From 000322d6ee205f7420e600046c976d2ac88a92be Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 13 May 2026 00:07:17 +0300 Subject: [PATCH 2/2] test(restapi): drop /swagger-ui from 404 test (env-dependent) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Failed pipeline 6805: /swagger-ui/index.html в test profile вернул 200 (springdoc enabled по умолчанию через ORDINIS_SWAGGER_UI_ENABLED default true). Это валидно — Swagger render'ит реальную UI page. Замена: проверяем generic non-API typo path который точно не имеет controller'а и не SPA-route'д. Покрывает тот же NoResourceFoundException branch handler'а независимо от springdoc env config. --- .../nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java b/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java index 4669ce8..b5c5ab7 100644 --- a/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java +++ b/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java @@ -141,8 +141,9 @@ class SmokeE2ETest { * debugging без traceId correlation. * * - *

Покрывает оба пути: путь похожий на API ({@code /api/v1/notexist}) и - * generic non-API ({@code /random/path}). + *

Тест проверяет API-typo путь — он независим от env (springdoc может + * быть enabled/disabled, не влияет). Проверка swagger-ui→404 при disabled + * springdoc относится к prod integration, не к unit-уровню handler'а. */ @Test void unknownPath_returns404WithNotFoundCode() throws Exception { @@ -151,7 +152,9 @@ class SmokeE2ETest { .andExpect(jsonPath("$.status").value(404)) .andExpect(jsonPath("$.code").value("not_found")); - mvc.perform(get("/swagger-ui/index.html")) + // Generic non-API typo тоже должен 404'нуться (covers static resource + // fallback branch NoResourceFoundException — этот путь не SPA-routed). + mvc.perform(get("/totally-bogus-path-xyz-12345.unknown")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value(404)) .andExpect(jsonPath("$.code").value("not_found"));