skip to content

How does RestClient-based proxying work in Gateway Server MVC, and how is the proxy client configured?

level: seniorimportance: should knowfreq 35%

answer

  1. http() = proxy handler backed by blocking RestClient
  2. copy method/path/headers/body, call upstream synchronously
  3. stream status+headers+body back as ServerResponse
  4. tune via RestClient.Builder / ClientHttpRequestFactory timeouts
  5. blocking -> needs timeouts, circuit breakers, virtual threads

basics

~20 s

The 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 s

The 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 lines
java
import 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

for a junior

Knows http() forwards the request to the backend.

for a middle

Describes copying method/headers/body and streaming the response back via RestClient.

for a senior

Configures timeouts/pooling on the proxy client and explains the blocking thread cost.

for a principal

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

context