Beware of using spring WebClient(Episode3)

1. When we don't need response body

There are three options that can use when we don't need response body. releaseBody(), toBodilessEntity(), bodyToMono(Void.class)

However the reactor-netty maintainer recommended only two method releaseBody(), toBodilessEntity() in youtube video(2020.9.25)

reactor-netty maintainer said that the reactor-netty can't return a connected connection to the connection pool when using bodyToMono(Void.class). This is because the reactor-netty doesn't know whether we need to receive something more from the server. Therefore, in this case, the reactor-netty closes the corresponding connection and removes it from the connection pool. On the other hand, releaseBody() and toBodilessEntity() are drained from the spring framework.

You can say "what are you talking about? we used the bodyToMono(Void.class) because we don't need the response body. but why we care about whether we need to receive something more from the server? ".

This is the case when a response body is received even though bodyToMono(Void.class)is used. The connection closes when the response body is received, but cannot be returned to the connection pool.

However The bodyToMono(Void.class) connection pool return issue was resolved in the patch reflected in 5.3.0-RC2.

The patch was made on 2020.09.26, and the youtube video was uploaded on 2020.09.25, so I asked again and received confirmation because I thought the reactor-netty maintainer might not have known. As a result, there is no problem using bodyToMono (Void.class). image.png

In DefaultWebClient (5.3.0-RC2) diff file, we can see the existing drainBody code has been modified. image.png

2. duplicated url encoding issue

In general, scheme, domain, and port information are entered in baseUrl and we use uriBuilder to add a path or query.

WebClient.builder()
    .clientConnector(connector)
    .baseUrl("http://localhost:8080")
    .build();

... 

this.webClient
    .get()
    .uri(uriBuilder -> uriBuilder
        .path("/api")
        // %3C is '<' encoded character
        .query("name=yang%3Cbong%3Csoo") // beware !!
        .build()
    )
    .retrieve()

If we write baseUrl, baseUrl information such as scheme, host, etc will internally go to the UriComponentsBuilder object. so, If we use uriBuilder, the UriComponentsBuilder object internally has already scheme and host information.

If uriBuilder execute build method in uri lambda body, DefaultUriBuilderFactory.uriComponentsBuilder.build().expand(uriVars) is executed internally, and encoding is set to false by default.

public UriComponents build() {
    return build(false);
}

We should be careful about that if we make it using uriBuilder, it will be encoded and sent. In the above code, if we put an encoded value, it will be encoded again, so we should pay attention to the query value.

So what should we do to avoid encoding? First, there is a way to make UriComponentsBuilder a new instance and set true when building. However, we can't use the pre-registered one with baseUrl because we didn't use the existing UriComponentsBuilder and created a new one.

.uri(uriBuilder -> {
    UriComponentsBuilder test = UriComponentsBuilder.newInstance();
    test.fromUriString(FULL_URL)
    return test.build(true).toUri();
});

The second method is to use the existing UriBuilder.

.uri(uriBuilder -> {
  DefaultUriBuilderFactory test (DefaultUriBuilderFactory.DefaultUriBuilder)uriBuilder;
  test.uriString(PATH + "?" + QUERY_STRING);
  test.setEncodingMode(EncodingMode.NONE);
  return test.builder().build();
});

If we type-cast and use the existing UriBuilder implementation DefaultUriBuilderFactory.DefaultUriBuilder as above, the scheme and host information already exists through baseUrl and only the rest is filled with the uriString method. And it is possible to set whether to encode through the setEncodingMode method.

However, this method is not available. The DefaultUriBuilderFactory.DefaultUriBuilder class modifier is private so we can't import and use.

Eventually I create a new URI object in URI.create() and worked on it. I can't use the pre-registered one in baseUrl likewise.

URI uri = URI.create(FULL_URL);
this.webClient
  .get()
  .uri(uri)
  .retrieve()
  ...