Spring Webflux: Webclient: получить тело при ошибке

Я использую веб-клиент из spring webflux, например:

WebClient.create()
            .post()
            .uri(url)
            .syncBody(body)
            .accept(MediaType.APPLICATION_JSON)
            .headers(headers)
            .exchange()
            .flatMap(clientResponse -> clientResponse.bodyToMono(tClass));

Работает хорошо. Теперь я хочу обработать ошибку вызываемого веб-сервиса (внутренняя ошибка Ex 500). Обычно я добавляю doOnError в «поток» и использую Throwable для проверки кода состояния,

Но моя проблема в том, что я хочу получить тело, предоставленное веб-сервисом, потому что он предоставляет мне сообщение, которое я хотел бы использовать.

Я хочу сделать flatMap, что бы ни случилось, и проверить код состояния для десериализации тела.


person adrien le roy    schedule 16.06.2017    source источник


Ответы (10)


Обратите внимание, что на момент написания ошибки 5xx больше не приводят к исключению из нижележащего уровня Netty. См. https://github.com/spring-projects/spring-framework/commit/b0ab84657b712aac59951420f4e9d696c3d84ba2

person Arjen Poutsma    schedule 06.07.2017

Я предпочитаю использовать методы, предоставляемые ClientResponse, для обработки ошибок http и выдачи исключений:

WebClient.create()
         .post()
         .uri( url )
         .body( bodyObject == null ? null : BodyInserters.fromValue( bodyObject ) )
         .accept( MediaType.APPLICATION_JSON )
         .headers( headers )
         .exchange()
         .flatMap( clientResponse -> {
             //Error handling
             if ( clientResponse.statusCode().isError() ) { // or clientResponse.statusCode().value() >= 400
                 return clientResponse.createException().flatMap( Mono::error );
             }
             return clientResponse.bodyToMono( clazz )
         } )
         //You can do your checks: doOnError (..), onErrorReturn (..) ...
         ...

Фактически, это та же логика, которая используется в DefaultResponseSpec DefaultWebClient для обработки ошибок. DefaultResponseSpec - это реализация ResponseSpec, которая была бы у нас, если бы мы сделали retrieve () вместо exchange ().

person Omar ZRIDI    schedule 28.02.2020
comment
супер! Большое спасибо. Это идеальное решение. Есть так много обходных путей с фильтром, глобальным обработчиком исключений ... но это отлично работает. Для меня я хочу сделать некоторые действия с БД в подписке .. и это отлично работает! - person Winster; 10.04.2020

Разве у нас нет onStatus()?

    public Mono<Void> cancel(SomeDTO requestDto) {
        return webClient.post().uri(SOME_URL)
                .body(fromObject(requestDto))
                .header("API_KEY", properties.getApiKey())
                .retrieve()
                .onStatus(HttpStatus::isError, response -> {
                    logTraceResponse(log, response);
                    return Mono.error(new IllegalStateException(
                            String.format("Failed! %s", requestDto.getCartId())
                    ));
                })
                .bodyToMono(Void.class)
                .timeout(timeout);
    }

А также:

    public static void logTraceResponse(Logger log, ClientResponse response) {
        if (log.isTraceEnabled()) {
            log.trace("Response status: {}", response.statusCode());
            log.trace("Response headers: {}", response.headers().asHttpHeaders());
            response.bodyToMono(String.class)
                    .publishOn(Schedulers.elastic())
                    .subscribe(body -> log.trace("Response body: {}", body));
        }
    }
person WesternGun    schedule 03.06.2020

Вы также можете сделать это

return webClient.getWebClient()
 .post()
 .uri("/api/Card")
 .body(BodyInserters.fromObject(cardObject))
 .exchange()
 .flatMap(clientResponse -> {
     if (clientResponse.statusCode().is5xxServerError()) {
        clientResponse.body((clientHttpResponse, context) -> {
           return clientHttpResponse.getBody();
        });
     return clientResponse.bodyToMono(String.class);
   }
   else
     return clientResponse.bodyToMono(String.class);
});

Прочтите эту статью для получения дополнительных примеров ссылка, я обнаружил, что это быть полезным, когда у меня возникла аналогичная проблема с обработкой ошибок

person Mohale    schedule 26.03.2018
comment
Целый день я пытался найти ответ. Совсем забыл, что исключение встроено в тело ответа. Спасибо! - person Blake Neal; 16.03.2020
comment
Как мы можем выдать исключение в случае is5xxServerError и распечатать ответ серверной части? - person Rocky4Ever; 25.08.2020
comment
@ Rocky4Ever вы можете генерировать исключение вместо того, чтобы возвращать успешный ответ. Проверьте ответы ниже stackoverflow.com/a/44593201/4200414 - person Mohale; 25.08.2020

Я получил тело ошибки следующим образом:

webClient
...
.retrieve()    
.onStatus(HttpStatus::isError, response -> response.bodyToMono(String.class) // error body as String or other class
                                                   .flatMap(error -> Mono.error(new RuntimeException(error)))) // throw a functional exception
.bodyToMono(MyResponseType.class)
.block();
person Karim G    schedule 29.10.2020

Я делаю примерно так:

Mono<ClientResponse> responseMono = requestSpec.exchange()
            .doOnNext(response -> {
                HttpStatus httpStatus = response.statusCode();
                if (httpStatus.is4xxClientError() || httpStatus.is5xxServerError()) {
                    throw new WebClientException(
                            "ClientResponse has erroneous status code: " + httpStatus.value() +
                                    " " + httpStatus.getReasonPhrase());
                }
            });

а потом:

responseMono.subscribe(v -> { }, ex -> processError(ex));
person Artem Bilan    schedule 16.06.2017
comment
Не работая на нашей стороне, мы никогда не заходим в doOnNext в случае ошибки servererror. Мы пробовали использовать doOnEach, но мы можем получить тело оттуда - person adrien le roy; 19.06.2017
comment
Какой сервер приложений вы используете? Нетти с нашей стороны. - person adrien le roy; 19.06.2017
comment
Помню, я обещал не делать этого ... Но если у вас есть свободное время, задайте похожий вопрос (не о SI) stackoverflow.com/ questions / 59313564 / - person gstackoverflow; 13.12.2019

Я только что столкнулся с подобной ситуацией, и я обнаружил, что webClient не генерирует никаких исключений, даже если он получает ответы 4xx / 5xx. В моем случае я использую веб-клиент, чтобы сначала выполнить вызов, чтобы получить ответ, и если он возвращает ответ 2xx, я извлекаю данные из ответа и использую их для выполнения второго вызова. Если первый вызов получает ответ, отличный от 2xx, генерирует исключение. Потому что он не генерирует исключение, поэтому, когда первый вызов не удался, а второй все еще продолжается. Так что я сделал

return webClient.post().uri("URI")
    .header(HttpHeaders.CONTENT_TYPE, "XXXX")
    .header(HttpHeaders.ACCEPT, "XXXX")
    .header(HttpHeaders.AUTHORIZATION, "XXXX")
    .body(BodyInserters.fromObject(BODY))
    .exchange()
    .doOnSuccess(response -> {
        HttpStatus statusCode = response.statusCode();
        if (statusCode.is4xxClientError()) {
            throw new Exception(statusCode.toString());
        }
        if (statusCode.is5xxServerError()) {
            throw new Exception(statusCode.toString());
        }
    )
    .flatMap(response -> response.bodyToMono(ANY.class))
    .map(response -> response.getSomething())
    .flatMap(something -> callsSecondEndpoint(something));
}
person Alan Ho    schedule 23.08.2019

Мы наконец поняли, что происходит: по умолчанию httpclient Netty (HttpClientRequest) настроен на сбой при ошибке сервера (ответ 5XX), а не при ошибке клиента (4XX), поэтому он всегда генерировал исключение.

Что мы сделали, так это расширили AbstractClientHttpRequest и ClientHttpConnector, чтобы настроить httpclient, который ведет себя так, как мы хотим, и когда мы вызываем WebClient, мы используем наш настраиваемый ClientHttpConnector:

 WebClient.builder().clientConnector(new CommonsReactorClientHttpConnector()).build();
person adrien le roy    schedule 23.06.2017

Метод retrieve () в WebClient вызывает исключение WebClientResponseException всякий раз, когда получен ответ с кодом состояния 4xx или 5xx.

Вы можете обработать исключение, проверив код состояния ответа.

   Mono<Object> result = webClient.get().uri(URL).exchange().log().flatMap(entity -> {
        HttpStatus statusCode = entity.statusCode();
        if (statusCode.is4xxClientError() || statusCode.is5xxServerError())
        {
            return Mono.error(new Exception(statusCode.toString()));
        }
        return Mono.just(entity);
    }).flatMap(clientResponse -> clientResponse.bodyToMono(JSONObject.class))

Ссылка: https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/

person Pramod H G    schedule 05.05.2021

Я наткнулся на это, поэтому решил, что могу опубликовать свой код.

Что я сделал, так это создал глобальный обработчик, который берет на себя ответственность за ошибки запроса и ответа, исходящие из веб-клиента. Это в Kotlin, но, конечно, может быть легко преобразовано в Java. Это расширяет поведение по умолчанию, поэтому вы можете быть уверены, что вся автоматическая конфигурация будет выполняться поверх обработки ваших клиентов.

Как видите, на самом деле это не делает ничего особенного, а просто переводит ошибки веб-клиента в соответствующие ответы. В случае ошибок ответа код и тело ответа просто передаются клиенту. Для ошибок запроса в настоящее время он просто обрабатывает проблемы с подключением, потому что это все, что меня волнует (на данный момент), но, как вы можете видеть, его можно легко расширить.

@Configuration
class WebExceptionConfig(private val serverProperties: ServerProperties) {

    @Bean
    @Order(-2)
    fun errorWebExceptionHandler(
        errorAttributes: ErrorAttributes,
        resourceProperties: ResourceProperties,
        webProperties: WebProperties,
        viewResolvers: ObjectProvider<ViewResolver>,
        serverCodecConfigurer: ServerCodecConfigurer,
        applicationContext: ApplicationContext
    ): ErrorWebExceptionHandler? {
        val exceptionHandler = CustomErrorWebExceptionHandler(
            errorAttributes,
            (if (resourceProperties.hasBeenCustomized()) resourceProperties else webProperties.resources) as WebProperties.Resources,
            serverProperties.error,
            applicationContext
        )
        exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()))
        exceptionHandler.setMessageWriters(serverCodecConfigurer.writers)
        exceptionHandler.setMessageReaders(serverCodecConfigurer.readers)
        return exceptionHandler
    }
}

class CustomErrorWebExceptionHandler(
    errorAttributes: ErrorAttributes,
    resources: WebProperties.Resources,
    errorProperties: ErrorProperties,
    applicationContext: ApplicationContext
)  : DefaultErrorWebExceptionHandler(errorAttributes, resources, errorProperties, applicationContext) {

    override fun handle(exchange: ServerWebExchange, throwable: Throwable): Mono<Void> =
        when (throwable) {
            is WebClientRequestException -> handleWebClientRequestException(exchange, throwable)
            is WebClientResponseException -> handleWebClientResponseException(exchange, throwable)
            else -> super.handle(exchange, throwable)
        }

    private fun handleWebClientResponseException(exchange: ServerWebExchange, throwable: WebClientResponseException): Mono<Void> {
        exchange.response.headers.add("Content-Type", "application/json")
        exchange.response.statusCode = throwable.statusCode

        val responseBodyBuffer = exchange
            .response
            .bufferFactory()
            .wrap(throwable.responseBodyAsByteArray)

        return exchange.response.writeWith(Mono.just(responseBodyBuffer))
    }

    private fun handleWebClientRequestException(exchange: ServerWebExchange, throwable: WebClientRequestException): Mono<Void> {
        if (throwable.rootCause is ConnectException) {

            exchange.response.headers.add("Content-Type", "application/json")
            exchange.response.statusCode = HttpStatus.BAD_GATEWAY

            val responseBodyBuffer = exchange
                .response
                .bufferFactory()
                .wrap(ObjectMapper().writeValueAsBytes(customErrorWebException(exchange, HttpStatus.BAD_GATEWAY, throwable.message)))

            return exchange.response.writeWith(Mono.just(responseBodyBuffer))

        } else {
            return super.handle(exchange, throwable)
        }
    }

    private fun customErrorWebException(exchange: ServerWebExchange, status: HttpStatus, message: Any?) =
        CustomErrorWebException(
            Instant.now().toString(),
            exchange.request.path.value(),
            status.value(),
            status.reasonPhrase,
            message,
            exchange.request.id
        )
}

data class CustomErrorWebException(
    val timestamp: String,
    val path: String,
    val status: Int,
    val error: String,
    val message: Any?,
    val requestId: String,
)
person GhostBytes    schedule 28.06.2021