Spring WebClient - как получить доступ к телу ответа в случае ошибок HTTP (4xx, 5xx)? - PullRequest
0 голосов
/ 18 марта 2020

Я хочу повторно выбросить мое исключение из моего REST API «База данных» в мой «REST API» Backend, но я теряю исходное сообщение об исключении.

Это то, что я получаю из моего «База данных» REST API через Почтальона:

{
    "timestamp": "2020-03-18T15:19:14.273+0000",
    "status": 400,
    "error": "Bad Request",
    "message": "I'm DatabaseException (0)",
    "path": "/database/api/vehicle/test/0"
}

С этой частью все в порядке.

Это то, что я получаю из моего "Backend" REST API через Почтальона:

{
    "timestamp": "2020-03-18T15:22:12.801+0000",
    "status": 400,
    "error": "Bad Request",
    "message": "400 BAD_REQUEST \"\"; nested exception is org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from GET http://localhost:8090/database/api/vehicle/test/0",
    "path": "/backend/api/vehicle/test/0"
}

Как вы можно увидеть, что оригинальное поле "message" потеряно.

Я использую:

  • Spring boot 2.2.5.RELEASE
  • spring-boot-starter-web
  • spring-boot-starter-webflux

Бэкенд и база данных начинаются с Tomcat ( web и webflux в одном приложении ).

This это Backend:

    @GetMapping(path = "/test/{id}")
    public Mono<Integer> test(@PathVariable String id) {
        return vehicleService.test(id);
    }

С vehicleService.test:

    public Mono<Integer> test(String id) {
        return WebClient
            .create("http://localhost:8090/database/api")
            .get()
            .uri("/vehicle/test/{id}", id)
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(Integer.class);
    }

Это база данных:

    @GetMapping(path = "/test/{id}")
    public Mono<Integer> test(@PathVariable String id) throws Exception {

        if (id.equals("0")) {
            throw new DatabaseException("I'm DatabaseException (0)");
        }

        return Mono.just(Integer.valueOf(2));
    }

Я также пытался с return Mono.error(new DatabaseException("I'm DatabaseException (0)"));

И DatabaseException:

public class DatabaseException extends ResponseStatusException {

    private static final long serialVersionUID = 1L;

    public DatabaseException(String message) {
        super(HttpStatus.BAD_REQUEST, message);

    }
}

Кажется, мой бэкэнд преобразует ответ и не может найти ответ на inte rnet.

Ответы [ 2 ]

0 голосов
/ 27 марта 2020

Код ниже теперь работает, это другой код, чем мой оригинальный вопрос, но это почти та же идея (с Backend REST api и Database REST api).

My Database REST api:

@RestController
@RequestMapping("/user")
public class UserControl {

    @Autowired
    UserRepo userRepo;

    @Autowired
    UserMapper userMapper;

    @GetMapping("/{login}")
    public Mono<UserDTO> getUser(@PathVariable String login) throws DatabaseException {
        User user = userRepo.findByLogin(login);
        if(user == null) {
            throw new DatabaseException(HttpStatus.BAD_REQUEST, "error.user.not.found");
        }
        return Mono.just(userMapper.toDTO(user));
    }
}

UserRepo - это просто @ RestReporitory.

UserMapper использует MapStruct для сопоставления моего объекта с объектом DTO.

С:

@Data
@EqualsAndHashCode(callSuper=false)
public class DatabaseException extends ResponseStatusException {

    private static final long serialVersionUID = 1L;

    public DatabaseException(String message) {
        super(HttpStatus.BAD_REQUEST, message);
    }
}

@ Data & EqualsAndHashCode come из библиотеки Lombok.

Расширение ResponseStatusException очень важно здесь, если вы этого не сделаете, ответ будет плохо обработан.

My Backend REST API, который получает данные из Database REST API:

@RestController
@RequestMapping("/user")
public class UserControl {

    @Value("${database.api.url}")
    public String databaseApiUrl;

    private String prefixedURI = "/user";

    @GetMapping("/{login}")
    public Mono<UserDTO> getUser(@PathVariable String login) {
        return WebClient
                .create(databaseApiUrl)
                .get()
                .uri(prefixedURI + "/{login}", login).retrieve()
                .onStatus(HttpStatus::isError, GlobalErrorHandler::manageError)
                .bodyToMono(UserDTO.class);
    }
}

С GlobalErrorHandler ::

public class GlobalErrorHandler {

    /**
     * Translation key for i18n
     */
    public final static String I18N_KEY_ERROR_TECHNICAL_EXCEPTION = "error.technical.exception";

    public static Mono<ResponseStatusException> manageError(ClientResponse clientResponse) {

        if (clientResponse.statusCode().is4xxClientError()) {
            // re-throw original status and message or they will be lost
            return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> {
                return Mono.error(new ResponseStatusException(response.getStatus(), response.getMessage()));
            });
        } else { // Case when it's 5xx ClientError
            // User doesn't have to know which technical exception has happened
            return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> {
                return Mono.error(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
                        I18N_KEY_ERROR_TECHNICAL_EXCEPTION));
            });
        }

    }
}

и ExceptionResponseDTO, который является обязательным для извлечения некоторых данных из clientResponse:

/**
 * Used to map <a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ClientResponse.html">ClientResponse</a> from WebFlux 
 */
@Data
@EqualsAndHashCode(callSuper=false)
public class ExceptionResponseDTO extends Exception {

    private static final long serialVersionUID = 1L;

    private HttpStatus status;

    public ExceptionResponseDTO(String message) {
        super(message);
    }

    /**
     * Status has to be converted into {@link HttpStatus}
     */
    public void setStatus(String status) {
        this.status = HttpStatus.valueOf(Integer.valueOf(status));
    }

}

Один другой связанный класс, который может быть полезен : ExchangeFilterFunctions. java

Я нашел много информации в этом выпуске:

https://github.com/spring-projects/spring-framework/issues/20280

Даже если эта информация старая отдых плохо отпущен!

0 голосов
/ 20 марта 2020

Вместо retrieve из WebClient вы можете использовать exchange, который позволяет обрабатывать ошибку и распространять пользовательское исключение с помощью сообщения, полученного из ответа службы.

private void execute()
{
    WebClient webClient = WebClient.create();

    webClient.get()
             .uri("http://localhost:8089")
             .exchange()
             .flatMap(this::handleResponse)
             .doOnNext(System.out::println)
             .block();  // not required, just for testing purposes
}

private Mono<Response> handleResponse(ClientResponse clientResponse)
{
    if (clientResponse.statusCode().isError())
    {
        return clientResponse.bodyToMono(Response.class)
                             .flatMap(response -> Mono.error(new RuntimeException(response.message)));
    }

    return clientResponse.bodyToMono(Response.class);
}

private static class Response
{
    private String message;

    public Response()
    {
    }

    public String getMessage()
    {
        return message;
    }

    public void setMessage(String message)
    {
        this.message = message;
    }

    @Override
    public String toString()
    {
        return "Response{" +
                "message='" + message + '\'' +
                '}';
    }
}
...