skip to content

How do you modify the request or response from a `WebFilter` given that `ServerWebExchange` is immutable? Explain `exchange.mutate()`.

level: seniorimportance: should knowfreq 46%

answer

  1. exchange.mutate() -> Builder -> build()
  2. pass the MUTATED exchange to chain.filter
  3. .request(r -> r.header(...))
  4. response headers writable until commit; beforeCommit()
  5. body transform = ServerHttpResponseDecorator.writeWith

basics

~20 s

ServerWebExchange (and its request/response) are effectively immutable, so you can't set fields directly. Call exchange.mutate() to get a builder, apply changes (e.g. .request(r -> r.header(...))), call .build() to get a new exchange, and pass THAT to chain.filter(...).

solid answer

~40 s

You don't mutate `ServerWebExchange` in place — it's designed to be immutable so it's safe to share across the reactive pipeline. Instead you create a modified copy: `exchange.mutate()` returns a `ServerWebExchange.Builder`. To change the request, use `.request(builder -> ...)` where the inner `ServerHttpRequest.Builder` lets you add/override headers (`.header(...)`, `.headers(consumer)`), rewrite the path (`.path(...)`), or change method. Then `.build()` yields a new `ServerWebExchange`, and you must forward *that* one: `return chain.filter(mutatedExchange)`. Response headers are simpler — `exchange.getResponse().getHeaders()` is directly mutable *until the response is committed*, so you often set them without mutate(), sometimes inside `beforeCommit(...)`. To transform the response *body*, you decorate the response with a `ServerHttpResponseDecorator` overriding `writeWith`, and pass an exchange mutated to use that decorator. Forgetting to pass the built exchange downstream is the classic bug — the original, unmodified exchange continues.

code

java · 29 lines
java
import org.springframework.web.server.*;
import reactor.core.publisher.Mono;

public class TenantHeaderFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String tenant = resolveTenant(exchange);

        // Create a modified copy of the exchange with an added request header.
        ServerWebExchange mutated = exchange.mutate()
                .request(builder -> builder.header("X-Tenant-Id", tenant))
                .build();

        // Add a response header safely, even if the handler commits later.
        exchange.getResponse().beforeCommit(() -> {
            exchange.getResponse().getHeaders().set("X-Served-Tenant", tenant);
            return Mono.empty();
        });

        // IMPORTANT: forward the MUTATED exchange, not the original.
        return chain.filter(mutated);
    }

    private String resolveTenant(ServerWebExchange exchange) {
        String host = exchange.getRequest().getHeaders().getFirst("Host");
        return host != null ? host.split("\\.")[0] : "default";
    }
}

go deeper

for a junior

Know exchange.mutate().build() creates a modified copy and you pass it to chain.filter.

for a middle

Show request header mutation and that response headers are writable until commit.

for a senior

Explain the immutability rationale, beforeCommit timing, and response-body decoration via ServerHttpResponseDecorator.

for a principal

Discuss DataBuffer lifecycle/backpressure in body rewriting, request-body caching pitfalls, and where Spring Cloud Gateway abstractions replace hand-rolled decorators.

## Why immutability `ServerWebExchange`, `ServerHttpRequest`, and `ServerHttpResponse` are treated as immutable value objects so the same instance can flow through an asynchronous, multi-operator Reactor pipeline without race conditions. Therefore there is no `request.setHeader(...)`; you build a modified copy. ## The `mutate()` builder ```java ServerWebExchange mutated = exchange.mutate() .request(builder -> builder .header("X-Tenant", tenantId) // add/override a header .path("/v2" + exchange.getRequest().getPath().value())) // rewrite path .build(); return chain.filter(mutated); // MUST forward the mutated exchange ``` - `exchange.mutate()` → `ServerWebExchange.Builder`. - `.request(Consumer<ServerHttpRequest.Builder>)` — the inner builder supports `header(name, values...)`, `headers(Consumer<HttpHeaders>)`, `path(String)`, `contextPath(String)`, `method(HttpMethod)`, `uri(URI)`, and `sslInfo(...)`. Internally it produces a `ServerHttpRequestDecorator`. - `.response(Consumer<ServerHttpResponse.Builder>)` — less common; can supply a decorated response. - `.principal(Mono<Principal>)` — override the authenticated principal. - `.build()` → the new exchange. **Critical:** the returned builder does not change the original. If you write `exchange.mutate()....build();` but then still call `chain.filter(exchange)`, your changes are discarded. ## Mutating request headers — common recipe ```java ServerWebExchange mutated = exchange.mutate() .request(r -> r.headers(h -> h.set("X-Correlation-Id", id))) .build(); return chain.filter(mutated); ``` ## Response headers The response's headers map is *directly writable* until commit, so you frequently skip `mutate()`: ```java exchange.getResponse().getHeaders().set("X-Frame-Options", "DENY"); return chain.filter(exchange); ``` But a downstream handler may set headers later; to guarantee your header wins at flush time, register a callback: ```java exchange.getResponse().beforeCommit(() -> { exchange.getResponse().getHeaders().set("X-Response-Time", elapsed()); return Mono.empty(); }); return chain.filter(exchange); ``` Once the response is **committed** (headers flushed / `isCommitted()` true), header and status changes are ignored. ## Transforming the response body Headers are easy; the body is a reactive stream you must intercept. Use a `ServerHttpResponseDecorator` overriding `writeWith`: ```java ServerHttpResponseDecorator decorated = new ServerHttpResponseDecorator(exchange.getResponse()) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { // wrap/transform DataBuffers here (e.g. capture or rewrite the body) return super.writeWith(body); } }; return chain.filter(exchange.mutate().response(decorated).build()); ``` Body rewriting requires care with `DataBuffer` lifecycle (release buffers) and Content-Length. For simple response-body transforms Spring also offers `ModifyResponseBody`-style helpers in Spring Cloud Gateway, but at the raw WebFlux level the decorator is the tool. ## Reading the request body in a filter (gotcha) The request body is a one-shot stream — reading it in a filter consumes it so the controller can't. To inspect and still forward it, you must cache/re-emit it (e.g. decorate the request with a `ServerHttpRequestDecorator` that replays cached `DataBuffer`s, or use `ServerWebExchangeUtils` caching in Spring Cloud Gateway). Naively `bodyToMono` in a filter and then continuing will usually break the handler. ## Gotchas summary - **Pass the built exchange** to `chain.filter`, not the original. - **Immutable copies**, not in-place edits, for request. - **Committed responses** ignore status/header changes — use `beforeCommit` if timing is uncertain. - **Body streams are single-subscription** — mutate/cache carefully and release `DataBuffer`s. - **Don't block** while transforming. ## When to use Request mutation: inject tenant/correlation/auth-derived headers, path rewriting, normalizing. Response mutation: security headers, timing headers, body redaction/rewrite (advanced).

  • You call exchange.mutate().request(...).build() but the controller still sees the old headers. Why?
    You didn't forward the built exchange. `mutate()` returns a new copy; you must pass *that* to `chain.filter(mutated)`. Continuing with the original `exchange` discards your changes.
  • Why can't you always just set a response header directly, and what fixes it?
    The response's header map is only mutable until the response is committed; if the handler commits before or your timing is uncertain, direct edits are ignored. Register `exchange.getResponse().beforeCommit(() -> { ...set header...; return Mono.empty(); })` so it's applied right before flush.
  • How do you transform the response body from a WebFilter?
    Wrap the response in a `ServerHttpResponseDecorator` overriding `writeWith(Publisher<DataBuffer>)`, transform/replace the buffers (releasing them properly), and pass `exchange.mutate().response(decorated).build()` downstream.

saying these in an interview costs you the question

  • Trying to call a setter on ServerHttpRequest to change a header (there is none)
  • Mutating but forwarding the original exchange to chain.filter
  • Setting response status/headers after the response is committed and expecting it to take effect
  • Reading the request body in a filter without caching, then wondering why the controller gets an empty body

context