Gateway Filters

Built-in Filters

Filters change the request before it goes to the service, or the response before it goes back. default-filters apply to every route:

spring:
  cloud:
    gateway:
      default-filters:
        - AddResponseHeader=X-Powered-By, ShopEase-Gateway
        - RemoveResponseHeader=X-Internal-Debug
        - DedupeResponseHeader=Access-Control-Allow-Origin
      routes:
        - id: catalog-public
          uri: lb://product-service
          predicates:
            - Path=/catalog/**
          filters:
            # /catalog/7  ->  /api/products/7
            - RewritePath=/catalog/(?<segment>.*), /api/products/$\{segment}
            - AddRequestHeader=X-Source, public-catalog

        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - name: Retry
              args:
                retries: 2
                methods: GET           # never blindly retry POST /orders!
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
FilterDoes
AddRequestHeader / AddResponseHeaderAdd a header
RewritePathChange the path with a regex
StripPrefix=1Drop the first path segment: /shop/api/x → /api/x
RetryRetry failed calls (safe methods only)
CircuitBreakerResilience4j circuit breaker with fallbackUri
RequestRateLimiterRate limit with Redis (next topic)
TokenRelayPass the OAuth2 token to the service (advance level)

A Custom Global Filter

A GlobalFilter runs for every route. This one makes sure each request has an X-Correlation-Id (so you can follow one order across all service logs) and logs how long the call took:

@Component
@Slf4j
class CorrelationIdFilter implements GlobalFilter, Ordered {

    static final String HEADER = "X-Correlation-Id";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String incoming = exchange.getRequest().getHeaders().getFirst(HEADER);
        String id = (incoming != null) ? incoming : UUID.randomUUID().toString();

        ServerWebExchange withId = exchange.mutate()
                .request(r -> r.header(HEADER, id))
                .build();
        withId.getResponse().getHeaders().add(HEADER, id);

        long start = System.nanoTime();
        return chain.filter(withId).then(Mono.fromRunnable(() -> {
            long ms = (System.nanoTime() - start) / 1_000_000;
            log.info("{} {} -> {} in {} ms [{}]",
                    exchange.getRequest().getMethod(),
                    exchange.getRequest().getPath(),
                    withId.getResponse().getStatusCode(), ms, id);
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;   // run first
    }
}

Example

curl -i localhost:8080/api/products/7

Example output

HTTP/1.1 200 OK
X-Correlation-Id: 3f2b9c1e-6a4d-4c8e-9a51-0b7e2d4f8c13
X-Powered-By: ShopEase-Gateway
{"id":7,"name":"boAt Rockerz 450","price":1499.00,"stock":120,"category":"Audio"}

# gateway log
INFO  c.s.g.CorrelationIdFilter : GET /api/products/7 -> 200 OK in 23 ms [3f2b9c1e-6a4d-4c8e-9a51-0b7e2d4f8c13]

How a Request Flows Through Filters

request -> [CorrelationIdFilter] -> [Security] -> [RateLimiter] -> [RewritePath] -> service
                                                                                    |
 response <- [CorrelationIdFilter] <- ...........................................<- +
            (code inside .then(...) runs on the way back)

Tip: Code written before chain.filter(exchange) is the "pre" part (request side); code in .then(...) is the "post" part (response side). Never call blocking code (JDBC, Thread.sleep) inside a gateway filter.