How does RestClient-based proxying work in Gateway Server MVC, and how is the proxy client configured?
answer
- http() = proxy handler backed by blocking RestClient
- copy method/path/headers/body, call upstream synchronously
- stream status+headers+body back as ServerResponse
- tune via RestClient.Builder / ClientHttpRequestFactory timeouts
- blocking -> needs timeouts, circuit breakers, virtual threads
basics
~20 sThe terminal http() handler forwards the incoming request to the upstream URI using a blocking RestClient: it copies method, path, headers and body, calls the backend synchronously, and streams the response back as a ServerResponse.
solid answer
~50 sThe terminal handler from `HandlerFunctions.http()` is a proxy-exchange HandlerFunction backed by a blocking `RestClient`. On each request it reads the target URI (either the one you passed to `http(uri)` or one placed in a request attribute, e.g. by the load-balancer filter), reconstructs the outgoing call — same HTTP method, computed path, filtered headers, and the request body — invokes the upstream synchronously on the servlet thread, then writes the upstream status, headers, and body back into a `ServerResponse` streamed to the client. The `RestClient` (and its underlying `ClientHttpRequestFactory`) is autoconfigured by the gateway; you tune connect/read timeouts, connection pooling, SSL, and redirect handling by customizing the `RestClient.Builder`/request factory the gateway uses. Because it is blocking, each proxied call holds a thread for the upstream's full duration, which is why timeouts, circuit breakers, and virtual threads matter.
code
java · 19 linesimport org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClientCustomizer;
import java.time.Duration;
@org.springframework.context.annotation.Configuration
class ProxyClientConfig {
// Customize the RestClient the gateway uses to proxy upstream calls.
@Bean
RestClientCustomizer gatewayRestClientTimeouts() {
var settings = ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofSeconds(2))
.withReadTimeout(Duration.ofSeconds(5));
var factory = ClientHttpRequestFactories.get(settings);
return builder -> builder.requestFactory(factory);
}
}go deeper
Knows http() forwards the request to the backend.
Describes copying method/headers/body and streaming the response back via RestClient.
Configures timeouts/pooling on the proxy client and explains the blocking thread cost.
Designs resiliency (timeouts, circuit breakers, virtual threads) around the blocking proxy and reasons about header normalization/streaming edge cases.
**What `http()` actually is.** `org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http(...)` returns a proxying `HandlerFunction<ServerResponse>` (internally a proxy-exchange handler). This is the terminal of the route: predicates select the route, filters transform it, and this handler performs the actual upstream call and produces the response. **RestClient, not WebClient.** The MVC gateway proxies using Spring's **blocking** `org.springframework.web.client.RestClient` (introduced in Spring Framework 6.1), the synchronous analog of the reactive `WebClient`. Autoconfiguration (in the gateway server webmvc module) supplies a `RestClient` built for proxying, backed by a `ClientHttpRequestFactory` (e.g. JDK `HttpClient`, Apache HttpComponents, or Reactor Netty's client factory depending on what's on the classpath). **The exchange, step by step:** 1. **Resolve target URI** — from `http(uri)`, or a no-arg `http()` that reads a request attribute set upstream (e.g. `LoadBalancerFilterFunctions.lb()` resolving `lb://service-id`). 2. **Build outgoing request** — copy the HTTP method; compute the upstream path (after `stripPrefix`/`rewritePath`/`setPath` before-filters); copy request headers subject to filtering (hop-by-hop headers like `Connection`, `Transfer-Encoding`, and `Content-Length` are handled/normalized; `Host` can be preserved via `preserveHostHeader`); stream the request body. 3. **Invoke upstream** — a **synchronous** RestClient call on the current servlet thread; the thread blocks until the response (or timeout) arrives. 4. **Write response** — copy status code and response headers, stream the body back to the client as a `ServerResponse`. After-filters then post-process. **Configuring the client.** You influence behavior by customizing the proxy `RestClient`/request factory: **connect and read timeouts** (via `ClientHttpRequestFactorySettings`/the request factory), **connection pooling and keep-alive** (Apache HttpClient pool), **TLS/trust material**, **redirect following**, **compression**, and **buffering**. Provide a `RestClientCustomizer` / `RestClient.Builder` bean or a custom `ClientHttpRequestFactory` that the gateway autoconfiguration uses. Some behaviors are also exposed as config properties (e.g. HTTP client connect/response timeouts under the gateway's http-client properties). **Threading consequences.** Because the call is blocking, a slow upstream ties up a platform thread for the whole exchange. Under load this can exhaust the servlet thread pool. Mitigations: strict **read timeouts**, **circuit breakers** (`CircuitBreakerFilterFunctions`), **bulkheads/rate limiting**, and running on **virtual threads** (`spring.threads.virtual.enabled=true`, Java 21+) so blocked threads are cheap. This is the fundamental tradeoff versus the reactive gateway's non-blocking WebClient. **Streaming & large bodies.** The proxy streams bodies rather than fully buffering by default, which matters for large uploads/downloads and SSE; misconfigured buffering or an interfering filter that reads the body can break streaming or double-count `Content-Length`. **Gotchas:** - Hop-by-hop header leakage or duplicated `Content-Length`/`Transfer-Encoding` if you hand-copy headers instead of letting the handler normalize them (there is even a `TransferEncodingNormalization` filter). - Forgetting a read timeout means one hung upstream can slowly consume the whole pool. - Expecting reactive backpressure — there is none; it's blocking IO. - Assuming `http()` load-balances by itself — you must add the `lb()` filter and use `lb://` targets.
- Why does the MVC gateway use RestClient instead of WebClient?Because the whole stack is servlet/blocking. RestClient is the synchronous HTTP client that fits thread-per-request; WebClient is reactive and belongs to the WebFlux gateway. Using WebClient here would reintroduce reactive types the servlet model isn't built around.
- One upstream becomes very slow. What's the failure mode and how do you contain it?Each blocked proxy call holds a servlet thread; a slow upstream can exhaust the pool and stall unrelated routes. Contain it with aggressive read timeouts, per-route circuit breakers and bulkhead/rate limits, and by running on virtual threads so blocked threads are cheap.
saying these in an interview costs you the question
- Saying the MVC gateway proxies with WebClient
- Assuming http() load-balances without the lb() filter
- Believing there is reactive backpressure/non-blocking IO
- Ignoring read timeouts so a hung upstream drains the pool