Mastering API Gateway Routing: A Deep Dive into Spring Cloud Gateway Predicates and Filters

In the world of microservices architecture, API Gateways have become essential components for handling cross-cutting concerns like routing, security, monitoring, and resilience. Spring Cloud Gateway builds upon Project Reactor to provide a powerful, non-blocking way to route requests to appropriate services. At the heart of this capability lie two fundamental concepts: Route Predicates and Filters.

This article explores how predicates and filters work together in Spring Cloud Gateway to create sophisticated routing rules and request transformations.


Spring Cloud Gateway Architecture

Before diving into predicates and filters, let's understand the basic architecture:

HTTP Request → Gateway Handler → Route Predicate Matching → Filter Chain → Backend Service
↑
Configuration:
- Predicates (WHEN)
- Filters (WHAT/WHERE)
  • Routes: The basic building block containing an ID, a destination URI, a collection of predicates, and a collection of filters
  • Predicates: Determine WHEN a route should be matched
  • Filters: Define WHAT transformations to apply to requests and responses

Route Predicates: The "When" of Routing

Predicates are conditions that must be true for a route to be matched. Think of them as the "if" conditions for your routing rules.

Common Predicate Factories:

Spring Cloud Gateway provides numerous built-in predicate factories:

  1. Path Route Predicate: Matches based on request path
  2. Method Route Predicate: Matches based on HTTP method
  3. Header Route Predicate: Matches based on request headers
  4. Host Route Predicate: Matches based on the Host header
  5. Query Route Predicate: Matches based on query parameters
  6. Cookie Route Predicate: Matches based on cookies
  7. RemoteAddr Route Predicate: Matches based on client IP address
  8. DateTime Route Predicate: Matches based on time windows
  9. Weight Route Predicate: For A/B testing and canary deployments

Configuring Predicates

Method 1: Application.yml Configuration

spring:
cloud:
gateway:
routes:
# Route 1: Simple path-based routing
- id: user_service_route
uri: http://localhost:8081
predicates:
- Path=/api/users/**
- Method=GET,POST
filters:
- StripPrefix=1
# Route 2: Header-based routing
- id: premium_service_route
uri: http://localhost:8082
predicates:
- Path=/api/**
- Header=X-User-Type, premium
filters:
- StripPrefix=1
# Route 3: Host-based routing with rewrite
- id: host_based_route
uri: http://localhost:8083
predicates:
- Host=**.example.com
filters:
- RewritePath=/api/v1/(?<segment>.*), /$\{segment}
# Route 4: Query parameter routing
- id: query_param_route
uri: http://localhost:8084
predicates:
- Path=/search
- Query=type,advanced
filters:
- AddRequestParameter=source,gateway
# Route 5: Cookie-based routing
- id: cookie_based_route
uri: http://localhost:8085
predicates:
- Path=/premium/**
- Cookie=premium_user, true
# Route 6: Time-based routing
- id: business_hours_route
uri: http://localhost:8086
predicates:
- Path=/support/**
- Between=2024-01-01T09:00:00.000-05:00, 2024-12-31T17:00:00.000-05:00
# Route 7: Weight-based routing (A/B testing)
- id: weight_high
uri: http://localhost:8087
predicates:
- Path=/feature/**
- Weight=group-a, 80
filters:
- AddRequestHeader=X-Feature-Version, v2
- id: weight_low
uri: http://localhost:8088
predicates:
- Path=/feature/**
- Weight=group-a, 20
filters:
- AddRequestHeader=X-Feature-Version, v1

Method 2: Java DSL Configuration

@Configuration
public class GatewayConfiguration {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// Path and method based routing
.route("user_service", r -> r
.path("/api/users/**")
.and()
.method("GET", "POST")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:8081"))
// Header based routing
.route("premium_service", r -> r
.path("/api/**")
.and()
.header("X-User-Type", "premium")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:8082"))
// Host based routing with rewrite
.route("host_route", r -> r
.host("*.example.com")
.filters(f -> f.rewritePath("/api/v1/(?<segment>.*)", "/${segment}"))
.uri("http://localhost:8083"))
// Query parameter routing
.route("query_route", r -> r
.path("/search")
.and()
.query("type", "advanced")
.filters(f -> f.addRequestParameter("source", "gateway"))
.uri("http://localhost:8084"))
// Complex predicate combination
.route("complex_route", r -> r
.path("/api/**")
.and()
.method("POST", "PUT")
.and()
.header("X-API-Version", "v2")
.and()
.cookie("session_id", ".*")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway-Route", "complex"))
.uri("http://localhost:8089"))
.build();
}
}

Gateway Filters: The "What" of Request Processing

Filters modify requests and responses as they pass through the gateway. They can perform various transformations, add headers, modify bodies, and more.

Filter Categories:

  • Pre Filters: Execute before forwarding the request to the backend
  • Post Filters: Execute after receiving the response from the backend
  • Route Filters: Can affect the request routing itself

Common Gateway Filters

1. Request Modification Filters

spring:
cloud:
gateway:
routes:
- id: request_modification_route
uri: http://localhost:8090
predicates:
- Path=/api/**
filters:
- AddRequestHeader=X-Gateway-Request-ID, 12345
- AddRequestParameter=source, gateway
- AddRequestHeader=X-Forwarded-For, 192.168.1.1
- RemoveRequestHeader=Authorization  # Remove sensitive headers
- SetRequestHeader=Content-Type, application/json
- SetRequestHeader=X-API-Version, v2
- StripPrefix=2  # Remove first 2 path segments

2. Path Rewriting Filters

        - id: path_rewrite_route
uri: http://localhost:8091
predicates:
- Path=/old-api/**
filters:
- RewritePath=/old-api/(?<segment>.*), /new-api/$\{segment}
# Converts /old-api/users to /new-api/users
- id: complex_rewrite_route
uri: http://localhost:8092
predicates:
- Path=/v1/api/**
filters:
- RewritePath=/v1/api/users/(?<id>.*), /api/v2/users/$\{id}/profile
# Converts /v1/api/users/123 to /api/v2/users/123/profile

3. Response Modification Filters

        - id: response_modification_route
uri: http://localhost:8093
predicates:
- Path=/api/public/**
filters:
- AddResponseHeader=X-Processed-By, api-gateway
- SetResponseHeader=Content-Type, application/json
- RemoveResponseHeader=Server  # Hide backend server info
- DedupeResponseHeader=Access-Control-Allow-Origin

4. Circuit Breaker and Retry Filters

        - id: resilient_route
uri: http://localhost:8094
predicates:
- Path=/api/orders/**
filters:
- name: CircuitBreaker
args:
name: orderService
fallbackUri: forward:/fallback/order
- name: Retry
args:
retries: 3
series: SERVER_ERROR
methods: GET,POST
backoff:
firstBackoff: 50ms
maxBackoff: 500ms
factor: 2
basedOnPreviousValue: false

5. Rate Limiting Filter

        - id: rate_limited_route
uri: http://localhost:8095
predicates:
- Path=/api/limited/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
- AddRequestHeader=X-RateLimit-Bypass, false

Custom Filters Implementation

You can also create custom filters for specific business logic.

Custom Global Filter:

@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// Check for API key in header
String apiKey = request.getHeaders().getFirst("X-API-Key");
if (!isValidApiKey(apiKey)) {
// Reject request with 401 Unauthorized
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// Add custom header for downstream services
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-Authenticated-User", "user-from-api-key")
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
}
private boolean isValidApiKey(String apiKey) {
return apiKey != null && apiKey.startsWith("key_");
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}

Custom Gateway Filter Factory:

@Component
public class LoggingGatewayFilterFactory extends 
AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {
public LoggingGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// Pre-processing logging
if (config.isEnabled()) {
System.out.println("Incoming request: " + 
request.getMethod() + " " + 
request.getURI() + 
" - Headers: " + request.getHeaders());
}
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// Post-processing logging
if (config.isEnabled()) {
ServerHttpResponse response = exchange.getResponse();
System.out.println("Outgoing response: " + 
response.getStatusCode() + 
" - Headers: " + response.getHeaders());
}
}));
};
}
public static class Config {
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("enabled");
}
}

Using Custom Filter in Configuration:

spring:
cloud:
gateway:
routes:
- id: custom_filter_route
uri: http://localhost:8096
predicates:
- Path=/api/logged/**
filters:
- Logging=true
- StripPrefix=1

Advanced Routing Scenarios

Scenario 1: Version-Based Routing

spring:
cloud:
gateway:
routes:
- id: api_v1_route
uri: http://localhost:8101
predicates:
- Path=/api/v1/**
filters:
- StripPrefix=2
- id: api_v2_route
uri: http://localhost:8102
predicates:
- Path=/api/v2/**
filters:
- StripPrefix=2
- id: api_latest_route
uri: http://localhost:8103
predicates:
- Path=/api/**
- Header=X-API-Version, latest
filters:
- StripPrefix=1
- SetPath=/api/v3/{segment}

Scenario 2: Tenant-Based Routing

@Bean
public RouteLocator tenantBasedRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("tenant_a_route", r -> r
.header("X-Tenant-ID", "tenant-a")
.filters(f -> f
.addRequestHeader("X-Tenant-Name", "Company A")
.rewritePath("/(?<segment>.*)", "/tenant-a/${segment}"))
.uri("http://tenant-a-service:8080"))
.route("tenant_b_route", r -> r
.header("X-Tenant-ID", "tenant-b")
.filters(f -> f
.addRequestHeader("X-Tenant-Name", "Company B")
.rewritePath("/(?<segment>.*)", "/tenant-b/${segment}"))
.uri("http://tenant-b-service:8080"))
.route("default_tenant_route", r -> r
.alwaysTrue()  // Catch-all predicate
.filters(f -> f
.addRequestHeader("X-Tenant-Name", "Default")
.rewritePath("/(?<segment>.*)", "/default/${segment}"))
.uri("http://default-service:8080"))
.build();
}

Best Practices

  1. Order Matters: Routes are evaluated in order, so put specific routes before generic ones
  2. Use Meaningful IDs: Route IDs should clearly indicate the route's purpose
  3. Monitor Performance: Use Actuator endpoints to monitor gateway performance
  4. Test Thoroughly: Always test routing rules with various scenarios
  5. Security First: Remove sensitive headers and validate inputs
  6. Circuit Breaking: Implement resilience patterns for dependent services
  7. Rate Limiting: Protect backend services from excessive traffic

Conclusion

Spring Cloud Gateway's predicates and filters provide a powerful, flexible mechanism for building sophisticated API gateways. By mastering these components, you can:

  • Implement Complex Routing: Based on path, headers, methods, and other request attributes
  • Transform Requests/Responses: Modify headers, rewrite paths, and manipulate content
  • Enhance Security: Add authentication, remove sensitive information
  • Improve Resilience: Implement circuit breakers, retries, and rate limiting
  • Enable Advanced Patterns: Support for A/B testing, canary releases, and multi-tenancy

The combination of declarative configuration and programmatic customization makes Spring Cloud Gateway an excellent choice for modern microservices architectures, providing the routing intelligence needed to build robust, scalable systems.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper