Search code examples
spring-bootreactive-programmingspring-webfluxspring-webclient

What's the correct way to get the response body from a WebClient in an error case?


I'm new to WebClient and reactive programming. I want to get the response body from a request. In case of an error the http-code, headers and body must be logged, but the body should still be returned.

After lots of digging and googling I found two solutions. But both look over complicated to me. Is there a simpler solution?

Staying with a Mono I found this solution:

public Mono<String> log(ProtocolLine protocolLine) {
    return webClient.post()
            .uri("/log")
            .body(BodyInserters.fromObject(protocolLine))
            .exchange()
            .flatMap(clientResponse -> {
                Mono<String> stringMono = clientResponse.bodyToMono(String.class);
                CompletableFuture<String> stringCompleteFuture = new CompletableFuture<String>();
                Mono<String> bodyCompletedMono = Mono.fromFuture(stringCompleteFuture);
                if (clientResponse.statusCode().isError()) {
                    stringMono.subscribe(bodyString -> {
                        LOGGER.error("HttpStatusCode = {}", clientResponse.statusCode());
                        LOGGER.error("HttpHeaders = {}", clientResponse.headers().asHttpHeaders());
                        LOGGER.error("ResponseBody = {}", bodyString);
                        stringCompleteFuture.complete(bodyString);
                    });
                }

                return bodyCompletedMono;
            });
}

Based on Flux it takes less code. But I think I should not use Flux if I know that there will be only one result.

public Flux<String> log(ProtocolLine protocolLine) {
    return webClient.post()
            .uri("/log")
            .body(BodyInserters.fromObject(protocolLine))
            .exchange()
            .flux()
            .flatMap(clientResponse -> {
                Flux<String> stringFlux = clientResponse.bodyToFlux(String.class).share();
                if (clientResponse.statusCode().isError()) {
                    stringFlux.subscribe(bodyString -> {
                        LOGGER.error("HttpStatusCode = {}", clientResponse.statusCode());
                        LOGGER.error("HttpHeaders = {}", clientResponse.headers().asHttpHeaders());
                        LOGGER.error("ResponseBody = {}", bodyString);
                    });
                }

                return stringFlux;
            });
}

Solution

  • both solutions are ugly and wrong. You should almost never subscribe in the middle of a reactive pipeline. The subscriber is usually the calling client, not your own application.

        public Mono<String> log(ProtocolLine protocolLine) {
        return webClient.post()
                .uri("/log")
                .body(BodyInserters.fromObject(protocolLine))
                .exchange()
                .flatMap(clientResponse -> clientResponse.bodyToMono(String.class)
                    .doOnSuccess(body -> {
                        if (clientResponse.statusCode().isError()) {
                            log.error("HttpStatusCode = {}", clientResponse.statusCode());
                            log.error("HttpHeaders = {}", clientResponse.headers().asHttpHeaders());
                            log.error("ResponseBody = {}", body);
                        }
                }));
    }
    

    Here you can see the way of thinking. We always take our clientResponse and map its body to a string. We then doOnSuccess when this Mono is consumed by the subscriber (our calling client) and check the status code if there is an error and if that is the case we log.

    The doOnSuccess method returns void so it doesn't "consume" the mono or anything, it just triggers something when this Mono says it "has something in itself", when it's "done" so to speek.

    This can be used with Flux the same way.