You need cross-cutting timing metrics plus a filter that runs immediately before the downstream call. How do you design the ordering, and what constraints do the terminal routing filters impose?
answer
- Outermost (lowest order) = total latency via post/unwind
- Just-before-call = order near LOWEST_PRECEDENCE - 1
- Terminal routing filter => no pre-logic after it
- Response commit at NettyWriteResponseFilter(-1) => decorate before routing
- Space orders, avoid ties, stay non-blocking
basics
~20 sPut the timing filter at a very low order so it wraps everything, doing pre-work first and post-work last via .then(). Any "just before the call" work must also go pre-side at a high order (near, but before, LOWEST_PRECEDENCE), because the routing filter is terminal and won't run anything after it on the pre path.
solid answer
~40 sModel the chain as nested wrappers. For end-to-end timing, give the metrics GlobalFilter HIGHEST_PRECEDENCE (or a very negative order) so it's outermost: capture start in pre, record duration in `.then(...)`/`.doFinally(...)` which unwinds last, after the response returns. For "immediately before the proxied call," place a filter at a high (but still less than LOWEST_PRECEDENCE) order so its pre-logic runs just before NettyRoutingFilter; you cannot run pre-logic *after* routing because NettyRoutingFilter is terminal and never delegates. Read/mutate the request via `exchange.mutate()` and hand off through `GATEWAY_REQUEST_URL_ATTR` if needed. If you need to observe the response (headers/body), remember NettyWriteResponseFilter (order -1) commits it; decorate the response before routing rather than mutating after. Avoid order ties, keep everything non-blocking, and don't rely on ordering after the terminal filter.
code
java · 28 linesimport org.springframework.core.Ordered;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
// Outermost: measures TOTAL latency incl. downstream call.
@Component
class MetricsFilter implements GlobalFilter, Ordered {
public Mono<Void> filter(ServerWebExchange ex, GatewayFilterChain chain) {
long start = System.nanoTime();
return chain.filter(ex)
.doFinally(sig -> record(ex, System.nanoTime() - start, sig.toString()));
}
public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
private void record(ServerWebExchange ex, long nanos, String signal) { /* emit */ }
}
// Runs immediately before NettyRoutingFilter performs the proxied call.
@Component
class JustBeforeCallFilter implements GlobalFilter, Ordered {
public Mono<Void> filter(ServerWebExchange ex, GatewayFilterChain chain) {
// last chance to touch the request before it leaves the gateway
return chain.filter(ex);
}
public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 1; }
}go deeper
Grasp that timing wraps everything and routing runs last.
Place metrics at highest precedence and know post runs on unwind.
Handle response-commit timing, doFinally vs then, and lb resolution ordering.
Architect a spaced, tie-free ordering that respects terminal routing filters, non-blocking constraints, and response-decoration timing across the whole filter suite.
**Mental model:** the sorted filter list executes as nested decorators. Lowest order = outermost = pre-first / post-last. The terminal routing filter (`NettyRoutingFilter`/`ForwardRoutingFilter`, `Ordered.LOWEST_PRECEDENCE`) is the innermost meaningful step and does **not** call `chain.filter`, so the pre path effectively ends there. **Requirement 1 — end-to-end timing (cross-cutting):** - Order: `Ordered.HIGHEST_PRECEDENCE` (or a small negative like -1000) so it wraps the entire chain including routing. - Pre: record `start`. - Post: use `.then(Mono.fromRunnable(...))` for success, or `.doFinally(signal -> ...)` to also capture errors/cancellations. Because this filter is outermost, its post unwinds *last*, giving true total latency including the downstream call. **Requirement 2 — run immediately before the downstream call:** - You want pre-logic as close to routing as possible. Give it an order just below `LOWEST_PRECEDENCE` (e.g. `LOWEST_PRECEDENCE - 1`, or a value above the load-balancer filter's ~10150 but below Integer.MAX_VALUE). It will run after URL resolution and load balancing but before Netty proxies. - **Constraint:** you can never run pre-logic *after* the terminal routing filter — it doesn't delegate. "After the call" always means post-processing on an earlier filter, executed on the reactive unwind. **Response observation/modification:** - `NettyRoutingFilter` streams the downstream response; `NettyWriteResponseFilter` (order -1) writes it to the client and commits it. Once committed, you can't change status/headers. - To rewrite headers or body, wrap `exchange.getResponse()` with a `ServerHttpResponseDecorator` in a filter ordered *before* routing (so the decorator is in place when the response is produced), or use the built-in `modifyResponseBody` GatewayFilter. **Cross-filter communication:** use `exchange.getAttributes()`. Read the resolved target from `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`; check `ServerWebExchangeUtils.isAlreadyRouted(exchange)` to avoid double routing if you write a custom routing filter. **Operational constraints & pitfalls:** - **Ties:** two filters with equal order have undefined relative order — assign explicit, spaced values (e.g. -1000, -100, 10050) so future insertions fit between. - **Blocking:** every GlobalFilter runs on the Netty event loop; a blocking call degrades all traffic. Offload with a bounded scheduler if unavoidable. - **Terminal assumption:** never design a filter expecting to sit "after" routing on the pre side; it silently never runs. - **Load balancing:** if targeting `lb://`, ensure your just-before-call filter is ordered *after* `ReactiveLoadBalancerClientFilter` (~10150) if it needs the concrete host, but still before the terminal filter. - **Idempotency of post-work:** on retries or cancels, `.then` won't fire on error; prefer `doFinally` for metrics you must always emit, and inspect the `SignalType`. **Summary design:** metrics filter at HIGHEST_PRECEDENCE (outermost, total latency); pre-call filter near LOWEST_PRECEDENCE-1 (last pre before Netty); any response mutation via a decorator installed before routing. This respects the terminal nature of the routing filters and the reactive unwind semantics.
- Why not just place the 'just before the call' logic at LOWEST_PRECEDENCE, same as the routing filter?Ties have undefined order, so it might land after NettyRoutingFilter — and since the routing filter is terminal and never delegates, your pre-logic would never run. Use LOWEST_PRECEDENCE - 1 to guarantee it executes just before routing.
- How do you capture latency that includes downstream errors and client cancellations?Use .doFinally(signal -> ...) instead of .then(...). then only runs on successful completion; doFinally fires on complete, error, and cancel, and gives you the SignalType so you can label the metric accordingly.
- You need to add a header to the downstream response. Where in the ordering does that filter go?Before the routing filters, where it wraps exchange.getResponse() with a ServerHttpResponseDecorator (or uses modifyResponseBody). Mutating headers in plain post-processing often fails because NettyWriteResponseFilter has already committed the response.
saying these in an interview costs you the question
- Designing a pre-side filter to run after the terminal routing filter
- Using .then for metrics that must also count errors/cancels
- Setting response headers in post-processing after commit
- Leaving multiple filters at the same order and relying on insertion order
- Blocking on the event loop inside a global filter