skip to content

Walk through the reactive request pipeline in Spring Cloud Gateway: how does an incoming request flow through ServerWebExchange and the GatewayFilterChain to become a proxied call?

level: middleimportance: must knowfreq 60%

answer

  1. RoutePredicateHandlerMapping picks the Route
  2. ServerWebExchange holds request+response+attributes
  3. FilteringWebHandler builds ordered GatewayFilterChain
  4. filter returns Mono<Void>; pre before chain.filter, post in .then(...)
  5. NettyRoutingFilter proxies via reactor-netty HttpClient

basics

~20 s

A handler mapping matches the request to a route, then a filter chain of global + route filters runs. Each filter can modify the request before calling the next, and modify the response after. A routing filter actually proxies the call; the whole flow returns a Mono.

solid answer

~40 s

`RoutePredicateHandlerMapping` matches the incoming request against each route's predicates and selects a `Route`, storing it on the `ServerWebExchange` (the reactive holder for request + response + attributes). `FilteringWebHandler` then builds a `GatewayFilterChain` combining the matched route's `GatewayFilter`s with all `GlobalFilter`s, ordered by `@Order`/`Ordered`. Each filter has the signature `filter(exchange, chain)` returning `Mono<Void>`: 'pre' logic runs before `chain.filter(exchange)`, and 'post' logic is attached with `.then(Mono.fromRunnable(...))` so it runs after the downstream response is available. A global routing filter — typically `NettyRoutingFilter` — performs the actual non-blocking proxy call via reactor-netty's `HttpClient` and writes the response back. Because every stage returns a `Mono`, the entire pipeline is a single composed reactive chain with no blocking.

code

java · 29 lines
java
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class TimingGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long start = System.nanoTime();

        // PRE: runs before the request is proxied downstream
        exchange.getRequest().mutate().header("X-Gateway-Trace", "on");

        return chain.filter(exchange)                 // continue the chain -> eventually NettyRoutingFilter
            .then(Mono.fromRunnable(() -> {            // POST: runs after downstream responds
                long tookMs = (System.nanoTime() - start) / 1_000_000;
                exchange.getResponse().getHeaders().add("X-Elapsed-Ms", String.valueOf(tookMs));
            }));
    }

    @Override
    public int getOrder() {
        return -1; // run early relative to routing
    }
}

go deeper

for a junior

Should know there's a filter chain that can change request and response around a proxy call.

for a middle

Should name ServerWebExchange, the ordered GatewayFilterChain, the Mono<Void> contract, and where pre vs post logic goes.

for a senior

Should identify NettyRoutingFilter as the proxy step, explain attribute-based communication, and body-buffering pitfalls.

for a principal

Should reason about ordering interactions (auth/circuit-breaker/routing), custom filter design, and why the single reactive graph avoids blocking.

## The key object: ServerWebExchange `ServerWebExchange` is WebFlux's per-request context. It wraps the `ServerHttpRequest`, the `ServerHttpResponse`, and a mutable attribute map. Spring Cloud Gateway stashes routing state there under well-known keys (e.g. the matched `Route` under `GATEWAY_ROUTE_ATTR`, the resolved target URL under `GATEWAY_REQUEST_URL_ATTR`). Filters communicate by reading/writing exchange attributes. ## Step-by-step flow 1. **Handler mapping / route matching:** `RoutePredicateHandlerMapping` iterates the configured routes and evaluates each route's **predicate** (built by `RoutePredicateFactory` instances — Path, Method, Header, Host, etc.). The first route whose predicate returns true is selected and stored on the exchange. If none match, the request falls through (404 or another handler). 2. **Building the chain:** `FilteringWebHandler` takes the matched route's route-scoped `GatewayFilter`s plus all registered `GlobalFilter`s (global filters are adapted to the gateway filter interface), and sorts them by order (`Ordered`/`@Order`). It composes them into a `GatewayFilterChain`. 3. **Running filters (pre phase):** each filter implements `Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)`. Code placed **before** `chain.filter(exchange)` is the 'pre' phase — e.g. add a header, rewrite the path, check a token. The filter must return the `Mono` from `chain.filter(exchange)` so the chain continues. 4. **Routing filter (the proxy):** near the end of the chain, a routing **GlobalFilter** does the real work. For the Netty stack this is `NettyRoutingFilter`, which uses reactor-netty's non-blocking `HttpClient` to call the downstream URI resolved on the exchange, streaming the request body out and the response body back. (Other routing filters exist, e.g. for WebSocket.) 5. **Post phase:** logic that must run **after** the downstream responds is attached reactively, typically `return chain.filter(exchange).then(Mono.fromRunnable(() -> { /* inspect/modify response */ }));`. `NettyWriteResponseFilter` writes the buffered/streamed downstream response back to the client. ## Filter kinds — vocabulary - **GatewayFilter:** applies to a specific route; created by a `GatewayFilterFactory` (e.g. `AddRequestHeader`, `RewritePath`, `CircuitBreaker`). - **GlobalFilter:** applies to *every* route (e.g. `NettyRoutingFilter`, `ForwardRoutingFilter`, `LoadBalancerClientFilter`). - **Ordering:** both participate in one ordered chain; order matters (auth before routing, response-modifying filters wrap the routing filter via `.then(...)`). ## Why the Mono<Void> contract Returning `Mono<Void>` means a filter signals *completion* asynchronously. The framework subscribes once at the top; the whole request is one reactive graph. 'Pre' = operators before `chain.filter`; 'post' = operators chained after it. Nothing blocks; the thread is free between events. ## Gotchas - **Forgetting to return `chain.filter(exchange)`** breaks the chain — the request hangs or the downstream is never called. - **Reading the request body** (e.g. `ModifyRequestBody`) forces buffering; the body can normally be read once, so gateway caches it on the exchange for later filters. - **Doing post-processing synchronously before `chain.filter`** runs it in the pre phase by mistake — post logic must be inside `.then(...)`/`.doOnEach(...)` after the chain. - **Blocking inside a filter** stalls the event loop (see the threading question). ## When you write a custom filter Implement `GlobalFilter` + `Ordered`, or a `GatewayFilterFactory` for a per-route, configurable filter. Always compose reactively — never call `.block()`.

  • Where does the actual HTTP call to the downstream service happen in the chain?
    In a routing GlobalFilter placed late in the chain — typically `NettyRoutingFilter`, which uses reactor-netty's non-blocking `HttpClient`. It reads the target URI from the exchange attributes and streams request/response bodies. `NettyWriteResponseFilter` then writes the response back to the client.
  • How do you implement 'post' logic that inspects the downstream response?
    Attach it after `chain.filter(exchange)` with a reactive operator like `.then(Mono.fromRunnable(...))` or `.doOnEach(...)`. Since the response is only available after the chain completes, post logic must live inside that continuation, not before the `chain.filter` call.

saying these in an interview costs you the question

  • Thinking pre and post phases are separate methods rather than code before/after chain.filter(exchange)
  • Saying filters return the response object directly instead of Mono<Void>
  • Believing GlobalFilter and GatewayFilter run in separate chains
  • Claiming the request body can be freely re-read without buffering

context