Files
scheduller-service/src/main/java/ru/kayashov/schedule/web/GlobalExceptionHandler.java
T
2026-08-27 12:50:22 +04:00

81 lines
4.2 KiB
Java

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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> handleUnexpected(Exception e) {
log.error("Непредвиденная ошибка: {}", e.getMessage(), e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.of(500, "Internal Server Error", "Внутренняя ошибка сервиса"));
}
}