The difference between Webflux exchange and retrieve

Under what circumstances?

I was using WebFlux with Spring Boot version 2.x.x.

When I was using WebClient in my code, I usually used exchange() after the request because of the API I was using; when it failed, it responded with a 200 HTTP Status along with an error body.

At that time, I made a big mistake by thinking that exchange() and retrieve() were the same.

If you think that, you are wrong. They are different. Then what is the difference between exchange and retrieve ?

The response is different

According to the docs :

  • WebClient.ResponseSpec retrieve()

    • Provides the response body when the response status is 200 OK .
    • By default, 4xx and 5xx responses result in a WebClientResponseException. To customize error handling, use onStatus handlers.
  • reactor.core.publisher.Mono<ClientResponse> exchange()

    • Deprecated since Spring 5.3 due to the possibility of leaking memory and/or connections.
    • When using exchange(), it is the responsibility of the application to consume any response content regardless of the scenario (success, error, unexpected data, etc.). Not doing so can cause a memory leak.
    • Please use exchangeToMono(Function) or exchangeToFlux(Function) instead.
  • exchangeToMono, exchangeToFlux

    • Available since Spring >= 5.3.
    • Available since Spring Boot version >= 2.4.x.

When do memory leaks occur?

They occur when you do not use ClientResponse.bodyToMono or bodyToFlux in your code, like this:


.exchange()
.flatMap { clientResponse ->
    if (clientResponse.statusCode() === HttpStatus.OK) {
        // success request.
        clientResponse.bodyToMono(CLASS::class.java)
    }
    else {
        // failed http request. in here memory leak occured.
        throw Exception()
    }
}

In the failure case, you will face a memory leak because you didn't consume the response body.

If you don't consume your response body, the connection won't be disconnected.

Now, what happens? Your HTTP connection pool will be filled with used threads. When there are no more threads available, your request will fail.

How to solve it?

If you are using a Spring Boot version higher than 2.4.x, use exchangeToMono as shown below:


.exchangeToMono(response -> {
    if (response.statusCode().equals(HttpStatus.OK)) {
        return response.bodyToMono(Person.class);
    }
    else if (response.statusCode().is4xxClientError()) {
        return response.bodyToMono(ErrorContainer.class);
    }
    else {
        return Mono.error(response.createException());
    }
});

If you are using a version of Spring Boot earlier than 2.4.x, use retrieve() or consume your response body before throwing an exception.

For example, see the code below:


clientResponse.bodyToMono<String>().defaultIfEmpty("").map {
    throw Exception()
}

Conclusion

I thought I should get into the habit of reading and using the documentation properly before using a library.
Libraries don't do everything.

Stay Hungry, Stay Foolish

参考资料

AD