37

How do you retrieve the response body when trying to throw an exception based on the returned status code? For instance, lets say I want to throw an exception and reject HTTP 201.

client.post().exchange().doOnSuccess(response -> {
    if (response.statusCode().value() == 201) {
        throw new RuntimeException();
    }
}

How can I populate the exception with the response's body so I can throw a detailed WebClientResponseException?

Should I be using a different method to test the response status code?

edit: I am trying to duplicate the following functionality while using exchange() instead.

client.get()
    .retrieve()
    .onStatus(s -> !HttpStatus.CREATED.equals(s),
        MyClass::createResponseException);

//MyClass
public static Mono<WebClientResponseException> createResponseException(ClientResponse response) {
    return response.body(BodyExtractors.toDataBuffers())
            .reduce(DataBuffer::write)
            .map(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                return bytes;
            })
            .defaultIfEmpty(new byte[0])
            .map(bodyBytes -> {
                String msg = String.format("ClientResponse has erroneous status code: %d %s", response.statusCode().value(),
                        response.statusCode().getReasonPhrase());
                Charset charset = response.headers().contentType()
                        .map(MimeType::getCharset)
                        .orElse(StandardCharsets.ISO_8859_1);
                return new WebClientResponseException(msg,
                        response.statusCode().value(),
                        response.statusCode().getReasonPhrase(),
                        response.headers().asHttpHeaders(),
                        bodyBytes,
                        charset
                        );
            });
}
CoryO
  • 533
  • 1
  • 5
  • 11

3 Answers3

41

You could achieve like this by having a custom ExchangeFilterFunction and then hooking this up with WebClient.Builder before you build WebClient.

public static ExchangeFilterFunction errorHandlingFilter() {
        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
            if(clientResponse.statusCode()!=null && (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError()) ) {
                 return clientResponse.bodyToMono(String.class)
                         .flatMap(errorBody -> {
                             return Mono.error(new CustomWebClientResponseException(errorBody,clientResponse.statusCode()));
                             });
            }else {
                return Mono.just(clientResponse);
            }
        });
    }

You can use the above like this:

WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(clientOptions))
                .defaultHeader(HttpHeaders.USER_AGENT, "Application")
                .filter(WebClientUtil.errorHandlingFilter())
                .baseUrl("https://httpbin.org/")
                .build()
                .post()
                .uri("/post")
                .body(BodyInserters.fromObject(customObjectReference) )
                .exchange()
                .flatMap(response -> response.toEntity(String.class) );

So any 4XX or 5XX HttpResponse will actually throw CustomWebClientResponseException and you can configure some global exception handler and do what you like to with this. Atleast using ExchangeFilterFunction you can have global place to handle things like this or add custom headers and stuff too.

ROCKY
  • 1,763
  • 1
  • 21
  • 25
19

doOn** operators are side-effects operators and should be used for logging purposes, for example.

Here, you'd like to implement that behavior at the pipeline level, so onStatus would be a better fit here:

Mono<ClientHttpResponse> clientResponse = client.post().uri("/resource")
    .retrieve()
    .onStatus(httpStatus -> HttpStatus.CREATED.equals(httpStatus), 
        response -> response.bodyToMono(String.class).map(body -> new MyException(body)))
    bodyToXYZ(...);

Or

Mono<ResponseEntity<String>> result = client.post().uri("/resource")
    .exchange()
    .flatMap(response -> response.toEntity(String.class))
    .flatMap(entity -> {
        // return Mono.just(entity) or Mono.error() depending on the response 
    });

Note that getting the whole response body might not be a good idea if you expect large response bodies; in that case, you'd be buffering in memory large amounts of data.

Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
  • Sorry, I actually wanted to use `exchange()` so that I had access to `ClientResponse`. Can you perform the same functionality as `onStatus()` in some other way, since that method isn't available? – CoryO Oct 18 '17 at 03:02
  • added an alternative solution that might fit your use case, although I don't know why you'd specifically like access to ClientResponse. Would you like to set that up as an `ExchangeFilterFunction` in your client? – Brian Clozel Oct 18 '17 at 15:13
  • I have edited the question to hopefully provide a clearer picture of what I'm after. – CoryO Oct 19 '17 at 01:28
  • @BrianClozel what is a good practice for operating with a large response body? – Oleg Ushakov Dec 04 '19 at 13:22
  • @BrianClozel I followed your approach of handling the onStatus, but its getting swollowed i think....perhaps you got some idea? and i can send you my code? – Bernado Dec 11 '19 at 15:46
  • Could you ask a new question with all the required details? I’d be happy to take a look. – Brian Clozel Dec 11 '19 at 23:26
2

A bit of trial and error got me the following that appears to do the trick.

Mono<ClientResponse> mono = client.get().exchange()
        .flatMap(response -> {
            if (HttpStatus.CREATED.equals(response.statusCode())) {
                return Mono.just(response);
            } else {
                return response.body(BodyExtractors.toDataBuffers())
                        .reduce(DataBuffer::write)
                        .map(dataBuffer -> {
                            byte[] bytes = new byte[dataBuffer.readableByteCount()];
                            dataBuffer.read(bytes);
                            DataBufferUtils.release(dataBuffer);
                            return bytes;
                        })
                        .defaultIfEmpty(new byte[0])
                        .flatMap(bodyBytes -> {
                            String msg = String.format("ClientResponse has erroneous status code: %d %s", response.statusCode().value(),
                                    response.statusCode().getReasonPhrase());
                            Charset charset = response.headers().contentType()
                                    .map(MimeType::getCharset)
                                    .orElse(StandardCharsets.ISO_8859_1);
                            return Mono.error(new WebClientResponseException(msg,
                                    response.statusCode().value(),
                                    response.statusCode().getReasonPhrase(),
                                    response.headers().asHttpHeaders(),
                                    bodyBytes,
                                    charset
                                    ));
                        });
            }
        })
        .retry(3);
final CompletableFuture<ClientResponse> future = mono.toFuture();
CoryO
  • 533
  • 1
  • 5
  • 11
  • Thanks! For debugging this is fine. Be ware that databuffer needs to be released after reading, so response body is consumed, thus `bodyToMono()` will return null. – WesternGun Jan 28 '21 at 15:13