package ru.kayashov.schedule.web; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.client.RestClientException; import ru.kayashov.schedule.dto.ErrorResponse; /** * Централизованная обработка ошибок для всех REST-контроллеров: клиент (Shortcuts, * фронт, навык Алисы) всегда получает понятный JSON со статусом и описанием причины, * а не голый 500 без деталей. */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * Ожидаемые бизнес-ошибки уровня "клиент сделал что-то не по порядку" * (например, /shift без предварительного /wake) — 400, без стектрейса в теле ответа. */ @ExceptionHandler(IllegalStateException.class) public ResponseEntity handleIllegalState(IllegalStateException e) { log.warn("Некорректное состояние: {}", e.getMessage()); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ErrorResponse.of(400, "Bad Request", e.getMessage())); } /** * Ошибки валидации тела запроса (@Valid на WakeRequest/ShiftRequest) — * например, отсутствует обязательное поле wakeTime. */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidation(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(f -> f.getField() + ": " + f.getDefaultMessage()) .findFirst() .orElse("Некорректные данные запроса"); log.warn("Ошибка валидации: {}", message); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ErrorResponse.of(400, "Validation Error", message)); } @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity handleConstraintViolation(ConstraintViolationException e) { log.warn("Нарушение ограничений: {}", e.getMessage()); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ErrorResponse.of(400, "Validation Error", e.getMessage())); } /** * Baikal недоступен, таймаут, неверные креды и т.п. — отдаём 502 Bad Gateway, * подчёркивая, что проблема во внешней зависимости, а не в самом сервисе. */ @ExceptionHandler(RestClientException.class) public ResponseEntity handleCaldavFailure(RestClientException e) { log.error("Ошибка обращения к Baikal (CalDAV): {}", e.getMessage(), e); return ResponseEntity .status(HttpStatus.BAD_GATEWAY) .body(ErrorResponse.of(502, "Bad Gateway", "Не удалось связаться с календарём: " + e.getMessage())); } /** * Всё остальное непредвиденное — не раскрываем внутренние детали (стектрейс, имена * классов) клиенту, только логируем полностью на сервере. */ @ExceptionHandler(Exception.class) public ResponseEntity handleUnexpected(Exception e) { log.error("Непредвиденная ошибка: {}", e.getMessage(), e); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ErrorResponse.of(500, "Internal Server Error", "Внутренняя ошибка сервиса")); } }