skip to content

ExchangeFilterFunction

ExchangeFilterFunction wraps every request for auth headers, logging or retries, composed in order on the builder. The clean answer to 'where would you put the token propagation'.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is an ExchangeFilterFunction in Spring WebClient, and how do you register one?

level: juniorimportance: must knowfreq 60%

answer

  1. filter(ClientRequest, ExchangeFunction) -> Mono<ClientResponse>
  2. next.exchange(request) continues chain
  3. ClientRequest is immutable -> from().build()
  4. register via builder().filter(...)
  5. non-blocking, shared thread-safe client

basics

~10 s

It is an interceptor for WebClient calls. It receives each outgoing request plus the next step in the chain, and can modify the request or response. You register it with WebClient.builder().filter(...).

solid answer

~40 s

ExchangeFilterFunction is WebClient's equivalent of a servlet filter or interceptor: a functional interface with one method, filter(ClientRequest request, ExchangeFunction next), returning Mono<ClientResponse>. You inspect or rewrite the request, then call next.exchange(request) to continue the chain, and can also transform the returned response. It is the standard place for cross-cutting concerns shared by every call: adding auth headers, logging, correlation IDs, metrics, and retries. You attach it once on the builder with WebClient.builder().filter(myFilter).build(), so every request made by that client passes through it. Because WebClient is reactive, everything is expressed as Monos, and the filter must be non-blocking. A single configured WebClient (and its filters) is thread-safe and meant to be shared as a bean.

code

java · 12 lines
java
ExchangeFilterFunction correlationId = (request, next) -> {
    // ClientRequest is immutable: build a modified copy
    ClientRequest modified = ClientRequest.from(request)
        .header("X-Correlation-Id", UUID.randomUUID().toString())
        .build();
    return next.exchange(modified); // continue the chain
};

WebClient client = WebClient.builder()
    .baseUrl("https://api.example.com")
    .filter(correlationId)
    .build();

go deeper

for a junior

Know it's an interceptor for WebClient, registered via builder().filter(), with signature filter(ClientRequest, ExchangeFunction).

for a middle

Know ClientRequest immutability, that next.exchange continues the chain, and that filters must be non-blocking.

for a senior

Contrast with RestTemplate interceptors and server WebFilter; understand body-consumption pitfalls.

for a principal

Reason about filters as a reusable platform concern shared across many clients via a common builder configuration.

## What it is `ExchangeFilterFunction` is the interception mechanism for Spring's reactive `WebClient` (the WebFlux replacement for `RestTemplate`). It plays the same role that a `ClientHttpRequestInterceptor` plays for `RestTemplate`, or that a servlet `Filter` plays on the server side: it wraps every outgoing HTTP exchange so you can run shared logic in one place instead of copy-pasting it into every call site. It is a **functional interface** with a single abstract method: ```java Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next); ``` - **`ClientRequest`** is an *immutable* representation of the outgoing request (URL, method, headers, cookies, body). To change it you call `ClientRequest.from(request).header(...).build()` to produce a new one. - **`ExchangeFunction`** is "the rest of the chain": calling `next.exchange(newRequest)` performs the actual HTTP call (or hands off to the next filter) and returns `Mono<ClientResponse>`. - **`ClientResponse`** is the response; you can inspect status/headers and optionally transform it before returning. Because the method returns a `Mono`, the whole thing is **non-blocking / reactive** — you must not block a thread inside a filter (no `.block()`, no blocking I/O), or you can starve the small event-loop thread pool. ## How to register it Attach filters on the builder: ```java WebClient client = WebClient.builder() .baseUrl("https://api.example.com") .filter(loggingFilter()) // add one .filter(authFilter()) // add another .build(); ``` Each `.filter(...)` **appends** to an ordered list. There is also `.filters(Consumer<List<ExchangeFilterFunction>>)` which hands you the *mutable list* so you can insert at a position, reorder, or clear. ## Why use it Cross-cutting concerns that should apply to **every** request from a client: - Auth headers (bearer token, basic auth) - Logging / correlation-id propagation - Metrics and tracing - Retry / error normalization Defining them as filters keeps call sites clean and guarantees consistency. ## Gotchas - `ClientRequest` is immutable — you must build a copy to mutate. - Filters run on reactive threads; never block. - Reading a request or response **body** inside a filter consumes/buffers it, which can break the actual call if not handled carefully (see the logging question). - Configure filters once on a shared `WebClient` bean; don't rebuild a `WebClient` per request.

  • Why must a filter avoid calling .block() inside filter(...)?
    WebClient runs on a small pool of event-loop threads. Blocking one of them stalls all requests scheduled on it and can deadlock. Everything must stay reactive, chaining Monos instead of blocking.
  • How is ExchangeFilterFunction different from RestTemplate's ClientHttpRequestInterceptor?
    Same conceptual role (per-request interception) but reactive/non-blocking: it returns Mono<ClientResponse> and composes via ExchangeFunction, whereas the interceptor is synchronous and returns a ClientHttpResponse directly.

saying these in an interview costs you the question

  • Thinking you can mutate ClientRequest directly instead of building a copy via ClientRequest.from(...)
  • Blocking inside the filter (e.g., calling .block() or doing synchronous I/O)
  • Confusing it with server-side WebFilter, which intercepts incoming requests, not outgoing WebClient calls

context

open as a page

What do ExchangeFilterFunction.basicAuthentication, ofRequestProcessor, and ofResponseProcessor give you?

level: middleimportance: should knowfreq 45%

basics

~10 s

They are ready-made factory methods. basicAuthentication(user, password) adds an HTTP Basic auth header. ofRequestProcessor lets you transform just the request; ofResponseProcessor lets you transform just the response — without writing the full filter method.

open as a page

In what order do WebClient exchange filters execute, and how do you control that order?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Filters run in the order you add them on the builder. The first filter added sees the request first and the response last (it wraps the others, like nested layers). Use .filters(list -> ...) to reorder or insert.

open as a page

How would you implement retry and request/response logging as exchange filters, and what pitfalls arise?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Write a filter that calls next.exchange(request) and adds .retryWhen(...) for retries, or logs request details before/after. Main pitfalls: reading the body inside a filter consumes it, and retrying isn't safe when the request body can't be replayed.

open as a page

Design a resilient, reusable exchange-filter chain (tracing, token refresh, retry, logging) for a shared WebClient. What ordering and correctness concerns drive it?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Put tracing outermost, then retry, then auth/token-refresh, then logging closest to the call — so each retry attempt re-runs auth and logging under one trace span. Keep filters stateless, non-blocking, and configure them once on a shared WebClient bean.

open as a page