How Spring Cloud Gateway Routes Requests: Inside the Core Filters
An in‑depth walkthrough of Spring Boot 2.7.10 with Spring Cloud Gateway 3.1.6 shows how the RouteToRequestUrlFilter, ReactiveLoadBalancerClientFilter, NettyRoutingFilter, and NettyWriteResponseFilter sequentially transform incoming URLs, resolve service instances via load balancing, and forward requests to target microservices.
1. RouteToRequestUrlFilter
Based on the route configuration URL, this filter builds the target address. Example configuration shows global timeout, discovery, and default filters such as StripPrefix=1 . The filter rewrites the request URL (e.g., http://localhost:8088/api-1/demos ) to the backend address ( http://localhost:8787/demos ) and stores it in the exchange context.
<code>spring:
cloud:
gateway:
enabled: true
httpclient:
connect-timeout: 10000
response-timeout: 5000
discovery:
locator:
enabled: true
lowerCaseServiceId: true
default-filters:
- StripPrefix=1
routes:
- id: R001
uri: http://localhost:8787
predicates:
- Path=/api-1/**,/api-2/**
metadata:
akf: "dbc"
connect-timeout: 10000
response-timeout: 5000
- id: st001
uri: lb://storage-service
predicates:
- Path=/api-x/**
- id: o001
uri: lb://order-service
predicates:
- Path=/api-a/**, /api-b/**
metadata:
akf: "dbc"
connect-timeout: 10000
response-timeout: 5000
</code>The filter finally puts the transformed URL into the exchange attributes with ServerWebExchange#getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl) . It runs after the global StripPrefixGatewayFilterFactory filter.
2. ReactiveLoadBalancerClientFilter
If the URL uses the lb:// scheme, this filter resolves the service name (e.g., order-service ) to an actual host and port via Spring Cloud ReactorLoadBalancer, then replaces the URI in the request.
<code>public class ReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {
private final LoadBalancerClientFactory clientFactory;
private final GatewayLoadBalancerProperties properties;
private final LoadBalancerProperties loadBalancerProperties;
...
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}
addOriginalRequestUrl(exchange, url);
URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String serviceId = requestUri.getHost();
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
RequestDataContext.class, ResponseData.class, ServiceInstance.class);
DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(new RequestDataContext(
new RequestData(exchange.getRequest()), getHint(serviceId, loadBalancerProperties.getHint())));
return choose(lbRequest, serviceId, supportedLifecycleProcessors)
.doOnNext(response -> {
if (!response.hasServer()) {
supportedLifecycleProcessors.forEach(lifecycle ->
lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, response)));
throw NotFoundException.create(properties.isUse404(),
"Unable to find instance for " + url.getHost());
}
ServiceInstance retrievedInstance = response.getServer();
String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance, overrideScheme);
URI requestUrl = reconstructURI(serviceInstance, uri);
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, response));
})
.then(chain.filter(exchange))
.doOnError(...).doOnSuccess(...);
}
...
}
</code>The filter obtains the load‑balancer client, selects a service instance (default round‑robin), reconstructs the URI, and updates the exchange attributes.
3. NettyRoutingFilter
This global filter retrieves the resolved target URL from the context, creates an HttpClient , forwards the request to the downstream service, and stores the resulting Connection and response attributes for later processing.
<code>public class NettyRoutingFilter implements GlobalFilter {
private final HttpClient httpClient;
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
Flux<HttpClientResponse> responseFlux = getHttpClient(route, exchange)
.headers(headers -> { /* ... */ })
.request(method).uri(url)
.send((req, nettyOutbound) -> nettyOutbound.send(request.getBody().map(this::getByteBuf)))
.responseConnection((res, connection) -> {
exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection);
// copy response headers, status, etc.
return Mono.just(res);
});
Duration responseTimeout = getResponseTimeout(route);
if (responseTimeout != null) {
responseFlux = responseFlux.timeout(responseTimeout,
Mono.error(new TimeoutException("Response took longer than timeout: " + responseTimeout)))
.onErrorMap(TimeoutException.class,
th -> new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, th.getMessage(), th));
}
return responseFlux.then(chain.filter(exchange));
}
...
}
</code>4. NettyWriteResponseFilter
This filter reads the Connection saved by the previous filter, extracts the response body, and writes it back to the client, handling both streaming and non‑streaming media types.
<code>public class NettyWriteResponseFilter implements GlobalFilter, Ordered {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.doOnError(throwable -> cleanup(exchange))
.then(Mono.defer(() -> {
Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);
if (connection == null) {
return Mono.empty();
}
ServerHttpResponse response = exchange.getResponse();
Flux<DataBuffer> body = connection.inbound()
.receive()
.retain()
.map(byteBuf -> wrap(byteBuf, response));
MediaType contentType = null;
try { contentType = response.getHeaders().getContentType(); } catch (Exception ignored) {}
return isStreamingMediaType(contentType)
? response.writeAndFlushWith(body.map(Flux::just))
: response.writeWith(body);
})).doOnCancel(() -> cleanup(exchange));
}
...
}
</code>The overall flow demonstrates how Spring Cloud Gateway processes a request: URL rewriting, service discovery via load balancer, Netty‑based routing, and response writing.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.