skip to content

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

level: seniorimportance: should knowfreq 40%

answer

  1. first added = outermost = request first, response last
  2. combined via andThen, left-to-right
  3. nested boxes: request inward, response outward
  4. .filter appends; .filters(list) reorders
  5. no @Order on the builder — list position only

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.

solid answer

~40 s

Filters form an ordered chain built from the list on WebClient.Builder. They are combined left-to-right with andThen, so the first filter you add is the outermost: it processes the outgoing request first and the incoming response last, while the last-added filter is closest to the actual HTTP exchange. Think of nested wrappers — request travels inward through filter1, filter2, ... to the transport, and the response unwinds back out in reverse. This matters when filters depend on each other: a logging filter added first will log the request before an auth filter added later attaches its header, so its log won't show that header. To control ordering precisely, use WebClient.builder().filters(Consumer<List<ExchangeFilterFunction>>), which exposes the mutable list so you can add(0, ...) to prepend, insert at an index, reorder, or clear. Plain .filter(...) always appends.

code

java · 11 lines
java
// Ensure auth header is attached BEFORE logging captures the request
WebClient client = WebClient.builder()
    .filters(filters -> {
        filters.add(authFilter);              // outer-ish
        filters.add(loggingFilter);           // appended after -> logs decorated request
        // to force something outermost instead:
        // filters.add(0, tracingFilter);
    })
    .build();

// Reasoning: request flows auth -> logging -> HTTP; the log now shows the auth header.

go deeper

for a junior

Know filters run in the order added and that .filters(list) exists for reordering.

for a middle

Explain that first-added is outermost: request first, response last.

for a senior

Reason about auth/log/retry ordering interactions and that no @Order support exists on the builder.

for a principal

Design a canonical filter ordering policy (tracing outermost, retry, auth, logging) reused across all clients.

## The chain model Every `.filter(...)` call appends to an ordered `List<ExchangeFilterFunction>` on the builder. When the `WebClient` is built, the filters are composed into a single chain. `ExchangeFilterFunction` has an `andThen(ExchangeFilterFunction after)` method whose contract is: *"first apply this filter, then apply the after filter."* The builder reduces the list left-to-right with `andThen`, so: - **First filter added = outermost.** It receives the request first and, because control returns back up the stack, processes the response **last**. - **Last filter added = innermost**, sitting right next to the real HTTP exchange (`ExchangeFunction`). It sees the request last (fully decorated by earlier filters) and the response first. Visualize nested boxes: ``` request -> [ filter1 [ filter2 [ (HTTP exchange) ] filter2 ] filter1 ] -> response ``` Request flows inward top-to-bottom of the add order; response flows outward bottom-to-top. ## Why order matters Consider a logging filter and an auth filter: ```java WebClient.builder() .filter(loggingFilter) // added first = outermost .filter(authFilter) // added second = inner .build(); ``` The request hits `loggingFilter` **before** `authFilter` adds the `Authorization` header — so the logged request won't contain the auth header. Reverse the order (auth first, logging second) and the log will include it. Similarly, a retry filter must usually be **outer** relative to filters that must re-run on each attempt, and inner relative to ones that should run once. ## Controlling order - `.filter(f)` — **appends** to the end of the list. - `.filters(Consumer<List<ExchangeFilterFunction>>)` — hands you the **mutable list**. You can: - `list.add(0, f)` to **prepend** (make it outermost) - `list.add(index, f)` to insert at a position - reorder or `list.clear()` to rebuild ```java WebClient.builder() .filters(list -> { list.add(authFilter); list.add(0, loggingFilter); // force logging outermost }) .build(); ``` ## Gotchas - There is **no `@Order` / Ordered support** for `ExchangeFilterFunction` on the builder — ordering is purely list position. (This differs from server-side `WebFilter`, which does honor `@Order`.) - `defaultHeaders`/`defaultRequest` are applied to the base request before filters run, so a filter can still see and override them. - Rebuilding a client with `client.mutate()` copies the existing filter list; further `.filter(...)` appends after the inherited ones. ## When to think about it Any time filters interact: auth-then-log, retry wrapping, tracing spans that must enclose the actual call, or metrics timing that should include or exclude retries.

  • Does @Order or the Ordered interface affect ExchangeFilterFunction ordering?
    No. On the WebClient builder, ordering is purely the position in the filter list. @Order applies to server-side WebFilters, not to client exchange filters. Use .filters(list -> ...) to position them.
  • You want a log line that includes the Authorization header — which filter goes first?
    Add the auth filter before the logging filter so auth (outer) attaches the header on the way in, then logging (inner) observes the fully decorated request.

saying these in an interview costs you the question

  • Saying the last-added filter processes the request first (it's actually the first-added that sees the request first)
  • Assuming @Order or @Priority controls exchange-filter ordering on the builder
  • Believing .filter() can insert at a specific position (it only appends; use .filters(list))

context