Explain the pre and post phases of a GatewayFilter and how you mutate the ServerWebExchange request versus response in each.
answer
- Filter = (exchange, chain) -> Mono<Void>
- Pre = before chain.filter; mutate via exchange.mutate()
- Post = chain.filter(...).then(Mono.fromRunnable)
- Post runs in REVERSE order
- Committed response = headers/status frozen
basics
~20 sA GatewayFilter can run logic before forwarding (pre) and after the response comes back (post). In the pre phase you mutate the request via exchange.mutate(); in the post phase (inside .then(...)) you touch the response after chain.filter completes.
solid answer
~40 sA GatewayFilter is `(exchange, chain) -> Mono<Void>`. Everything you do before calling `chain.filter(exchange)` is the pre phase — this is where you inspect or mutate the outbound request. Because ServerWebExchange and its request/response are effectively immutable, you rebuild them: `exchange.mutate().request(exchange.getRequest().mutate().header(...).build()).build()` and pass the new exchange to the chain. The post phase runs after the downstream responds: you attach it with `chain.filter(exchange).then(Mono.fromRunnable(() -> ...))`, and there you can read status or add response headers. Post-phase filters execute in reverse order of the chain. The critical gotcha: once the response is committed (headers flushed to the client), you can no longer modify response headers or status — so post-phase header changes must happen before commit, and mutating the response body reactively requires decorating the response, not just running a Runnable.
code
java · 21 linespublic GatewayFilter apply() {
return (exchange, chain) -> {
// PRE: mutate the outbound request (immutable -> rebuild)
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-Trace-Id", UUID.randomUUID().toString())
.build();
ServerWebExchange mutatedExchange =
exchange.mutate().request(mutated).build();
long start = System.nanoTime();
// POST: runs after downstream responds, before commit for headers
return chain.filter(mutatedExchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
if (!response.isCommitted()) {
long ms = (System.nanoTime() - start) / 1_000_000;
response.getHeaders().add("X-Elapsed-Ms", Long.toString(ms));
}
}));
};
}go deeper
Knows there is a before and after phase.
Can write pre-phase request mutation and a basic .then post block.
Explains immutability/mutate(), reverse post ordering, and the commit gotcha.
Discusses response decorators for body rewriting, ordering with OrderedGatewayFilter, and reactive body-consumption constraints.
Spring Cloud Gateway filters run on the **reactive** WebFlux stack. A `GatewayFilter` is functionally: ```java Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain); ``` `ServerWebExchange` holds the `ServerHttpRequest` and `ServerHttpResponse` for the proxied call. **Pre phase.** Any code before `chain.filter(...)` runs *before* the request is sent downstream. This is where you mutate the request. Both `ServerWebExchange` and `ServerHttpRequest` are immutable, so you use the builder pattern: ```java ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() .header("X-Trace-Id", traceId) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); ``` `exchange.mutate()` produces a new exchange wrapping the new request; you must pass *that* new exchange to the chain, otherwise the mutation is lost. (Built-in AddRequestHeader/SetRequestHeader do exactly this internally.) **Post phase.** Logic that runs *after* the downstream response returns is scheduled by chaining onto the Mono the chain returns: ```java return chain.filter(exchange).then(Mono.fromRunnable(() -> { ServerHttpResponse response = exchange.getResponse(); response.getHeaders().add("X-Downstream-Time", Instant.now().toString()); })); ``` `chain.filter(exchange)` returns a `Mono<Void>` that completes when the response has been handled; `.then(...)` runs your post logic on completion. This is how SetStatus-style response manipulation and response-header additions are done. **Ordering.** Filters form a chain. Pre logic runs in ascending order; post logic (the `.then` blocks) runs as the Monos unwind, i.e. in **reverse** order — like nested try/finally. `OrderedGatewayFilter` and the `Ordered` interface let you set explicit order; NettyWriteResponseFilter and routing filters sit at fixed positions in that ordering. **The response-commit gotcha.** The single most important edge case: once the response is **committed** — status and headers flushed to the client, which happens as the body starts streaming back — you can no longer change status or headers. So `SetStatus` and response-header filters must act before commit. If you need to run something exactly at commit time, register a callback with `response.beforeCommit(...)`. Adding a header in a naive `.then(Runnable)` can be too late for streamed responses. **Mutating the response body.** You cannot just append to the body in a Runnable. To transform the response body you must wrap the response in a `ServerHttpResponseDecorator`, override `writeWith(...)`, and set the decorated response on the exchange in the pre phase. This is significantly more involved and is why body-rewriting filters (like ModifyResponseBody) are special. **Request body.** Similarly, reading/modifying the request body in the pre phase is non-trivial because the body is a `Flux<DataBuffer>` that can be consumed only once; caching filters (like the modify-request-body filter or `AdaptCachedBodyGlobalFilter`) exist for that reason. **When to use custom pre/post.** Pre: inject auth/correlation headers, rewrite request metadata. Post: record metrics/latency, add response headers, translate status. For anything touching the body, prefer built-in body filters or decorators rather than hand-rolling.
- Why can't you always add a response header in the post phase?Because for streamed responses the response may already be committed (status+headers flushed) by the time the post Runnable runs; committed responses reject header/status changes. Use response.isCommitted()/beforeCommit or act before commit.
- In what order do post-phase blocks execute relative to pre-phase order?Reverse. Pre logic runs in ascending filter order; the .then() post blocks unwind in descending order, like nested finally blocks.
- How do you modify the response body, not just headers?Wrap the response in a ServerHttpResponseDecorator overriding writeWith(...), set it on the exchange in the pre phase; a plain Runnable in .then() cannot transform the streamed body.
saying these in an interview costs you the question
- Mutating exchange.getRequest() in place and expecting it to stick without passing the new exchange to the chain
- Thinking you can always change status/headers in the post phase regardless of commit state
- Believing you can append to the response body inside a .then(Runnable)
- Assuming post filters run in the same order as pre filters