Explain GatewayFilterChain delegation: how does chain.filter(exchange) work and how do you add pre- and post-processing in a GlobalFilter?
answer
- chain.filter(exchange) = delegate to next, returns Mono
- Pre = before the call; post = .then()/.doFinally() onto it
- DefaultGatewayFilterChain = list + index recursion
- No delegate => short-circuit (no forward)
- Immutable request => exchange.mutate()
basics
~20 sEach filter does its pre-work, then calls chain.filter(exchange) to pass control to the next filter, returning a Mono. To do post-work, chain .then(...) or .doFinally(...) onto that Mono so it runs after downstream filters complete.
solid answer
~40 s`GatewayFilterChain` has one method, `Mono<Void> filter(ServerWebExchange exchange)`. The default implementation (`DefaultGatewayFilterChain`) holds the sorted filter list and an index; calling `chain.filter(exchange)` advances the index and invokes the next filter, or completes when the list is exhausted. A GlobalFilter does pre-processing (mutate the request/exchange), then returns `chain.filter(exchange)` to delegate. Everything before that call is the "pre" phase; to run "post" logic you compose onto the returned Mono — `chain.filter(exchange).then(Mono.fromRunnable(...))` or `.doFinally(...)`. Because it's reactive, post-work runs when the downstream Mono completes, i.e. after the response comes back. To mutate the request you build a new one via `exchange.mutate().request(...).build()` and pass that into the chain, since ServerHttpRequest is immutable.
code
java · 26 linesimport org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class ApiKeyFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String key = exchange.getRequest().getHeaders().getFirst("X-Api-Key");
if (key == null) { // short-circuit: never delegate
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// pre: mutate request
ServerHttpRequest req = exchange.getRequest().mutate()
.header("X-Caller", "verified").build();
// delegate + post
return chain.filter(exchange.mutate().request(req).build())
.then(Mono.fromRunnable(() ->
exchange.getResponse().getHeaders().add("X-Gateway", "ok")));
}
}go deeper
Know chain.filter delegates and returns a Mono you can chain onto.
Explain pre vs post via .then(), and short-circuit by not delegating.
Discuss DefaultGatewayFilterChain index recursion, exchange.mutate immutability, and response-commit gotchas.
Reason about non-blocking discipline, response decoration for body rewrite, and error-path post-work with doFinally.
**The chain abstraction:** `GatewayFilterChain` is a functional interface: ```java public interface GatewayFilterChain { Mono<Void> filter(ServerWebExchange exchange); } ``` The concrete `DefaultGatewayFilterChain` stores an immutable `List<GatewayFilter>` and an `int index`. Each call to `filter(exchange)` constructs a chain positioned at `index + 1` and invokes `filters.get(index).filter(exchange, nextChain)`. When `index == filters.size()`, it returns `Mono.empty()` — the terminal completion. This is the classic recursive/decorator chain pattern expressed reactively. **Pre vs post in a reactive world:** there is no separate `preHandle`/`postHandle` like a servlet `HandlerInterceptor`. Instead: - **Pre-processing** = code you run *before* `return chain.filter(exchange)`. - **Post-processing** = operators you compose *onto* the returned Mono, which execute when the downstream pipeline signals completion: ```java return chain.filter(exchange) .then(Mono.fromRunnable(() -> { ServerHttpResponse resp = exchange.getResponse(); // runs after downstream produced the response })); ``` `then(...)` runs on successful completion; use `doFinally(signal -> ...)` if you must run on error/cancel too. **Delegation is mandatory to continue:** if a filter never calls `chain.filter(exchange)`, the chain stops there — the request is *not* forwarded. That's how you short-circuit (e.g. return 401 by writing to `exchange.getResponse()` and returning `response.setComplete()` instead of delegating). **Mutating the request/exchange:** `ServerHttpRequest` and `ServerWebExchange` are immutable; use the builder: ```java ServerHttpRequest mutated = exchange.getRequest().mutate() .header("X-Trace-Id", id).build(); return chain.filter(exchange.mutate().request(mutated).build()); ``` Pass the mutated exchange downstream so the routing filter sees your changes. **Passing data between filters:** use `exchange.getAttributes()` (a `Map<String,Object>`). Built-in filters communicate this way, e.g. `RouteToRequestUrlFilter` stores the target under `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR` for the routing filter to read. **Gotchas:** - Post-logic that tries to change response *headers* may fail if the response is already committed (the body has started streaming). Use a `ServerHttpResponseDecorator` earlier in the chain, or the `modifyResponseBody` filter, for body/header rewriting. - Forgetting to return the `Mono` (e.g. calling `chain.filter(exchange)` and returning `Mono.empty()`) breaks the pipeline — always return the composed Mono. - Blocking inside a filter starves the event loop; keep it non-blocking or offload with `publishOn`/`subscribeOn` on a bounded scheduler.
- How do you short-circuit the chain to reject a request?Don't call chain.filter(exchange). Instead write to exchange.getResponse() (set status, headers, optionally body) and return exchange.getResponse().setComplete() (or the write Mono). The request is never forwarded downstream.
- Why might your post-processing fail to set a response header?Because the response may already be committed — once NettyWriteResponseFilter starts writing the proxied response, headers are flushed and can't change. Wrap the response with a ServerHttpResponseDecorator earlier, or use modifyResponseBody, to intercept before commit.
- What's the difference between .then() and .doFinally() for post-work?.then() runs only on successful completion. .doFinally(signal -> ...) runs on any terminal signal (complete, error, cancel), so use it for cleanup/metrics that must always fire.
saying these in an interview costs you the question
- Saying filters have separate preHandle/postHandle methods like HandlerInterceptor
- Mutating ServerHttpRequest directly instead of via mutate()
- Returning Mono.empty() after calling chain.filter instead of returning the composed Mono
- Believing you can always set response headers in post-processing regardless of commit state