skip to content

Explain GatewayFilterChain delegation: how does chain.filter(exchange) work and how do you add pre- and post-processing in a GlobalFilter?

level: seniorimportance: must knowfreq 60%

answer

  1. chain.filter(exchange) = delegate to next, returns Mono
  2. Pre = before the call; post = .then()/.doFinally() onto it
  3. DefaultGatewayFilterChain = list + index recursion
  4. No delegate => short-circuit (no forward)
  5. Immutable request => exchange.mutate()

basics

~20 s

Each 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 lines
java
import 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

for a junior

Know chain.filter delegates and returns a Mono you can chain onto.

for a middle

Explain pre vs post via .then(), and short-circuit by not delegating.

for a senior

Discuss DefaultGatewayFilterChain index recursion, exchange.mutate immutability, and response-commit gotchas.

for a principal

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

context