Search code examples
spring-bootcookieslambdawebclientspring-webflux

WebClient 5.3+ exchange vs exchangeToMono. Extracting cookies and body together


I am currently trying to consume a whole request coming back via webClient. I need to be able to read the body and the cookies that come with it. However I am currently having issues doing both together in one call. Below I have two ways of grabbing the data I want. I am pretty new at using lambdas and webClient. I do realize that exchangeToMono() is the latest method call to return a Mono or Flux because of the possible memory leak found in exchange(). I want to say subscribing() to a return monoBody might help me but so far I have had no luck extracting the data out that way as well. Thank you all for your input.

Mono<String> monoBody = webBuilder.build()
                .get()
                .uri(baseURI + "?Level=&pageSize=50&pageNo=&name=&id=&authToken=" + sessionToken)
                .accept(MediaType.ALL)
                .header("authToken", sessionToken)
                .header("x-Token", sessionToken)
                .header("X-token-X", sessionToken)
                .header("Ref", ref)
                .exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));

 Mono<MultiValueMap<String,ResponseCookie>> monoMap = webBuilder.build()
                .get()
                .uri(baseURI + "?Level=&pageSize=50&pageNo=&unitName=&id=&authToken=" + sessionToken)
                .accept(MediaType.ALL)
                .header("authToken", sessionToken)
                .header("x-Token", sessionToken)
                .header("X-token-X", sessionToken)
                .header("Ref", ref)
                .exchangeToMono(clientResponse -> Mono.fromCallable(()-> clientResponse.cookies() ));
    

Solution

  • The key thing to remember about the Reactor API (used by WebClient) is that you are always composing operations into a 'plan' of what to do and then at some point (subscription) you execute the entire plan.

    In your case you've found the two methods to get the information you want but with a Reactive stream you can only ever have one type at any point in the stream, so if you need two types of data, you can always compose them into a composite object.

    record DataWithCookies(MultiValueMap<String,ResponseCookie> cookies, String body){}
    
    ...
    Mono<DataWithCookies> dataWithCookies =
          webBuilder.build()
                    .get()
                    .uri(baseURI + "?Level=&pageSize=50&pageNo=&unitName=&id=&authToken=" + sessionToken)
                    .accept(MediaType.ALL)
                    .header("authToken", sessionToken)
                    .header("x-Token", sessionToken)
                    .header("X-token-X", sessionToken)
                    .header("Ref", ref)
                    .exchangeToMono(response -> 
                        response.bodyToMono(String.class)
                                .map(stringBody -> new DataWithCookies(
                                                            stringBody, 
                                                            response.cookies())
                                )
                    );
    

    I used the previewed JDK 16 feature "Records" for brevity, but you could use a simple class for DataWithCookies.

    You now have a Mono that will yield the body along with the cookies. You might want to even extract that lambda into a private method for readability.

                        ...
                        .exchangeToMono(this::extractBodyAndCookies);
    
    ...
    
    private DataWithCookies extractBodyAndCookies(final ClientResponse response) {
        return response.bodyToMono(String.class)
                       .map(stringBody -> new DataWithCookies(stringBody, 
                                                              response.cookies())
                       );
    }
    

    Another option is to use Tuples.of(stringBody, response.cookies()) which avoids creating another type while still composing the two data.