Обработка ошибок
Когда что-то идёт не так, клиент должен получить правильный код ответа и понятное сообщение, а не 500 «Internal Server Error». В Micronaut есть три способа сообщить об ошибке: вернуть пустой результат, бросить готовое исключение с кодом или бросить своё исключение и написать для него обработчик.
Пустой результат
Вы уже видели первый способ: метод возвращает Optional, и если он пуст, Micronaut сам отвечает 404. То же происходит, если метод вернул null. Это удобно, но текст ошибки стандартный — «Page Not Found».
Исключение с кодом
HttpStatusException превращается в ответ с указанным кодом и вашим сообщением. Допустим, первую, приветственную статью удалять нельзя:
// src/main/java/com/example/ArticleController.java
@Delete("/{id}")
public HttpResponse<Void> delete(Long id) {
if (id == 1) {
throw new HttpStatusException(HttpStatus.FORBIDDEN, "Первую статью удалять нельзя");
}
return HttpResponse.noContent();
}Классы импортируются из io.micronaut.http.exceptions и io.micronaut.http. Запрос curl -X DELETE http://localhost:8080/articles/1 вернёт 403:
{"_links":{"self":[{"href":"/articles/1","templated":false}]},"_embedded":{"errors":[{"message":"Первую статью удалять нельзя"}]},"message":"Forbidden"}Своё исключение
Исключение из HTTP-пакета не стоит бросать в сервисе: сервис не должен знать про HTTP. Лучше завести исключение предметной области:
// src/main/java/com/example/ArticleNotFoundException.java
package com.example;
public class ArticleNotFoundException extends RuntimeException {
public ArticleNotFoundException(Long id) {
super("Статья " + id + " не найдена");
}
}Контроллер бросает его, если статьи нет:
// src/main/java/com/example/ArticleController.java
@Get("/{id}")
public Article show(Long id) {
return articleService.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
}Обработчик исключения
Обработчик — бин, который реализует ExceptionHandler и превращает исключение в ответ. Ответ опишем короткой записью:
// src/main/java/com/example/ErrorMessage.java
package com.example;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
public record ErrorMessage(String error) {
}// src/main/java/com/example/ArticleNotFoundHandler.java
package com.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import jakarta.inject.Singleton;
@Produces
@Singleton
public class ArticleNotFoundHandler
implements ExceptionHandler<ArticleNotFoundException, HttpResponse<ErrorMessage>> {
@Override
public HttpResponse<ErrorMessage> handle(HttpRequest request, ArticleNotFoundException e) {
return HttpResponse.notFound(new ErrorMessage(e.getMessage()));
}
}Первый параметр типа — какое исключение ловим, второй — что возвращаем. @Produces без аргументов означает ответ в JSON. Обработчик срабатывает для любого контроллера.
curl -i http://localhost:8080/articles/42HTTP/1.1 404 Not Found
content-type: application/json
{"error":"Статья 42 не найдена"}Какой способ выбрать
| Способ | Когда подходит |
|---|---|
Optional или null | Простое «не найдено» в контроллере |
HttpStatusException | Разовая ошибка прямо в контроллере |
Своё исключение и ExceptionHandler | Ошибка из сервиса, одинаковый ответ во всём API |
Частая ошибка
Если забыть @Singleton на обработчике, Micronaut его не увидит: исключение дойдёт до фреймворка, и клиент получит 500. В журнале будет только текст исключения:
ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Статья 42 не найденаОбработчик — обычный бин, ему нужна аннотация области видимости.