Beware of using spring WebClient(Episode1)

header explosion

When you create a webClient in advance and reuse it whenever you need it,

this.webClient = WebClient.builder()
        .clientConnector(connector)
        .baseUrl("https://yangbongsoo.tech")
        .build()
        .post();

If the header is created as below, there is a problem that the request header continues to accumulate and is appended.

this.webClient
    .uri(uriBuilder -> uriBuilder
        .path("/api")
        .queryParam("txId", route.getId() + "-" + System.currentTimeMillis())
        .build()
    )
    .header("ybs", "123") // Be careful !!
    .contentType(APPLICATION_FORM_URLENCODED)
    .body(BodyInserters.fromValue("body"))
    .retrieve()
    .toEntity(String.class)
    .doOnError(result -> {
        logger.warn(result.getMessage());
    });

cf) If It called 3 times, stack one by one, and the final request header is as below.

ybs: 123
ybs: 123
ybs: 123

Problem solving is simple. In order to send a fixed header value, you can use defaultHeader method when creating a webClient in advance.

this.webClient = WebClient.builder()
        .clientConnector(connector)
        .baseUrl("https://yangbongsoo.tech")
        .defaultHeader("ybs", "123")
        .build();

But let's find out why the problem occurred specifically. When creating webClient in advance, there was a post method (also a get method), which internally creates a new DefaultRequestBodyUriSpec class.

public RequestBodyUriSpec post() {
    return this.methodInternal(HttpMethod.POST);
}

private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) {
    return new DefaultWebClient.DefaultRequestBodyUriSpec(httpMethod);
}

Since there is an HttpHeaders object as a member variable of DefaultRequestBodyUriSpec class, there is no accumulation If you use post or get method each time a request is made.

class DefaultWebClient implements WebClient {
    private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
        private final HttpMethod httpMethod;

        DefaultRequestBodyUriSpec(HttpMethod httpMethod) {
            this.httpMethod = httpMethod;
        }

        ...
    }
}

However if you pre-create post or get method, webClient will use the same HttpHeaders object, and continues to add.

@Override
public DefaultRequestBodyUriSpec header(String headerName, String... headerValues) {
    for (String headerValue : headerValues) {
        getHeaders().add(headerName, headerValue);
    }
    return this;
}

private HttpHeaders getHeaders() {
    if (this.headers == null) {
        this.headers = new HttpHeaders();
    }
    return this.headers;
}

memory leak when using exchange

The exchange method can be controlled in more detail than retrieve method. However, be careful of memory leak. Let's see the below codes. If the api response status code isn't 200(OK), webClient doesn't consume the response body and can make memory leak or connection leak.

.exchange()
.flatMap(res -> res.toEntity(String.class))
.doOnSuccess(
    result -> {
        if (result.getStatusCode() == HttpStatus.OK) {
            String body = result.getBody();
            doProcess(body);
        } else {
            // empty
        }
});

Spring maintainer recommend .retrieve().toEntity(String.class). Because It switches to WebClientResponseException containing response body content when the 4xx-5xx status code is come.

Fortunately, exchange method has deprecated since 5.3. Now we can use exchangeToMono or exchangeToFlux instead.

cf) There are various WebClient connector implementations, but I explained based on reactor-netty.