Spring Webflux : Webclient : Get body on error
Asked Answered
A

15

57

I am using the webclient from spring webflux, like this :

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

It is working well. I now want to handle the error from the webservice I am calling (Ex 500 internal error). Normally i would add an doOnError on the "stream" and isu the Throwable to test the status code,

But my issue is that I want to get the body provided by the webservice because it is providing me a message that i would like to use.

I am looking to do the flatMap whatever happen and test myself the status code to deserialize or not the body.

Acquiescence answered 16/6, 2017 at 15:34 Comment(2)
Hey, did you get the answer. I am also looking to get error response body from another service which is different than the success response body.Thereunder
I have a similar issue, in that I have a response from a WebClient, which is erroring (422), the error response has a body (specifically Error code within some JSON content, that I need to retrieve. I found no help in any of the answers here (though maybe didn't understand enough?), however I found that I was indeed helped by this tutorial: medium.com/nerd-for-tech/…Maag
L
8

Note that as of writing this, 5xx errors no longer result in an exception from the underlying Netty layer. See https://github.com/spring-projects/spring-framework/commit/b0ab84657b712aac59951420f4e9d696c3d84ba2

Locally answered 6/7, 2017 at 11:43 Comment(0)
P
37

I prefer to use the methods provided by the ClientResponse to handle http errors and throw exceptions:

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 (..) ...
         ...

In fact, it's the same logic used in the DefaultResponseSpec of DefaultWebClient to handle errors. The DefaultResponseSpec is an implementation of ResponseSpec that we would have if we made a retrieve() instead of exchange().

Planogamete answered 28/2, 2020 at 13:50 Comment(3)
super! Thank you so much. This is the perfect solution. There are so many workarounds with filter, global exception handler others.. but this works perfectly. For me, I want to do some actions on DB in subscribe.. and it works great!Executrix
Hi, in the new versions of Spring Webflux the method exchange is deprecated, how can I use this solution with retrieve?Bifid
@Bifid you can use exchangeToMono or exchangeToFlux...Planogamete
E
35

Don't we have 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);
    }

And:

    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));
        }
    }
Emanate answered 3/6, 2020 at 15:2 Comment(2)
onStatus is cool, but one quark it has is that an empty response body will circumvent the response -> { } lambda. i.e. a Mono.error is never returned. instead a null Mono will be returned.Laborer
Well here we always have a body. Maybe 500 without a body is... unusual?Emanate
G
25

I got the error body by doing like this:

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();
Gere answered 29/10, 2020 at 14:57 Comment(1)
Great answer, you might just want to return: Mono.error( new WebClientResponseException(error.toString(), response.rawStatusCode(), response.statusCode().name(), response.headers().asHttpHeaders(), error.toString().getBytes(), null))))Linguist
U
20

You could also do this

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);
});

Read this article for more examples link, I found it to be helpful when I experienced a similar problem with error handling

Uttica answered 26/3, 2018 at 19:52 Comment(3)
Spent an entire day trying to find this answer. Totally forgot that the exception is embedded in the response body. Thanks!Severus
How can we throw exception incase of is5xxServerError and print backend response?Crevice
@Crevice you can throw an exception instead of returning the success response. Check the below responses https://mcmap.net/q/1622533/-spring-webflux-webclient-get-body-on-errorUttica
M
8

I do something like this:

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());
                }
            });

and then:

responseMono.subscribe(v -> { }, ex -> processError(ex));
Manteau answered 16/6, 2017 at 15:44 Comment(2)
Not working on our side, we never go in doOnNext in case of servererror. We tried to use doOnEach but we can get the body from thereAcquiescence
What application server are you using ? Netty for our part.Acquiescence
L
8

Note that as of writing this, 5xx errors no longer result in an exception from the underlying Netty layer. See https://github.com/spring-projects/spring-framework/commit/b0ab84657b712aac59951420f4e9d696c3d84ba2

Locally answered 6/7, 2017 at 11:43 Comment(0)
R
8

Using what I learned this fantastic SO answer regarding the "Correct way of throwing exceptions with Reactor", I was able to put this answer together. It uses .onStatus, .bodyToMono, and .handle to map the error response body to an exception.

// create a chicken
webClient
    .post()
    .uri(urlService.getUrl(customer) + "/chickens")
    .contentType(MediaType.APPLICATION_JSON)
    .body(Mono.just(chickenCreateDto), ChickenCreateDto.class) // outbound request body
    .retrieve()
    // Might be HttpStatusCode depending on your Spring version
    .onStatus(HttpStatus::isError, clientResponse ->
        clientResponse.bodyToMono(ChickenCreateErrorDto.class)
            .handle((error, sink) -> 
                sink.error(new ChickenException(error))
            )
    )
    .bodyToMono(ChickenResponse.class)
    .subscribe(
            this::recordSuccessfulCreationOfChicken, // accepts ChickenResponse
            this::recordUnsuccessfulCreationOfChicken // accepts throwable (ChickenException)
    );
Rostand answered 4/10, 2021 at 22:20 Comment(1)
Latest Spring version, its changed to HttpStatus changed to HttpStatusCode and they did it to waste my 30 mins 🤦‍♂️Priestess
E
4

I had just faced the similar situation and I found out webClient does not throw any exception even it is getting 4xx/5xx responses. In my case, I use webclient to first make a call to get the response and if it is returning 2xx response then I extract the data from the response and use it for making the second call. If the first call is getting non-2xx response then throw an exception. Because it is not throwing exception so when the first call failed and the second is still be carried on. So what I did is

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));
}
Exsanguine answered 23/8, 2019 at 14:47 Comment(0)
A
3

We have finally understood what is happening : By default the Netty's httpclient (HttpClientRequest) is configured to fail on server error (response 5XX) and not on client error (4XX), this is why it was always emitting an exception.

What we have done is extend AbstractClientHttpRequest and ClientHttpConnector to configure the httpclient behave the way the want and when we are invoking the WebClient we use our custom ClientHttpConnector :

 WebClient.builder().clientConnector(new CommonsReactorClientHttpConnector()).build();
Acquiescence answered 23/6, 2017 at 7:25 Comment(0)
S
2

The retrieve() method in WebClient throws a WebClientResponseException whenever a response with status code 4xx or 5xx is received.

You can handle the exception by checking the response status code.

   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))

Reference: https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/

Scyphus answered 5/5, 2021 at 12:37 Comment(0)
C
1

You have to cast the "Throwable e" parameter to WebClientResponseException, then you can call getResponseBodyAsString() :

    WebClient webClient = WebClient.create("https://httpstat.us/404");
    Mono<Object> monoObject = webClient.get().retrieve().bodyToMono(Object.class);
    monoObject.doOnError(e -> {
        if( e instanceof WebClientResponseException ){
            System.out.println(
                "ResponseBody = " + 
                    ((WebClientResponseException) e).getResponseBodyAsString() 
            );
        }
    }).subscribe();
    // Display : ResponseBody = 404 Not Found
Counterrevolution answered 4/2, 2023 at 2:35 Comment(0)
P
0

I stumbled across this so figured I might as well post my code.

What I did was create a global handler that takes career of request and response errors coming out of the web client. This is in Kotlin but can be easily converted to Java, of course. This extends the default behavior so you can be sure to get all of the automatic configuration on top of your customer handling.

As you can see this doesn't really do anything custom, it just translates the web client errors into relevant responses. For response errors the code and response body are simply passed through to the client. For request errors currently it just handles connection troubles because that's all I care about (at the moment), but as you can see it can be easily extended.

@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,
)
Parrisch answered 28/6, 2021 at 15:56 Comment(0)
C
0

Actually, you can log the body easily in the onError call:

            .doOnError {
                logger.warn { body(it) }
            }

and:

    private fun body(it: Throwable) =
        if (it is WebClientResponseException) {
            ", body: ${it.responseBodyAsString}"
        } else {
            ""
        }
Common answered 5/3, 2022 at 16:35 Comment(0)
H
0

For those that wish to the details of a WebClient request that triggered a 500 Internal System error, override the DefaultErrorWebExceptionHandler like as follows.

The Spring default is to tell you the client had an error, but it does not provide the body of the WebClient call, which can be invaluable in debugging.

/**
 * Extends the DefaultErrorWebExceptionHandler to log the response body from a failed WebClient
 * response that results in a 500 Internal Server error.
 */
@Component
@Order(-2)
public class ExtendedErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {

  private static final Log logger = HttpLogging.forLogName(ExtendedErrorWebExceptionHandler.class);

  public FsErrorWebExceptionHandler(
      ErrorAttributes errorAttributes,
      Resources resources,
      ServerProperties serverProperties,
      ApplicationContext applicationContext,
      ServerCodecConfigurer serverCodecConfigurer) {
    super(errorAttributes, resources, serverProperties.getError(), applicationContext);
    super.setMessageWriters(serverCodecConfigurer.getWriters());
    super.setMessageReaders(serverCodecConfigurer.getReaders());
  }

  /**
   * Override the default error log behavior to provide details for WebClientResponseException. This
   * is so that administrators can better debug WebClient errors.
   *
   * @param request The request to the foundation service
   * @param response The response to the foundation service
   * @param throwable The error that occurred during processing the request
   */
  @Override
  protected void logError(ServerRequest request, ServerResponse response, Throwable throwable) {
    // When the throwable is a WebClientResponseException, also log the body
    if (HttpStatus.resolve(response.rawStatusCode()) != null
        && response.statusCode().equals(HttpStatus.INTERNAL_SERVER_ERROR)
        && throwable instanceof WebClientResponseException) {
      logger.error(
          LogMessage.of(
              () ->
                  String.format(
                      "%s 500 Server Error for %s\n%s",
                      request.exchange().getLogPrefix(),
                      formatRequest(request),
                      formatResponseError((WebClientResponseException) throwable))),
          throwable);
    } else {
      super.logError(request, response, throwable);
    }
  }

  private String formatRequest(ServerRequest request) {
    String rawQuery = request.uri().getRawQuery();
    String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
    return "HTTP " + request.methodName() + " \"" + request.path() + query + "\"";
  }

  private String formatResponseError(WebClientResponseException exception) {
    return String.format(
        "%-15s %s\n%-15s %s\n%-15s %d\n%-15s %s\n%-15s '%s'",
        "  Message:",
        exception.getMessage(),
        "  Status:",
        exception.getStatusText(),
        "  Status Code:",
        exception.getRawStatusCode(),
        "  Headers:",
        exception.getHeaders(),
        "  Body:",
        exception.getResponseBodyAsString());
  }
}
Heedless answered 19/5, 2022 at 15:27 Comment(0)
I
0

In newer versions where .exchange() is deprecated, you can use exchangeToMono to handle any erroneous status code and provide a custom value.

.exchangeToMono(
  r -> {
    if (r.statusCode().isError()) {
      return Mono.just("Some string")
    } else {
      return r.bodyToMono(String.class);
    }
  }
);
Indefinable answered 23/4 at 11:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.