Demystifying WebClient in Spring WebFlux: Handling Headers, Logging, and Attributes
In the world of reactive programming, Spring WebFlux's WebClient
has become a popular choice for making HTTP requests. It offers a powerful and flexible way to interact with remote services. However, it's essential to understand that WebClient
behaves differently from the traditional RestTemplate
, particularly when it comes to handling headers, logging requests and responses, and managing attributes.
In this article, we'll delve into some key differences and best practices for using WebClient
effectively in these scenarios.
Content-Length and Host Headers: A Closer Look
Content-Length Header
The Content-Length
header specifies the length of the request or response body in bytes. When using RestTemplate
, this header is automatically recalculated based on the actual payload size. However, in the world of WebClient
, it's important to note that the framework doesn't automatically overwrite an explicitly set Content-Length
header.
This can have consequences. If you set the Content-Length
header incorrectly, mismatching the actual payload size, the server might interpret the request incorrectly, leading to a BadRequest (400)
response. To avoid this, the best practice is to omit setting the Content-Length
header altogether. WebClient
is designed to calculate the Content-Length
automatically based on the payload.
Host Header
The Host
header specifies the target host of the request. RestTemplate
automatically derives this header from the provided URI, ensuring that it matches the destination. However, WebClient
does not override a manually set Host
header.
Setting an incorrect Host
header could lead to unintended consequences, such as Service Unavailable (503)
or even making requests to the wrong destination. To mitigate this risk, the recommended approach is to avoid setting the Host
header explicitly. Let WebClient
determine the appropriate Host
header from the base URL.
Logging Requests and Responses: A Debugging Necessity
Effective logging is crucial for debugging and tracing requests and responses. WebClient
makes it possible to achieve detailed request-response logging through custom ExchangeFilterFunctions
.
Setting Up Logging Filters
To log requests and responses, you can create custom ExchangeFilterFunctions
. These functions intercept requests and responses, allowing you to inspect and log the details.
Here's an example of how to set up logging filters for requests and responses:
WebClient webClient = WebClient.builder()
.filter(logRequest())
.filter(logResponse())
// ... other configuration ...
.build();
Implementing Logging Filters
The custom ExchangeFilterFunction
methods logRequest
and logResponse
handle the actual logging. Let's take a closer look:
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(request -> {
// Log request details
// ...
return Mono.just(request);
});
}
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(response -> {
// Log response details
// ...
return Mono.just(response);
});
}
Customizing logPrefix
You can enhance your logging by customizing the logPrefix
, providing more context to your logs. For instance, you might want to include a transaction ID, HTTP method, and URL.
Here's how you can do it:
webClient.method(method)
.attribute(LOG_ID_ATTRIBUTE, buildLogPrefix(transactionId, method, url))
// ... other configuration ...
.exchangeToMono(/* ... */)
.retryWhen(/* ... */)
.block();
Putting It All Together: Complete WebClient Example
Now, let's put everything we've discussed into a complete example. We'll set up a WebClient
, customize the logPrefix
, and see how the logging filters capture the request and response details.
public class WebClientHelper {
private final ObjectMapper objectMapper;
private static final int UNLIMITED_WEB_CLIENT_MAX_IN_MEMORY_SIZE = -1;
private static final String TRANSACTION_ID = "transaction_id";
public <T, R> R apiCall(final String url, final HttpMethod method, T payload, MultiValueMap<String, String> requestHeaders,
Class<R> responseType) {
return buildDefaultWebClient(url)
.method(method)
// set logPrefix to transaction_id + url
.attribute(LOG_ID_ATTRIBUTE, buildLogPrefix(requestHeaders.getFirst(TRANSACTION_ID), method, url))
.headers(headers -> headers.addAll(transform(requestHeaders)))
.body(BodyInserters.fromValue(payload != null ? payload : ""))
.retrieve()
.bodyToMono(responseType)
.retryWhen(retrySpec())
.block();
}
public <T, R> ResponseEntity<R> apiCallWithStatus(final String url, final HttpMethod method, T payload,
MultiValueMap<String, String> requestHeaders, Class<R> responseType) {
return buildDefaultWebClient(url)
.method(method)
// set logPrefix to transaction_id + url
.attribute(LOG_ID_ATTRIBUTE, buildLogPrefix(requestHeaders.getFirst(TRANSACTION_ID), method, url))
.headers(headers -> headers.addAll(transform(requestHeaders)))
.body(BodyInserters.fromValue(payload != null ? payload : ""))
.exchangeToMono(clientResponse -> clientResponse.bodyToMono(responseType)
.map(body -> new ResponseEntity<>(body, clientResponse.statusCode())))
.retryWhen(retrySpec())
.block();
}
private String buildLogPrefix(String transactionId, HttpMethod method, String url) {
return "transactionId="
+ transactionId
+ ", method="
+ method
+ ", url="
+ url;
}
private HttpHeaders transform(MultiValueMap<String, String> requestHeaders) {
HttpHeaders httpHeaders = new HttpHeaders();
requestHeaders.toSingleValueMap().forEach((key, value) -> {
// ignore host, RestTemplate will override it if the given value wrong
// while WebClient will not which might lead to 503 service unavailable
if (HttpHeaders.HOST.equalsIgnoreCase(key)) {
return;
}
// ignore content length, RestTemplate will override it if the given value wrong
// while WebClient will not which might lead to BAD_REQUEST from server
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(key)) {
return;
}
httpHeaders.add(key, value);
});
return httpHeaders;
}
private Retry retrySpec() {
// retry max 3 times in case any 5xx server error
return Retry.fixedDelay(3, java.time.Duration.ofSeconds(2))
.filter(throwable -> {
if (throwable instanceof WebClientResponseException) {
WebClientResponseException ex = (WebClientResponseException) throwable;
HttpStatus statusCode = ex.getStatusCode();
return statusCode.is5xxServerError();
}
return false;
});
}
private void logHeaders(HttpHeaders httpHeaders) {
httpHeaders.forEach((name, values) -> {
if (!name.equalsIgnoreCase(HttpHeaderNames.AUTHORIZATION.toString())) {
log.info("{} : {}", name, String.join(" ", values));
}
});
}
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(response -> {
log.info("Response - {} - {} :", response.logPrefix(), response.statusCode());
logHeaders(response.headers().asHttpHeaders());
return Mono.just(response);
});
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(request -> {
log.info("Request - {} -:", request.logPrefix());
logHeaders(request.headers());
return Mono.just(request);
});
}
private WebClient buildDefaultWebClient(String baseUrl) {
return WebClient.builder().baseUrl(baseUrl)
.filter(logRequest())
.filter(logResponse())
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer -> {
configurer.defaultCodecs().maxInMemorySize(UNLIMITED_WEB_CLIENT_MAX_IN_MEMORY_SIZE);
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper));
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper));
})
.build())
.build();
}
}
Conclusion
Understanding the nuances of WebClient
in Spring WebFlux is essential for building robust and reliable applications. By grasping the differences in handling headers, mastering request-response logging, and harnessing the power of attributes, you can wield WebClient
to its full potential.
In this article, we've explored how to handle Content-Length
and Host
headers appropriately, implement effective request-response logging using custom filters, and customize the logPrefix
to enrich your logs. Armed with this knowledge, you're well-equipped to navigate the intricacies of WebClient
in your reactive applications.
Remember, WebClient
offers more than just making HTTP requests; it's a versatile tool that empowers you to build efficient and reliable communication between microservices and external APIs.