Writing Custom Predicates and Filters

Spring Cloud Gateway Server MVC uses the Spring WebMvc.fn API (javadoc) as the basis for the API Gateway functionality.

Spring Cloud Gateway Server MVC is extensible using these APIs. Users might commonly expect to write custom implementations of RequestPredicate and HandlerFilterFunction and two variations of HandlerFilterFunction, one for "before" filters and another for "after" filters.

Fundamentals

The most basic interfaces that are a part of the Spring WebMvc.fn API are ServerRequest (javadoc) and ServerResponse (javadoc). These provide access to all parts of the HTTP request and response.

The Spring WebMvc.fn docs declare that "`ServerRequest` and ServerResponse are immutable interfaces. In some cases, Spring Cloud Gateway Server MVC has to provide alternate implementations so that some things can be mutable to satisfy the proxy requirements of an API gateway.

Implementing a RequestPredicate

The Spring WebMvc.fn RouterFunctions.Builder expects a RequestPredicate (javadoc) to match a given Route. RequestPredicate is a functional interface and can therefor be implemented with lambdas. The method signature to implement is:

boolean test(ServerRequest request)

Example RequestPredicate Implementation

For this example, we will show the implementation of a predicate to test that a particular HTTP headers is part of the HTTP request.

The RequestPredicate implementations in Spring WebMvc.fn RequestPredicates and in GatewayRequestPredicates are all implemented as static methods. We will do the same here.

SampleRequestPredicates.java
import org.springframework.web.reactive.function.server.RequestPredicate;

class SampleRequestPredicates {
    public static RequestPredicate headerExists(String header) {
		return request -> request.headers().asHttpHeaders().containsKey(header);
    }
}

The implementation is a simple lambda that transforms the ServerRequest.Headers object to the richer API of HttpHeaders. This allows the predicate to test for the presence of the named header.

How To Use A Custom RequestPredicate

To use our new headerExists RequestPredicate, we need to plug it in to an appropriate method on the RouterFunctions.Builder such as route(). Of course, the lambda in the headerExists method could be written inline in the example below.

RouteConfiguration.java
import static SampleRequestPredicates.headerExists;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;

@Configuration
class RouteConfiguration {

    @Bean
    public RouterFunction<ServerResponse> headerExistsRoute() {
		return route("header_exists_route")
				.route(headerExists("X-Green"), http("https://example.org"))
					.build();
    }
}

The above route will be matched when an HTTP request has a header named X-Green.

Writing Custom HandlerFilterFunction Implementations

The RouterFunctions.Builder has three options to add filters: filter, before, and after. The before and after methods are specializations of the general filter method.

Implementing a HandlerFilterFunction

The filter method takes a HandlerFilterFunction as a parameter. HandlerFilterFunction<T extends ServerResponse, R extends ServerResponse> is a functional interface and can therefor be implemented with lambdas. The method signature to implement is:

R filter(ServerRequest request, HandlerFunction<T> next)

This allows access to the ServerRequest and after calling next.handle(request) access to the ServerResponse is available.

Example HandlerFilterFunction Implementation

This example will show adding a header to both the request and response.

SampleHandlerFilterFunctions.java
import org.springframework.web.servlet.function.HandlerFilterFunction;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

class SampleHandlerFilterFunctions {
	public static HandlerFilterFunction<ServerResponse, ServerResponse> instrument(String requestHeader, String responseHeader) {
		return (request, next) -> {
			ServerRequest modified = ServerRequest.from(request).header(requestHeader, generateId()).build();
			ServerResponse response = next.handle(modified);
			response.headers().add(responseHeader, generateId());
			return response;
		};
	}
}

First, a new ServerRequest is created from the existing request. This allows us to add the header using the header() method. Then we call next.handle() passing in the modified ServerRequest. Then using the returned ServerResponse we add the header to the response.

How To Use Custom HandlerFilterFunction Implementations

RouteConfiguration.java
import static SampleHandlerFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;

@Configuration
class RouteConfiguration {

    @Bean
    public RouterFunction<ServerResponse> instrumentRoute() {
		return route("instrument_route")
				.GET("/**", http("https://example.org"))
					.filter(instrument("X-Request-Id", "X-Response-Id"))
					.build();
    }
}

The above route will add a X-Request-Id header to the request and a X-Response-Id header to the response.

Writing Custom Before Filter Implementations

The before method takes a Function<ServerRequest, ServerRequest> as a parameter. This allows for creating a new ServerRequest with updated data to be returned from the function.

Before functions may be adapted to HandlerFilterFunction instances via HandlerFilterFunction.ofRequestProcessor().

Example Before Filter Implementation

In this example we will add a header with a generated value to the request.

SampleBeforeFilterFunctions.java
import java.util.function.Function;
import org.springframework.web.servlet.function.ServerRequest;

class SampleBeforeFilterFunctions {
	public static Function<ServerRequest, ServerRequest> instrument(String header) {
		return request -> ServerRequest.from(request).header(header, generateId()).build();
	}
}

A new ServerRequest is created from the existing request. This allows us to add the header using the header() method. This implementation is simpler than the HandlerFilterFunction because we only deal with the ServerRequest.

How To Use Custom Before Filter Implementations

RouteConfiguration.java
import static SampleBeforeFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;

@Configuration
class RouteConfiguration {

    @Bean
    public RouterFunction<ServerResponse> instrumentRoute() {
		return route("instrument_route")
				.GET("/**", http("https://example.org"))
					.before(instrument("X-Request-Id"))
					.build();
    }
}

The above route will add a X-Request-Id header to the request. Note the use of the before() method, rather than filter().

Writing Custom After Filter Implementations

The after method takes a BiFunction<ServerRequest,ServerResponse,ServerResponse>. This allows access to both the ServerRequest and the ServerResponse and the ability to return a new ServerResponse with updated information.

After functions may be adapted to HandlerFilterFunction instances via HandlerFilterFunction.ofResponseProcessor().

Example After Filter Implementation

In this example we will add a header with a generated value to the response.

SampleAfterFilterFunctions.java
import java.util.function.BiFunction;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

class SampleAfterFilterFunctions {
	public static BiFunction<ServerRequest, ServerResponse, ServerResponse> instrument(String header) {
		return (request, response) -> {
			response.headers().add(header, generateId());
			return response;
		};
	}
}

In this case we simply add the header to the response and return it.

How To Use Custom After Filter Implementations

RouteConfiguration.java
import static SampleAfterFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;

@Configuration
class RouteConfiguration {

    @Bean
    public RouterFunction<ServerResponse> instrumentRoute() {
		return route("instrument_route")
				.GET("/**", http("https://example.org"))
					.after(instrument("X-Response-Id"))
					.build();
    }
}

The above route will add a X-Response-Id header to the response. Note the use of the after() method, rather than filter().

How To Register Custom Predicates and Filters for Configuration

To use custom Predicates and Filters in external configuration you need to create a special Supplier class and register it in META-INF/spring.factories.

Registering Custom Predicates

To register custom predicates you need to implement PredicateSupplier. The PredicateDiscoverer looks for static methods that return RequestPredicates to register.

SampleFilterSupplier.java

import org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier;

@Configuration
class SamplePredicateSupplier implements PredicateSupplier {

	@Override
	public Collection<Method> get() {
		return Arrays.asList(SampleRequestPredicates.class.getMethods());
	}

}

You then need to add the class in META-INF/spring.factories.

META-INF/spring.factories
org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier=\
  com.example.SamplePredicateSupplier

Registering Custom Filters

The SimpleFilterSupplier allows for easily registering custom filters. The FilterDiscoverer looks for static methods that return HandlerFilterFunction to register. If you need more flexibility than SimpleFilterSupplier you can implement FilterSupplier directly.

SampleFilterSupplier.java
import org.springframework.cloud.gateway.server.mvc.filter.SimpleFilterSupplier;

@Configuration
class SampleFilterSupplier extends SimpleFilterSupplier {

    public SampleFilterSupplier() {
		super(SampleAfterFilterFunctions.class);
	}
}

You then need to add the class in META-INF/spring.factories.

META-INF/spring.factories
org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\
  com.example.SampleFilterSupplier