Backend Development 13 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How Spring Cloud Gateway Routes Requests: Inside the Core Filters

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.

microservicesbackend developmentReactive ProgrammingLoad BalancerSpring Cloud GatewayFilters
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.