diff --git a/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/GlobalExceptionHandler.kt b/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/GlobalExceptionHandler.kt index 7109c2b..98b2435 100644 --- a/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/GlobalExceptionHandler.kt +++ b/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/GlobalExceptionHandler.kt @@ -3,8 +3,10 @@ package org.vibeerp.platform.bootstrap.web import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus import org.springframework.http.ProblemDetail +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.server.ResponseStatusException import org.vibeerp.platform.security.AuthenticationFailedException import org.vibeerp.platform.security.authz.PermissionDeniedException import java.time.Instant @@ -87,6 +89,23 @@ class GlobalExceptionHandler { problem(HttpStatus.FORBIDDEN, "permission denied: '${ex.permissionKey}'") /** + * Spring's `ResponseStatusException` carries its own HTTP status code + * (e.g. 400, 403, 409). Handlers and filters that throw it expect the + * status to propagate verbatim. Without this handler the catch-all + * `Throwable` branch would swallow it as a generic 500. + */ + @ExceptionHandler(ResponseStatusException::class) + fun handleResponseStatus(ex: ResponseStatusException): ResponseEntity> { + return ResponseEntity.status(ex.getStatusCode()).body(mapOf( + "type" to "about:blank", + "title" to ex.getStatusCode().toString(), + "status" to ex.getStatusCode().value(), + "detail" to (ex.reason ?: ex.message), + "timestamp" to Instant.now().toString(), + )) + } + + /** * Last-resort fallback. Anything not handled above is logged with a * full stack trace and surfaced as a generic 500 to the caller. The * detail message is intentionally vague so we don't leak internals.