Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Обработка ошибок

Когда что-то идёт не так, клиент должен получить правильный код ответа и понятное сообщение, а не 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/42
HTTP/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 не найдена

Обработчик — обычный бин, ему нужна аннотация области видимости.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.