java 37 lines · 8 steps

Merging default and per-request headers in Spring

A WebClient filter that layers global default headers under optional per-request overrides on every outbound call.

Explained by highlit
1@Component
2public class HeaderMergeFilter {
3 
4 private static final String PER_REQUEST_HEADERS = HeaderMergeFilter.class.getName() + ".headers";
5 
6 private final Map<String, String> globalHeaders;
7 
8 public HeaderMergeFilter(HttpHeaderProperties properties) {
9 this.globalHeaders = properties.getDefaults();
10 }
11 
12 public ExchangeFilterFunction asFilter() {
13 return (request, next) -> {
14 ClientRequest merged = ClientRequest.from(request)
15 .headers(headers -> {
16 globalHeaders.forEach(headers::putIfAbsent);
17 HttpHeaders overrides = request.attribute(PER_REQUEST_HEADERS)
18 .map(HttpHeaders.class::cast)
19 .orElseGet(HttpHeaders::new);
20 overrides.forEach((name, values) -> headers.put(name, values));
21 })
22 .build();
23 return next.exchange(merged);
24 };
25 }
26 
27 public static Consumer<ClientRequest.Builder> withHeaders(HttpHeaders headers) {
28 return builder -> builder.attribute(PER_REQUEST_HEADERS, headers);
29 }
30 
31 @Bean
32 public WebClient outboundWebClient(WebClient.Builder builder) {
33 return builder
34 .filter(asFilter())
35 .build();
36 }
37}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Storing per-request data in a request attribute lets you thread configuration through a filter without changing method signatures.
  2. 2putIfAbsent versus put cleanly encodes precedence: defaults fill gaps while overrides win outright.
  3. 3Exposing behavior as an ExchangeFilterFunction keeps header logic in one place and applies it to every WebClient call.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Merging default and per-request headers in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code