Merge branch 'fix/no-resource-found-returns-404' into 'main'
fix(restapi): NoResource/NoHandler exceptions → 404, не 500 See merge request 2-6/2-6-4/terravault/ordinis!153
This commit is contained in:
@@ -127,4 +127,36 @@ class SmokeE2ETest {
|
|||||||
assertThat(newEntries).isNotEmpty();
|
assertThat(newEntries).isNotEmpty();
|
||||||
assertThat(newEntries.get(0).getBusinessKey()).isEqualTo(businessKey);
|
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-typo путь — он независим от env (springdoc может
|
||||||
|
* быть enabled/disabled, не влияет). Проверка swagger-ui→404 при disabled
|
||||||
|
* springdoc относится к prod integration, не к unit-уровню handler'а.
|
||||||
|
*/
|
||||||
|
@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"));
|
||||||
|
|
||||||
|
// 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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -11,6 +11,8 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
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;
|
import java.time.format.DateTimeParseException;
|
||||||
|
|
||||||
@@ -73,6 +75,43 @@ public class GlobalExceptionHandler {
|
|||||||
return ResponseEntity.badRequest().body(body);
|
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)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<ApiErrorResponse> onGeneric(Exception e) {
|
public ResponseEntity<ApiErrorResponse> onGeneric(Exception e) {
|
||||||
log.error("Unhandled exception in REST API", e);
|
log.error("Unhandled exception in REST API", e);
|
||||||
|
|||||||
Reference in New Issue
Block a user