What do NettyRoutingFilter and ForwardRoutingFilter do, and how does the gateway decide which one performs the proxied call?
answer
- Both terminal, LOWEST_PRECEDENCE
- http/https => NettyRoutingFilter (reactor-netty HttpClient)
- forward: => ForwardRoutingFilter (local DispatcherHandler)
- lb:// resolved earlier by ReactiveLoadBalancerClientFilter
- Scheme + GATEWAY_ALREADY_ROUTED_ATTR pick the one
basics
~20 sThey are the terminal GlobalFilters that actually make the call. NettyRoutingFilter proxies http/https URIs using a Netty HTTP client. ForwardRoutingFilter handles forward: URIs by dispatching locally in the same gateway app. The URI scheme decides which runs.
solid answer
~40 sBoth are built-in GlobalFilters at LOWEST_PRECEDENCE, so they run last after predicates, URL resolution, and load-balancing have set the request URL. Each inspects `GATEWAY_REQUEST_URL_ATTR` (the resolved URI). `NettyRoutingFilter` fires when the scheme is `http`/`https`: it uses a reactor-netty `HttpClient` to make the real downstream request, streams the response into the exchange, and marks the exchange routed (`GATEWAY_ALREADY_ROUTED_ATTR`) — it does NOT call `chain.filter`, it's terminal. `ForwardRoutingFilter` fires when the scheme is `forward` (e.g. `forward:/local`): it dispatches to the gateway's own `DispatcherHandler`, handling the request in-process rather than over the network. `ReactiveLoadBalancerClientFilter` runs earlier to turn `lb://service` into a concrete `http://host:port` before Netty routes. Only one routing filter actually executes per request, guarded by scheme and the already-routed flag.
code
yaml · 17 linesspring:
cloud:
gateway:
routes:
# proxied over the network by NettyRoutingFilter
- id: remote
uri: http://users-svc:8080
predicates: [Path=/users/**]
# load-balanced: ReactiveLoadBalancerClientFilter resolves lb://,
# then NettyRoutingFilter proxies to the chosen instance
- id: balanced
uri: lb://ORDERS
predicates: [Path=/orders/**]
# handled in-process by ForwardRoutingFilter
- id: local-fallback
uri: forward:/fallback
predicates: [Path=/down/**]go deeper
Know a built-in filter makes the actual downstream call.
Distinguish http/https (Netty) from forward: (local) and that they run last.
Explain GATEWAY_REQUEST_URL_ATTR, already-routed flag, lb:// resolution, and terminal behavior.
Reason about response decoration timing, timeout config, and why terminal filters constrain chain design.
**Where routing sits:** after all pre-filters run, the request must actually be sent somewhere. Spring Cloud Gateway does this with dedicated **routing GlobalFilters**, all at `Ordered.LOWEST_PRECEDENCE` so they execute after everything else has prepared the exchange. **The resolved URL:** `RouteToRequestUrlFilter` (order 10000) reads the matched Route's `uri` plus the incoming path and writes the target into the exchange attribute `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`. Routing filters read this attribute to know where/how to send. **NettyRoutingFilter:** - Triggers when the request URL scheme is `http` or `https`. - Uses a Project Reactor Netty `HttpClient` to open a connection to the downstream host, replay the method/headers/body, and receive the response reactively. - Copies the downstream status/headers into `exchange.getResponse()` and stashes the client response under `CLIENT_RESPONSE_ATTR` so `NettyWriteResponseFilter` (order -1) streams the body back to the caller. - Sets `GATEWAY_ALREADY_ROUTED_ATTR` = true and is **terminal**: it returns a Mono for the proxied call and never invokes `chain.filter`, because there's nothing after it to route to. - Honors per-route/global options like connect/response timeouts and whether to preserve host header. **ForwardRoutingFilter:** - Triggers when the scheme is `forward`, e.g. a route `uri: forward:/fallback`. - Instead of a network hop, it forwards the request to the gateway application's own `DispatcherHandler`, so a controller/handler *inside the gateway process* serves it. Useful for local fallbacks, default responses, or serving something the gateway itself hosts. - Also terminal. **ReactiveLoadBalancerClientFilter (context):** - Order ~10150. When the route URI is `lb://my-service`, this filter uses Spring Cloud LoadBalancer to pick a live instance and rewrites the URL to a concrete `http://host:port`, updating `GATEWAY_REQUEST_URL_ATTR`. Then `NettyRoutingFilter` proxies to it. (The older `LoadBalancerClientFilter` did the same in the non-reactive/RestTemplate era.) **How selection works:** each routing filter first checks the already-routed flag (`ServerWebExchangeUtils.isAlreadyRouted`) and its expected scheme. Only the one matching the current scheme acts; the others see a non-matching scheme (or already-routed) and simply delegate `chain.filter(exchange)` (a no-op continuation). Because scheme is exclusive (`http` xor `forward` xor …), exactly one performs the call. **Gotchas:** - If you write a GlobalFilter at LOWEST_PRECEDENCE hoping to run *after* the proxied call on the pre side, you'll be disappointed — the routing filter is terminal and does not delegate, so your pre-logic never runs. Put post-work in `.then(...)` on an earlier filter instead. - Forgetting `lb://` (using a bare service name) means no load balancing happens and Netty tries to resolve it as a literal host. - The routing filter streams the body; if you need to read/modify it, you must decorate the response *before* routing, not after. - `forward:` only reaches handlers within the same gateway app; it is not a network redirect.
- How does lb://my-service end up as a real host for NettyRoutingFilter?ReactiveLoadBalancerClientFilter runs before the routing filters, uses Spring Cloud LoadBalancer to choose a live instance of my-service, and rewrites GATEWAY_REQUEST_URL_ATTR to a concrete http://host:port. NettyRoutingFilter then proxies to that resolved URL.
- Why can't you add a GlobalFilter that runs on the pre side after NettyRoutingFilter?NettyRoutingFilter is terminal — it performs the call and never invokes chain.filter, so nothing after it in the chain runs its pre-logic. Anything meant to run after routing must be attached as post-processing (.then/.doFinally) on an earlier filter, which unwinds after the response returns.
saying these in an interview costs you the question
- Thinking ForwardRoutingFilter makes an external HTTP redirect
- Believing NettyRoutingFilter calls chain.filter to continue after routing
- Assuming a bare service name (no lb://) triggers load balancing
- Saying both routing filters run for every request