Merge branch 'spring-cloud:main' into #2975-Suggestion-Add-Permissions-Policy-as-configurable-option-to-SecureHeaders-GatewayFilter

This commit is contained in:
Jörg Richter
2025-04-07 22:22:36 +02:00
committed by GitHub
79 changed files with 1574 additions and 432 deletions

View File

@@ -12,6 +12,9 @@ The `Retry` `GatewayFilter` factory supports the following parameters:
Retries are performed after a backoff interval of `firstBackoff * (factor ^ n)`, where `n` is the iteration.
If `maxBackoff` is configured, the maximum backoff applied is limited to `maxBackoff`.
If `basedOnPreviousValue` is true, the backoff is calculated by using `prevBackoff * factor`.
* `jitter`: The configured random jitter for the retries.
Generating a backoff between `[backoff - backoff*randomFactor, backoff + backoff*randomFactor]`
* `timeout`: The configured timeout for the retries.
The following defaults are configured for `Retry` filter, if enabled:
@@ -20,6 +23,8 @@ The following defaults are configured for `Retry` filter, if enabled:
* `methods`: GET method
* `exceptions`: `IOException` and `TimeoutException`
* `backoff`: disabled
* `jitter`: disabled
* `timeout`: unlimited
The following listing configures a Retry `GatewayFilter`:
@@ -45,6 +50,9 @@ spring:
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
jitter:
randomFactor: 0.5
timeout: 100ms
----
NOTE: When using the retry filter with a `forward:` prefixed URL, the target endpoint should be written carefully so that, in case of an error, it does not do anything that could result in a response being sent to the client and committed.
@@ -79,10 +87,13 @@ spring:
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
jitter:
randomFactor: 0.5
timeout: 100ms
- id: retryshortcut_route
uri: https://example.org
filters:
- Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false
- Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false,0.5,100ms
----

View File

@@ -7,6 +7,10 @@
== Forwarded Headers Filter
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header.
The `Forwarded by` header part can be enabled by setting the following property to true (defaults to false):
- `spring.cloud.gateway.forwarded.by.enabled=true`
[[removehopbyhop-headers-filter]]
== RemoveHopByHop Headers Filter
The `RemoveHopByHop` Headers Filter removes headers from forwarded requests. The default list of headers that is removed comes from the https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3[IETF].

View File

@@ -14,6 +14,8 @@ spring:
routes:
- id: add_request_header_route
uri: https://example.org
predicates:
- Path=/red
filters:
- AddRequestHeader=X-Request-red, blue
----
@@ -21,6 +23,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestHeader;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -30,10 +33,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("addRequestHeader")
.route(GET("/red"), http("https://example.org"))
.before(addRequestHeader("X-Request-red", "blue"))
.build();
return route("add_request_header_route")
.GET("/red", http())
.before(uri("https://example.org"))
.before(addRequestHeader("X-Request-red", "blue"));
}
}
----
@@ -47,15 +50,20 @@ The following example configures an `AddRequestHeader` filter that uses a variab
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestHeader;
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> gatewayRouterFunctionsAddReqHeader() {
return route("addRequestHeader")
.route(GET("/red/{segment}"), http("https://example.org"))
.before(addRequestHeader("X-Request-red", "blue-{segment}"))
.build();
return route("add_request_header_route")
.GET("/red/{segment}", http())
.before(uri("https://example.org"))
.before(addRequestHeader("X-Request-red", "blue-{segment}"));
}
}
----

View File

@@ -12,8 +12,10 @@ spring:
gateway:
mvc:
routes:
- id: add_request_headers_route
- id: add_request_headers_route_inp
uri: https://example.org
predicates:
- Path=/red
filters:
- AddRequestHeadersIfNotPresent=X-Request-Color-1:blue,X-Request-Color-2:green
----
@@ -21,6 +23,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestHeadersIfNotPresent;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -29,9 +32,11 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route(GET("/red"), http("https://example.org"))
.before(addRequestHeadersIfNotPresent("X-Request-Color-1:blue","X-Request-Color-2:green"));
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeaderInp() {
return route("add_request_headers_route_inp")
.GET("/red", http())
.before(uri("https://example.org"))
.before(addRequestHeadersIfNotPresent("X-Request-Color-1:blue","X-Request-Color-2:green"));
}
}
----
@@ -49,13 +54,20 @@ The following example configures an `AddRequestHeadersIfNotPresent` filter that
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestHeadersIfNotPresent;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
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> gatewayRouterFunctionsAddReqHeader() {
return route(GET("/red/{segment}"), http("https://example.org"))
.before(addRequestHeadersIfNotPresent("X-Request-red", "blue-{segment}"));
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeaderInp() {
return route("add_request_header_route_inp")
.GET("/red/{segment}", http())
.before(uri("https://example.org"))
.before(addRequestHeadersIfNotPresent("X-Request-red", "blue-{segment}"));
}
}
----
@@ -66,7 +78,7 @@ spring:
cloud:
gateway:
routes:
- id: add_request_header_route
- id: add_request_header_route_inp
uri: https://example.org
predicates:
- Path=/red/{segment}

View File

@@ -14,6 +14,8 @@ spring:
routes:
- id: add_request_parameter_route
uri: https://example.org
predicates:
- Path=/anything/addrequestparam
filters:
- AddRequestParameter=red, blue
----
@@ -21,6 +23,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestParameter;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -29,11 +32,12 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("add_request_parameter_route")
.GET("/anything/addrequestparam", http("https://example.org"))
.before(addRequestParameter("red", "blue"))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqParameter() {
return route("add_request_parameter_route")
.GET("/anything/addrequestparam", http())
.before(uri("https://example.org"))
.before(addRequestParameter("red", "blue"))
.build();
}
}
----
@@ -47,6 +51,7 @@ The following example configures an `AddRequestParameter` filter that uses a var
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestParameter;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -56,11 +61,12 @@ import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequ
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("add_request_parameter_route")
.route(host("{segment}.myhost.org"), http("https://example.org"))
.before(addRequestParameter("foo", "bar-{segment}"))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqParameter() {
return route("add_request_parameter_route")
.route(host("{segment}.myhost.org"), http())
.before(uri("https://example.org"))
.before(addRequestParameter("foo", "bar-{segment}"))
.build();
}
}
----

View File

@@ -14,6 +14,8 @@ spring:
routes:
- id: add_response_header_route
uri: https://example.org
predicates:
- Path=/anything/addresheader
filters:
- AddResponseHeader=X-Response-Red, Blue
----
@@ -21,6 +23,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.addResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -30,9 +33,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddRespHeader() {
return route("addresponseheader")
.GET("/anything/addresheader", http("https://example.org"))
.after(addResponseHeader("X-Response-Red", "Blue"))
return route("add_response_header_route")
.GET("/anything/addresheader", http())
.before(uri("https://example.org"))
.after(addResponseHeader("X-Response-Red", "Blue"))
.build();
}
}
@@ -47,6 +51,7 @@ The following example configures an `AddResponseHeader` filter that uses a varia
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.addResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -57,10 +62,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddRespHeader() {
return route("add_response_header_route")
.route(host("{segment}.myhost.org"), http("https://example.org"))
.after(addResponseHeader("foo", "bar-{segment}"))
.build();
return route("add_response_header_route")
.route(host("{segment}.myhost.org"), http())
.before(uri("https://example.org"))
.after(addResponseHeader("foo", "bar-{segment}"))
.build();
}
}
----

View File

@@ -15,14 +15,17 @@ spring:
gateway:
mvc:
routes:
- id: circuitbreaker_route
- id: circuitbreakernofallback
uri: https://example.org
predicates:
- Path=/anything/circuitbreakernofallback
filters:
- CircuitBreaker=myCircuitBreaker
----
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.circuitBreaker;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -33,9 +36,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerNoFallback() {
return route("circuitbreakernofallback")
.route(path("/anything/circuitbreakernofallback"), http("https://example.org"))
.filter(circuitBreaker("mycb3"))
.build();
.route(path("/anything/circuitbreakernofallback"), http())
.before(uri("https://example.org"))
.filter(circuitBreaker("myCircuitBreaker"))
.build();
}
}
----
@@ -74,6 +78,7 @@ The following listing does the same thing in Java:
[source,java]
----
import java.net.URI;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.circuitBreaker;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -84,9 +89,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerFallback() {
return route("circuitbreaker_route")
.route(path("/consumingServiceEndpoint"), http("https://example.org"))
.filter(circuitBreaker("myCircuitBreaker", URI.create("forward:/inCaseOfFailureUseThis")))
.build();
.route(path("/consumingServiceEndpoint"), http())
.before(uri("https://example.org"))
.filter(circuitBreaker("myCircuitBreaker", URI.create("forward:/inCaseOfFailureUseThis")))
.build();
}
}
----
@@ -102,6 +108,7 @@ In the example below the call `consumingServiceEndpoint/users/1` will be redirec
[source,java]
----
import java.net.URI;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.circuitBreaker;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -112,9 +119,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerFallback() {
return route("circuitbreaker_route")
.route(path("/consumingServiceEndpoint/{*segments}"), http("https://example.org"))
.filter(circuitBreaker("myCircuitBreaker", URI.create("forward:/inCaseOfFailureUseThis/{segments}")))
.build();
.route(path("/consumingServiceEndpoint/{*segments}"), http())
.before(uri("https://example.org"))
.filter(circuitBreaker("myCircuitBreaker", URI.create("forward:/inCaseOfFailureUseThis/{segments}")))
.build();
}
}
----
@@ -126,6 +134,7 @@ However, you can also reroute the request to a controller or handler in an exter
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.circuitBreaker;
import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
@@ -142,7 +151,8 @@ class RouteConfiguration {
.filter(circuitBreaker("fetchIngredients", URI.create("forward:/fallback")))
.build()
.and(route("ingredients-fallback")
.route(path("/fallback"), http("http://localhost:9994"))
.route(path("/fallback"), http())
.before(uri("https://localhost:9994"))
.build());
}
}

View File

@@ -14,6 +14,8 @@ spring:
routes:
- id: dedupe_response_header_route
uri: https://example.org
predicates:
- Path=/hello
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
----
@@ -21,18 +23,19 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.dedupeResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.web.servlet.function.RequestPredicates.path;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsDedupeResponseHeader() {
return route("dedupe_response_header_route")
.route(path("/hello"), http("https://example.org"))
.route(path("/hello"), http())
.before(uri("https://example.org"))
.after(dedupeResponseHeader("Access-Control-Allow-Credentials Access-Control-Allow-Origin"))
.build();
}

View File

@@ -14,7 +14,7 @@ spring:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
- Path=/ingredients/**
filters:
- name: CircuitBreaker
args:
@@ -33,8 +33,9 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.circuitBreaker;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.fallbackHeaders;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.circuitBreaker;
import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -50,7 +51,8 @@ class RouteConfiguration {
.filter(circuitBreaker("fetchIngredients", URI.create("forward:/fallback")))
.build()
.and(route("ingredients-fallback")
.route(path("/fallback"), http("http://localhost:9994"))
.route(path("/fallback"), http())
.before(uri("http://localhost:9994"))
.before(fallbackHeaders())
.build());
}

View File

@@ -43,6 +43,8 @@ spring:
- Path=/api/**
----
WARNING: If using the `lb()` filter, it needs to be after any filter that manipulates the path such as `setPath()` or `stripPrefix()`, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.
NOTE: By default, when a service instance cannot be found by the `ReactorLoadBalancer`, a `503` is returned.
// TODO: implement use404
// You can configure the gateway to return a `404` by setting `spring.cloud.gateway.loadbalancer.use404=true`.

View File

@@ -17,6 +17,8 @@ spring:
routes:
- id: map_request_header_route
uri: https://example.org
predicates:
- Path=/mypath
filters:
- MapRequestHeader=Blue, X-Request-Red
----
@@ -24,7 +26,8 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestParameter;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.mapRequestHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -34,7 +37,8 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsMapRequestHeader() {
return route("map_request_header_route")
.GET("/mypath", http("https://example.org"))
.GET("/mypath", http())
.before(uri("https://example.org"))
.before(mapRequestHeader("Blue", "X-Request-Red"))
.build();
}

View File

@@ -11,6 +11,7 @@ The following listing shows how to modify a request body filter:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.modifyRequestBody;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
@@ -20,12 +21,13 @@ import org.springframework.http.MediaType;
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("rewrite_request_obj")
.route(host("*.rewriterequestobj.org"), http("https://example.org"))
.before(modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
(request, s) -> new Hello(s.toUpperCase())))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsModifyRequestBody() {
return route("modify_request_body")
.route(host("*.modifyrequestbody.org"), http())
.before(uri("https://example.org"))
.before(modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
(request, s) -> new Hello(s.toUpperCase())))
.build();
}
record Hello(String message) { }

View File

@@ -5,24 +5,34 @@ You can use the `ModifyResponseBody` filter to modify the response body before i
NOTE: This filter can be configured only by using the Java DSL.
The following listing shows how to modify a response body filter:
The following listing shows how to modify a response body filter:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.modifyResponseBody;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsModifyResponseBodySimple() {
return route("modify_response_body")
.GET("/anything/modifyresponsebody", http())
.before(new HttpbinUriResolver())
.before(uri("https://example.org"))
.after(modifyResponseBody(String.class, String.class, null,
(request, response, s) -> s.replace("fooval", "FOOVAL")))
.build();
}
----
The sample above does not change the content type or do anything dynamic. Below, the sample changes the content type and dynamically modifies the content.
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.modifyResponseBody;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
@@ -32,11 +42,13 @@ import org.springframework.http.MediaType;
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("rewrite_request_obj")
.route(host("*.rewriterequestobj.org"), http("https://example.org"))
.before(modifyResponseBody(String.class, String.class, MediaType.APPLICATION_JSON_VALUE, (request, response, s) -> s.toUpperCase()))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsModifyResponseBody() {
return route("modify_response_bodu")
.route(host("*.modifyresponsebodu.org"), http())
.before(uri("https://example.org"))
.after(modifyResponseBody(String.class, String.class, MediaType.APPLICATION_JSON_VALUE,
(request, s) -> s.toUpperCase()))
.build();
}
}

View File

@@ -15,6 +15,8 @@ spring:
routes:
- id: prefixpath_route
uri: https://example.org
predicates:
- Path=/**
filters:
- PrefixPath=/mypath
----
@@ -22,6 +24,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.prefixPath;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -31,10 +34,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsPrefixPath() {
return route("prefixpath_route")
.GET("/**", http("https://example.org"))
.before("/mypath")
.build();
return route("prefixpath_route")
.GET("/**", http())
.before(uri("https://example.org"))
.before(prefixPath("/mypath"))
.build();
}
}
----
@@ -42,3 +46,4 @@ class RouteConfiguration {
This prefixes `/mypath` to the path of all matching requests.
So a request to `/hello` is sent to `/mypath/hello`.
WARNING: If using the `lb()` filter, it needs to be after the `prefixPath()` filter, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.

View File

@@ -16,6 +16,8 @@ spring:
routes:
- id: preserve_host_route
uri: https://example.org
predicates:
- Path=/**
filters:
- PreserveHostHeader
----
@@ -23,6 +25,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.preserveHostHeader;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -32,10 +35,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsPreserveHostHeader() {
return route("preserve_host_route")
.GET("/**", http("https://example.org"))
.before(preserveHostHeader())
.build();
return route("preserve_host_route")
.GET("/**", http())
.before(uri("https://example.org"))
.before(preserveHostHeader())
.build();
}
}
----

View File

@@ -47,6 +47,7 @@ The following is an example of configuring a route with rate limiting:
.RouteConfiguration.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions.rateLimit;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -56,12 +57,13 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRateLimited() {
return route("rate_limited_route")
.GET("/api/**", http("https://example.org"))
.filter(rateLimit(c -> c.setCapacity(100)
.setPeriod(Duration.ofMinutes(1))
.setKeyResolver(request -> request.servletRequest().getUserPrincipal().getName())))
.build();
return route("rate_limited_route")
.GET("/api/**", http())
.before(uri("https://example.org"))
.filter(rateLimit(c -> c.setCapacity(100)
.setPeriod(Duration.ofMinutes(1))
.setKeyResolver(request -> request.servletRequest().getUserPrincipal().getName())))
.build();
}
}
----

View File

@@ -18,6 +18,8 @@ spring:
routes:
- id: redirectto_route
uri: https://example.org
predicates:
- Path=/**
filters:
- RedirectTo=302, https://acme.org
----
@@ -25,6 +27,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.redirectTo;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -34,10 +37,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRedirectTo() {
return route("redirectto_route")
.GET("/**", http("https://example.org"))
.filter(redirectTo(302, URI.create("acme.org")))
.build();
return route("redirectto_route")
.GET("/**", http())
.before(uri("https://example.org"))
.filter(redirectTo(302, URI.create("acme.org")))
.build();
}
}
----

View File

@@ -16,6 +16,8 @@ spring:
routes:
- id: removerequestheader_route
uri: https://example.org
predicates:
- Path=/**
filters:
- RemoveRequestHeader=X-Request-Foo
----
@@ -23,6 +25,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.removeRequestHeader;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -31,11 +34,12 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsremoveRequestHeader() {
return route("removerequestheader_route")
.GET("/**", http("https://example.org"))
.before(removeRequestHeader("X-Request-Foo"))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsRemoveRequestHeader() {
return route("removerequestheader_route")
.GET("/**", http())
.before(uri("https://example.org"))
.before(removeRequestHeader("X-Request-Foo"))
.build();
}
}
----

View File

@@ -16,6 +16,8 @@ spring:
routes:
- id: removerequestparameter_route
uri: https://example.org
predicates:
- Path=/**
filters:
- RemoveRequestParameter=red
----
@@ -23,7 +25,8 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestParameter;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.removeRequestParameter;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -31,11 +34,12 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("removerequestparameter_route")
.GET("/**", http("https://example.org"))
.before(removeRequestParameter("red"))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsRemoveRequestParameter() {
return route("removerequestparameter_route")
.GET("/**", http())
.before(uri("https://example.org"))
.before(removeRequestParameter("red"))
.build();
}
}
----

View File

@@ -15,6 +15,8 @@ spring:
routes:
- id: removeresponseheader_route
uri: https://example.org
predicates:
- Path=/anything/removeresponseheader
filters:
- RemoveResponseHeader=X-Response-Foo
----
@@ -22,6 +24,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.removeResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -31,9 +34,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRemoveResponseHeader() {
return route("addresponseheader")
.GET("/anything/addresheader", http("https://example.org"))
.after(removeResponseHeader("X-Response-Foo"))
return route("removeresponseheader_route")
.GET("/anything/removeresponseheader", http())
.before(uri("https://example.org"))
.after(removeResponseHeader("X-Response-Foo"))
.build();
}
}

View File

@@ -16,6 +16,8 @@ spring:
routes:
- id: requestheadersize_route
uri: https://example.org
predicates:
- Path=/**
filters:
- RequestHeaderSize=1000B
----
@@ -23,6 +25,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.requestHeaderSize;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -32,10 +35,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRequestHeaderSize() {
return route("requestheadersize_route")
.GET("/**", http("https://example.org"))
.before(requestHeaderSize("1000B"))
.build();
return route("requestheadersize_route")
.GET("/**", http())
.before(uri("https://example.org"))
.before(requestHeaderSize("1000B"))
.build();
}
}
----

View File

@@ -28,6 +28,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.requestSize;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -37,10 +38,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRequestSize() {
return route("request_size_route")
.GET("/upload", http("http://localhost:8080"))
.before(requestSize("5000000"))
.build();
return route("request_size_route")
.GET("/upload", http())
.before(uri("http://localhost:8080"))
.before(requestSize("5000000"))
.build();
}
}
----

View File

@@ -36,7 +36,7 @@ spring:
gateway:
mvc:
routes:
- id: retry_test
- id: retry_route
uri: http://localhost:8080/flakey
predicates:
- Host=*.retry.com
@@ -53,6 +53,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.adaptCachedBody;
import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
@@ -63,12 +64,16 @@ import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequ
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("add_request_parameter_route")
.route(host("*.retry.com"), http("https://example.org"))
.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST)).setCacheBody(true)))
.filter(adaptCachedBody())
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsRetry() {
return route("retry_route")
.route(host("*.retry.com"), http())
.before(uri("http://localhost:8080/flakey"))
.filter(retry(config -> config.setRetries(3)
.setSeries(Set.of(HttpStatus.Series.SERVER_ERROR))
.setMethods(Set.of(HttpMethod.GET, HttpMethod.POST))
.setCacheBody(true)))
.filter(adaptCachedBody())
.build();
}
}
----
@@ -79,11 +84,11 @@ Instead, it should throw an `Exception` or signal an error (for example, through
NOTE: When using the retry filter, it will retry all filters that come after it. Make sure the results of the filters following the retry filter are as expected when they are executed multiple times.
// WARNING: When using the retry filter with any HTTP method with a body, the body will be cached and the gateway will become memory constrained. The body is cached in a request attribute defined by `ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR`. The type of the object is `org.springframework.core.io.buffer.DataBuffer`.
WARNING: When using the retry filter with any HTTP method with a body and `cacheBody=true`, the body will be cached and the gateway will become memory constrained. The body is cached in a request attribute defined by `MvcUtils.CACHED_REQUEST_BODY_ATT`. The type of the object is `ByteArrayInputStream`.
A simplified "shortcut" notation can be added with a single `status` and `method`.
The following two examples are equivalent:
The following two example routes are equivalent:
.application.yml
[source,yaml]

View File

@@ -15,6 +15,8 @@ spring:
routes:
- id: rewritelocationresponseheader_route
uri: http://example.org
predicates:
- Path=/**
filters:
- RewriteLocationResponseHeader=AS_IN_REQUEST, Location, ,
----
@@ -22,7 +24,8 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.addResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.RewriteLocationResponseHeaderFilterFunctions.rewriteLocationResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.RewriteLocationResponseHeaderFilterFunctions.StripVersion;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -33,9 +36,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRewriteLocationResponseHeader() {
return route("rewritelocationresponseheader_route")
.GET("/**", http("https://example.org"))
.after(rewriteLocationResponseHeader(config -> config.setLocationHeaderName("Location").setStripVersion(StripVersion.AS_IN_REQUEST)))
.build();
.GET("/**", http())
.before(uri("https://example.org"))
.after(rewriteLocationResponseHeader(config -> config.setLocationHeaderName("Location")
.setStripVersion(StripVersion.AS_IN_REQUEST)))
.build();
}
}
----

View File

@@ -25,6 +25,7 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.rewritePath;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -33,13 +34,15 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRewritePath() {
return route("rewritepath_route")
.GET("/red/**", http("https://example.org"))
.before(rewritePath("/red/(?<segment>.*)", "/${segment}"))
.build();
return route("rewritepath_route")
.GET("/red/**", http())
.before(uri("https://example.org"))
.before(rewritePath("/red/(?<segment>.*)", "/${segment}"))
.build();
}
}
----
For a request path of `/red/blue`, this sets the path to `/blue` before making the downstream request. Note that in `application.yml` the `$` should be replaced with `$\` because of the YAML specification.
WARNING: If using the `lb()` filter, it needs to be after the `rewritePath()` filter, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.

View File

@@ -26,6 +26,7 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.addRequestParameter;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -33,11 +34,12 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("add_request_parameter_route")
.GET("/anything/addrequestparam", http("https://example.org"))
.before(addRequestParameter("red", "blue"))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsRewriteRequestParameter() {
return route("rewriterequestparameter_route")
.GET("products", http())
.before(uri("https://example.org"))
.before(addRequestParameter("red", "blue"))
.build();
}
}
----

View File

@@ -16,14 +16,17 @@ spring:
routes:
- id: rewriteresponseheader_route
uri: https://example.org
predicates:
- Path=/**
filters:
- RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=***
- RewriteResponseHeader=X-Response-Red, password=[^&]+, password=***
----
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.addResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.rewriteResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -33,8 +36,9 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRewriteResponseHeader() {
return route("rewriteresponseheader_route")
.GET("/**", http("https://example.org"))
.after(rewriteResponseHeader("X-Request-Red", "password=[^&]+", "password=***"))
.GET("/**", http())
.before(uri("https://example.org"))
.after(rewriteResponseHeader("X-Request-Red", "password=[^&]+", "password=***"))
.build();
}
}

View File

@@ -27,6 +27,7 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.setPath;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -35,13 +36,15 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetPath() {
return route("add_request_parameter_route")
.GET("/red/{segment}", http("https://example.org"))
.before(setPath("/{segment"))
.build();
return route("setpath_route")
.GET("/red/{segment}", http())
.before(uri("https://example.org"))
.before(setPath("/{segment"))
.build();
}
}
----
For a request path of `/red/blue`, this sets the path to `/blue` before making the downstream request.
WARNING: If using the `lb()` filter, it needs to be after the `setPath()` filter, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.

View File

@@ -14,6 +14,8 @@ spring:
routes:
- id: setrequestheader_route
uri: https://example.org
predicates:
- Path=/**
filters:
- SetRequestHeader=X-Request-Red, Blue
----
@@ -22,6 +24,7 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.setRequestHostHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -30,10 +33,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetRequestHeader() {
return route("add_request_parameter_route")
.GET("/**", http("https://example.org"))
.before(setRequestHostHeader("X-Request-Red", "Blue"))
.build();
return route("setrequestheader_route")
.GET("/**", http())
.before(uri("https://example.org"))
.before(setRequestHostHeader("X-Request-Red", "Blue"))
.build();
}
}
----
@@ -64,19 +68,21 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.setRequestHostHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetRequestHeader() {
return route("add_request_parameter_route")
.route(host("{segment}.myhost.org"), http("https://example.org"))
.before(setRequestHostHeader("X-Request-Red", "Blue-{segment}"))
.build();
return route("setrequestheader_route")
.route(host("{segment}.myhost.org"), http())
.before(uri("https://example.org"))
.before(setRequestHostHeader("X-Request-Red", "Blue-{segment}"))
.build();
}
}
----

View File

@@ -26,6 +26,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.setRequestHostHeader;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -35,10 +36,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetRequestHostHeader() {
return route("set_request_host_header_route")
.GET("/headers", http("http://localhost:8080"))
.before(setRequestHostHeader("example.org"))
.build();
return route("set_request_host_header_route")
.GET("/headers", http())
.before(uri("http://localhost:8080"))
.before(setRequestHostHeader("example.org"))
.build();
}
}
----

View File

@@ -14,6 +14,8 @@ spring:
routes:
- id: setresponseheader_route
uri: https://example.org
predicates:
- Path=/anything/setresponseheader
filters:
- SetResponseHeader=X-Response-Red, Blue
----
@@ -22,6 +24,7 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.setResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -30,9 +33,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetResponseHeader() {
return route("addresponseheader")
.GET("/anything/addresheader", http("https://example.org"))
.after(setResponseHeader("X-Response-Red", "Blue"))
return route("setresponseheader_route")
.GET("/anything/setresponseheader", http())
.before(uri("https://example.org"))
.after(setResponseHeader("X-Response-Red", "Blue"))
.build();
}
}
@@ -64,6 +68,7 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.setResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
@@ -73,10 +78,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetResponseHeader() {
return route("add_response_header_route")
.route(host("{segment}.myhost.org"), http("https://example.org"))
.after(setResponseHeader("foo", "bar-{segment}"))
.build();
return route("setresponseheader_route")
.route(host("{segment}.myhost.org"), http())
.before(uri("https://example.org"))
.after(setResponseHeader("foo", "bar-{segment}"))
.build();
}
}
----

View File

@@ -16,10 +16,14 @@ spring:
routes:
- id: setstatusstring_route
uri: https://example.org
predicates:
- Path=/path1
filters:
- SetStatus=UNAUTHORIZED
- id: setstatusint_route
uri: https://example.org
predicates:
- Path=/path2
filters:
- SetStatus=401
----
@@ -28,6 +32,7 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.setStatus;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -37,13 +42,15 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetStatus() {
return route("setstatus_route")
.GET("/path1", http("https://example.org"))
.GET("/path1", http())
.before(uri("https://example.org"))
// setStatus("UNAUTHORIZED") works as well
.after(setStatus(HttpStatus.UNAUTHORIZED))
.build().and(route("setstatusint_route")
.GET("/path2", http("https://example.org"))
.before(uri("https://example.org"))
.GET("/path2", http())
.after(setStatus("401"))
.build());
.build());
}
}
----

View File

@@ -13,8 +13,8 @@ spring:
gateway:
mvc:
routes:
- id: nameRoot
uri: https://nameservice
- id: strip_prefix_route
uri: https://example.org
predicates:
- Path=/name/**
filters:
@@ -25,6 +25,7 @@ spring:
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.stripPrefix;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -33,13 +34,15 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsStripPrefix() {
return route("nameRoot")
.GET("/name/**", http("https://example.org"))
.before(stripPrefix(2))
.build();
return route("strip_prefix_route")
.GET("/name/**", http())
.before(uri("https://example.org"))
.before(stripPrefix(2))
.build();
}
}
----
When a request is made through the gateway to `/name/blue/red`, the request made to `nameservice` looks like `https://nameservice/red`.
When a request is made through the gateway to `/name/blue/red`, the full request url looks like `https://example.org/red`.
WARNING: If using the `lb()` filter, it needs to be after the `stripPrefix()` filter, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.

View File

@@ -54,6 +54,7 @@ Spring Cloud Gateway Server MVC can forward the OAuth2 access token of the curre
.RouteConfiguration.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions.tokenRelay;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -62,11 +63,12 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
return route("resource")
.GET("/resource", http("http://localhost:9000"))
.filter(tokenRelay())
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsTokenRelay() {
return route("resource")
.GET("/resource", http())
.before(uri("https://localhost:9000"))
.filter(tokenRelay())
.build();
}
}
----

View File

@@ -31,6 +31,7 @@ spring:
[source,java]
----
import java.time.ZonedDateTime;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.after;
@@ -40,9 +41,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsAfter() {
return route("after_route")
.route(after(ZonedDateTime.parse("2017-01-20T17:42:47.789-07:00[America/Denver]")), http("https://example.org"))
.build();
return route("after_route")
.route(after(ZonedDateTime.parse("2017-01-20T17:42:47.789-07:00[America/Denver]")), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -74,6 +76,7 @@ spring:
[source,java]
----
import java.time.ZonedDateTime;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.before;
@@ -83,9 +86,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsBefore() {
return route("before_route")
.route(before(ZonedDateTime.parse("2017-01-20T17:42:47.789-07:00[America/Denver]")), http("https://example.org"))
.build();
return route("before_route")
.route(before(ZonedDateTime.parse("2017-01-20T17:42:47.789-07:00[America/Denver]")), http()
.before(uri("https://example.org"))
.build();
}
}
----
@@ -119,6 +123,7 @@ spring:
[source,java]
----
import java.time.ZonedDateTime;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.between;
@@ -128,9 +133,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsBetween() {
return route("between_route")
.route(between(ZonedDateTime.parse("2017-01-20T17:42:47.789-07:00[America/Denver]"), ZonedDateTime.parse("2017-01-21T17:42:47.789-07:00[America/Denver]")), http("https://example.org"))
.build();
return route("between_route")
.route(between(ZonedDateTime.parse("2017-01-20T17:42:47.789-07:00[America/Denver]"),
ZonedDateTime.parse("2017-01-21T17:42:47.789-07:00[America/Denver]")), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -162,18 +169,20 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.between;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.cookie;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsCookie() {
return route("cookie_route")
.route(cookie("chocolate", "ch.p"), http("https://example.org"))
.build();
return route("cookie_route")
.route(cookie("chocolate", "ch.p"), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -204,6 +213,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.header;
@@ -212,10 +222,11 @@ import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequ
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsCookie() {
return route("cookie_route")
.route(header("X-Request-Id", "\\d+"), http("https://example.org"))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsHeader() {
return route("header_route")
.route(header("X-Request-Id", "\\d+"), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -247,6 +258,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
@@ -256,9 +268,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsHost() {
return route("host_route")
.route(host("**.somehost.org", "**.anotherhost.org"), http("https://example.org"))
.build();
return route("host_route")
.route(host("**.somehost.org", "**.anotherhost.org"), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -296,6 +309,7 @@ spring:
[source,java]
----
import org.springframework.http.HttpMethod;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.method;
@@ -305,9 +319,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsMethod() {
return route("method_route")
.route(method(HttpMethod.GET, HttpMethod.POST), http("https://example.org"))
.build();
return route("method_route")
.route(method(HttpMethod.GET, HttpMethod.POST), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -319,19 +334,19 @@ This route matches if the request method was a `GET` or a `POST`.
.GatewaySampleApplication.java
[source,java]
----
import org.springframework.http.HttpMethod;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.methods;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsMethod() {
return route("method_route")
.GET("/mypath", http("https://example.org"))
.build();
public RouterFunction<ServerResponse> gatewayRouterFunctionsMethodAndPath() {
return route("method_and_path_route")
.GET("/mypath", http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -363,19 +378,20 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import org.springframework.http.HttpMethod;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.method;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.path;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsPath() {
return route("path_route")
.route(path("/red/{segment}", "/blue/{segment}"), http("https://example.org"))
.build();
return route("path_route")
.route(path("/red/{segment}", "/blue/{segment}"), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -421,7 +437,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import org.springframework.http.HttpMethod;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.query;
@@ -431,9 +447,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsQuery() {
return route("query_route")
.route(query("green"), http("https://example.org"))
.build();
return route("query_route")
.route(query("green"), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -457,7 +474,7 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import org.springframework.http.HttpMethod;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.query;
@@ -467,9 +484,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsQuery() {
return route("query_route")
.route(query("red", "gree."), http("https://example.org"))
.build();
return route("query_route")
.route(query("red", "gree."), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -585,22 +603,25 @@ spring:
.GatewaySampleApplication.java
[source,java]
----
import org.springframework.http.HttpMethod;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.method;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.path;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.weight;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsWeights() {
return route("weight_high")
.route(weight("group1", 8).and(path("/**")), http("https://weighthigh.org"))
.build().and(
route("weight_low")
.route(weight("group1", 2).and(path("/**")), http("https://weightlow.org"))
.build());
return route("weight_high")
.route(weight("group1", 8).and(path("/**")), http())
.before(uri("https://weighthigh.org"))
.build().and(
route("weight_low")
.route(weight("group1", 2).and(path("/**")), http())
.before(uri("https://weightlow.org"))
.build());
}
}
----

View File

@@ -9,12 +9,15 @@ A https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springfra
[source,java]
----
import static org.springframework.web.servlet.function.RouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
class SimpleGateway {
@Bean
public RouterFunction<ServerResponse> getRoute() {
return route().GET("/get", http("https://httpbin.org")).build();
return route().GET("/get", http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -29,13 +32,16 @@ Some advanced filters require some metadata to be added to request attributes. T
.GatewaySampleApplication.java
[source,java]
----
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
class SimpleGateway {
@Bean
public RouterFunction<ServerResponse> getRoute() {
return route("simple_route").GET("/get", http("https://httpbin.org")).build();
return route("simple_route").GET("/get", http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -43,4 +49,6 @@ class SimpleGateway {
[[gateway-handlerfunctions]]
== Gateway MVC Handler Functions
Various `RouterFunctions.Builder` methods require a `HandlerFunction<ServerResponse>`. To create a route that is proxied by the MVC Gateway, `HandlerFunction` implementations are supplied in `org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions`. The most basic is the `http()` `HandlerFunction`. If a `URI` is supplied as a parameter, that is the `URI` used as the downstream target for sending the HTTP requests (as seen in the example above). If no parameter is passed, the function looks for a `URI` in the `org.springframework.cloud.gateway.server.mvc.common.MvcUtils.GATEWAY_REQUEST_URL_ATTR` request attribute. This allows for dynamic targets such as load balancing to set the `URI`.
Various `RouterFunctions.Builder` methods require a `HandlerFunction<ServerResponse>`. To create a route that is proxied by the MVC Gateway, `HandlerFunction` implementations are supplied in `org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions`. The most basic is the `http()` `HandlerFunction`. The function looks for a `URI` in the `org.springframework.cloud.gateway.server.mvc.common.MvcUtils.GATEWAY_REQUEST_URL_ATTR` request attribute. This allows for dynamic targets such as load balancing to set the `URI`.
WARNING: As of version 4.1.7, `HandlerFunctions.http(String)` and `HandlerFunctions.http(URI)` are now deprecated. Please use `HandlerFunctions.http()` in combination with the `BeforeFilterFunctions.uri()` filter instead. This fixes inconsistencies in dealing with the route url request attribute.

View File

@@ -48,6 +48,7 @@ To use our new `headerExists` `RequestPredicate`, we need to plug it in to an ap
[source,java]
----
import static SampleRequestPredicates.headerExists;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -56,9 +57,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> headerExistsRoute() {
return route("header_exists_route")
.route(headerExists("X-Green"), http("https://example.org"))
.build();
return route("header_exists_route")
.route(headerExists("X-Green"), http())
.before(uri("https://example.org"))
.build();
}
}
----
@@ -111,6 +113,7 @@ First, a new `ServerRequest` is created from the existing request. This allows u
[source,java]
----
import static SampleHandlerFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -120,9 +123,10 @@ 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();
.GET("/**", http())
.filter(instrument("X-Request-Id", "X-Response-Id"))
.before(uri("https://example.org"))
.build();
}
}
----
@@ -160,6 +164,7 @@ A new `ServerRequest` is created from the existing request. This allows us to ad
[source,java]
----
import static SampleBeforeFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -168,10 +173,10 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> instrumentRoute() {
return route("instrument_route")
.GET("/**", http("https://example.org"))
.before(instrument("X-Request-Id"))
.build();
return route("instrument_route").GET("/**", http())
.before(uri("https://example.org"))
.before(instrument("X-Request-Id"))
.build();
}
}
----
@@ -213,6 +218,7 @@ In this case we simply add the header to the response and return it.
[source,java]
----
import static SampleAfterFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -221,10 +227,11 @@ class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> instrumentRoute() {
return route("instrument_route")
.GET("/**", http("https://example.org"))
.after(instrument("X-Response-Id"))
.build();
return route("instrument_route")
.GET("/**", http())
.before(uri("https://example.org"))
.after(instrument("X-Response-Id"))
.build();
}
}
----

View File

@@ -63,6 +63,7 @@
|spring.cloud.gateway.filter.set-response-header.enabled | `+++true+++` | Enables the set-response-header filter.
|spring.cloud.gateway.filter.set-status.enabled | `+++true+++` | Enables the set-status filter.
|spring.cloud.gateway.filter.strip-prefix.enabled | `+++true+++` | Enables the strip-prefix filter.
|spring.cloud.gateway.forwarded.by.enabled | `+++false+++` | Enables the Forwarded: by header part.
|spring.cloud.gateway.forwarded.enabled | `+++true+++` | Enables the ForwardedHeadersFilter.
|spring.cloud.gateway.global-filter.adapt-cached-body.enabled | `+++true+++` | Enables the adapt-cached-body global filter.
|spring.cloud.gateway.global-filter.forward-path.enabled | `+++true+++` | Enables the forward-path global filter.

View File

@@ -19,7 +19,7 @@
<version>4.3.0-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<modules>
<module>grpc</module>
<module>http2</module>

View File

@@ -136,4 +136,4 @@
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>

View File

@@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -72,6 +73,11 @@ public abstract class MvcUtils {
*/
public static final String GATEWAY_ATTRIBUTES_ATTR = qualify("gatewayAttributes");
/**
* Gateway original request URL attribute name.
*/
public static final String GATEWAY_ORIGINAL_REQUEST_URL_ATTR = qualify("gatewayOriginalRequestUrl");
/**
* Gateway request URL attribute name.
*/
@@ -250,6 +256,13 @@ public abstract class MvcUtils {
request.servletRequest().setAttribute(GATEWAY_REQUEST_URL_ATTR, url);
}
@SuppressWarnings("unchecked")
public static void addOriginalRequestUrl(ServerRequest request, URI url) {
LinkedHashSet<URI> urls = (LinkedHashSet<URI>) request.attributes()
.computeIfAbsent(GATEWAY_ORIGINAL_REQUEST_URL_ATTR, s -> new LinkedHashSet<>());
urls.add(url);
}
private record ByteArrayInputMessage(ServerRequest request, ByteArrayInputStream body) implements HttpInputMessage {
@Override

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.server.mvc.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.fn;
@Configuration
@ConditionalOnClass(name = "org.springframework.cloud.function.context.FunctionCatalog")
@ConditionalOnProperty(name = "spring.cloud.gateway.function.enabled", havingValue = "true", matchIfMissing = true)
public class DefaultFunctionConfiguration {
@Bean
RouterFunction<ServerResponse> gatewayToFunctionRouter() {
// @formatter:off
return route("functionroute")
.POST("/{path}/{name}", fn("{path}/{name}"))
.POST("/{path}", fn("{path}"))
.GET("/{path}/{name}", fn("{path}/{name}"))
.GET("/{path}", fn("{path}"))
.build();
// @formatter:on
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.gateway.server.mvc.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
@@ -38,7 +39,6 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.cloud.gateway.server.mvc.common.Configurable;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.FilterDiscoverer;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerDiscoverer;
import org.springframework.cloud.gateway.server.mvc.invoke.InvocationContext;
@@ -149,15 +149,6 @@ public class RouterFunctionHolderFactory {
RouterFunctions.Builder builder = route(routeId);
// MVC.fn users won't need this anonymous filter as url will be set directly.
// Put this function first, so if a filter from a handler changes the url
// it is after this one.
builder.filter((request, next) -> {
MvcUtils.setRequestUrl(request, routeProperties.getUri());
return next.handle(request);
});
builder.before(BeforeFilterFunctions.routeId(routeId));
MultiValueMap<String, OperationMethod> handlerOperations = handlerDiscoverer.getOperations();
// TODO: cache?
// translate handlerFunction
@@ -178,12 +169,17 @@ public class RouterFunctionHolderFactory {
NormalizedOperationMethod normalizedOpMethod = handlerOperationMethod.get();
Object response = invokeOperation(normalizedOpMethod, normalizedOpMethod.getNormalizedArgs());
HandlerFunction<ServerResponse> handlerFunction = null;
// filters added by HandlerDiscoverer need to go last, so save them
List<HandlerFilterFunction<ServerResponse, ServerResponse>> lowerPrecedenceFilters = new ArrayList<>();
List<HandlerFilterFunction<ServerResponse, ServerResponse>> higherPrecedenceFilters = new ArrayList<>();
if (response instanceof HandlerFunction<?>) {
handlerFunction = (HandlerFunction<ServerResponse>) response;
}
else if (response instanceof HandlerDiscoverer.Result result) {
handlerFunction = result.getHandlerFunction();
result.getFilters().forEach(builder::filter);
lowerPrecedenceFilters.addAll(result.getLowerPrecedenceFilters());
higherPrecedenceFilters.addAll(result.getHigherPrecedenceFilters());
}
if (handlerFunction == null) {
throw new IllegalStateException(
@@ -214,6 +210,9 @@ public class RouterFunctionHolderFactory {
builder.route(predicate.get(), handlerFunction);
predicate.set(null);
// HandlerDiscoverer filters needing lower priority, so put them first
lowerPrecedenceFilters.forEach(builder::filter);
// translate filters
MultiValueMap<String, OperationMethod> filterOperations = filterDiscoverer.getOperations();
routeProperties.getFilters().forEach(filterProperties -> {
@@ -221,6 +220,9 @@ public class RouterFunctionHolderFactory {
translate(filterOperations, filterProperties.getName(), args, HandlerFilterFunction.class, builder::filter);
});
// HandlerDiscoverer filters need higher priority, so put them last
higherPrecedenceFilters.forEach(builder::filter);
builder.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, routeId);
return builder.build();

View File

@@ -189,6 +189,7 @@ public abstract class BeforeFilterFunctions {
final UriTemplate uriTemplate = new UriTemplate(prefix);
return request -> {
MvcUtils.addOriginalRequestUrl(request, request.uri());
Map<String, Object> uriVariables = MvcUtils.getUriTemplateVariables(request);
URI uri = uriTemplate.expand(uriVariables);
@@ -326,16 +327,18 @@ public abstract class BeforeFilterFunctions {
String normalizedReplacement = replacement.replace("$\\", "$");
Pattern pattern = Pattern.compile(regexp);
return request -> {
// TODO: original request url
String path = request.uri().getRawPath();
MvcUtils.addOriginalRequestUrl(request, request.uri());
String path = request.uri().getPath();
String newPath = pattern.matcher(path).replaceAll(normalizedReplacement);
URI rewrittenUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(newPath).build().toUri();
URI rewrittenUri = UriComponentsBuilder.fromUri(request.uri())
.replacePath(newPath)
.encode()
.build()
.toUri();
ServerRequest modified = ServerRequest.from(request).uri(rewrittenUri).build();
// TODO: can this be restored at some point?
// MvcUtils.setRequestUrl(modified, modified.uri());
return modified;
};
}
@@ -372,14 +375,12 @@ public abstract class BeforeFilterFunctions {
UriTemplate uriTemplate = new UriTemplate(path);
return request -> {
MvcUtils.addOriginalRequestUrl(request, request.uri());
Map<String, Object> uriVariables = MvcUtils.getUriTemplateVariables(request);
URI uri = uriTemplate.expand(uriVariables);
URI prefixedUri = UriComponentsBuilder.fromUri(request.uri())
.replacePath(uri.getRawPath())
.build(true)
.toUri();
return ServerRequest.from(request).uri(prefixedUri).build();
URI newUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(uri.getRawPath()).build(true).toUri();
return ServerRequest.from(request).uri(newUri).build();
};
}
@@ -410,6 +411,7 @@ public abstract class BeforeFilterFunctions {
public static Function<ServerRequest, ServerRequest> stripPrefix(int parts) {
return request -> {
MvcUtils.addOriginalRequestUrl(request, request.uri());
// TODO: gateway url attributes
String path = request.uri().getRawPath();
// TODO: begin duplicate code from StripPrefixGatewayFilterFactory
@@ -435,10 +437,22 @@ public abstract class BeforeFilterFunctions {
.replacePath(newPath.toString())
.build(true)
.toUri();
return ServerRequest.from(request).uri(prefixedUri).build();
};
}
public static Function<ServerRequest, ServerRequest> uri(String uri) {
return uri(URI.create(uri));
}
public static Function<ServerRequest, ServerRequest> uri(URI uri) {
return request -> {
MvcUtils.setRequestUrl(request, uri);
return request;
};
}
public static class FallbackHeadersConfig {
private String executionExceptionTypeHeaderName = CB_EXECUTION_EXCEPTION_TYPE;

View File

@@ -228,6 +228,15 @@ public interface FilterFunctions {
return ofResponseProcessor(AfterFilterFunctions.setStatus(statusCode));
}
static HandlerFilterFunction<ServerResponse, ServerResponse> uri(String uri) {
return ofRequestProcessor(BeforeFilterFunctions.uri(uri));
}
@Shortcut
static HandlerFilterFunction<ServerResponse, ServerResponse> uri(URI uri) {
return ofRequestProcessor(BeforeFilterFunctions.uri(uri));
}
class FilterSupplier extends SimpleFilterSupplier {
public FilterSupplier() {

View File

@@ -63,6 +63,8 @@ public abstract class LoadBalancerFilterFunctions {
public static HandlerFilterFunction<ServerResponse, ServerResponse> lb(String serviceId,
BiFunction<ServiceInstance, URI, URI> reconstructUriFunction) {
return (request, next) -> {
MvcUtils.addOriginalRequestUrl(request, request.uri());
LoadBalancerClientFactory clientFactory = getApplicationContext(request)
.getBean(LoadBalancerClientFactory.class);
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator

View File

@@ -40,7 +40,7 @@ public class LoadBalancerHandlerSupplier implements HandlerSupplier {
public static HandlerDiscoverer.Result lb(URI uri) {
// TODO: how to do something other than http
return new HandlerDiscoverer.Result(HandlerFunctions.http(),
return new HandlerDiscoverer.Result(HandlerFunctions.http(), Collections.emptyList(),
Collections.singletonList(LoadBalancerFilterFunctions.lb(uri.getHost())));
}

View File

@@ -25,6 +25,7 @@ import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.util.ObjectUtils;
@@ -397,18 +398,15 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
// - see XForwardedHeadersFilterTests, so first get uris, then extract paths
// and remove one from another if it's the ending part.
LinkedHashSet<URI> originalUris = null; // TODO:
// exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
URI requestUri = null; // TODO:
// exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
LinkedHashSet<URI> originalUris = MvcUtils.getAttribute(request,
MvcUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
URI requestUri = request.uri();
if (originalUris != null && requestUri != null) {
originalUris.forEach(originalUri -> {
if (originalUri != null && originalUri.getPath() != null) {
String prefix = originalUri.getPath();
// strip trailing slashes before checking if request path is end
// of original path
String originalUriPath = stripTrailingSlash(originalUri);

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.server.mvc.handler;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.config.RouteProperties;
import org.springframework.web.servlet.function.HandlerFilterFunction;
import org.springframework.web.servlet.function.HandlerFunction;
import org.springframework.web.servlet.function.ServerResponse;
class DefaultHandlerSupplier implements HandlerSupplier {
@Override
public Collection<Method> get() {
return Arrays.asList(getClass().getMethods());
}
public static HandlerDiscoverer.Result fn(RouteProperties routeProperties) {
// fn:fnName
return fn(routeProperties.getUri().getSchemeSpecificPart());
}
public static HandlerDiscoverer.Result fn(String functionName) {
return new HandlerDiscoverer.Result(HandlerFunctions.fn(functionName), Collections.emptyList(),
Collections.emptyList());
}
public static HandlerDiscoverer.Result forward(RouteProperties routeProperties) {
return forward(routeProperties.getId(), routeProperties.getUri());
}
public static HandlerDiscoverer.Result forward(String id, URI uri) {
return new HandlerDiscoverer.Result(HandlerFunctions.forward(uri.getPath()), Collections.emptyList());
}
public static HandlerDiscoverer.Result http(RouteProperties routeProperties) {
return http(routeProperties.getId(), routeProperties.getUri());
}
public static HandlerDiscoverer.Result http(String id, URI uri) {
HandlerFunction<ServerResponse> http = HandlerFunctions.http();
return getResult(id, uri, http);
}
public static HandlerDiscoverer.Result https(RouteProperties routeProperties) {
return https(routeProperties.getId(), routeProperties.getUri());
}
public static HandlerDiscoverer.Result https(String id, URI uri) {
return getResult(id, uri, HandlerFunctions.https());
}
public static HandlerDiscoverer.Result no(RouteProperties routeProperties) {
return no(routeProperties.getId(), routeProperties.getUri());
}
public static HandlerDiscoverer.Result no(String id, URI uri) {
return getResult(id, uri, HandlerFunctions.no());
}
// for properties
public static HandlerDiscoverer.Result stream(RouteProperties routeProperties) {
// stream:bindingName
return stream(routeProperties.getUri().getSchemeSpecificPart());
}
public static HandlerDiscoverer.Result stream(String bindingName) {
return new HandlerDiscoverer.Result(HandlerFunctions.stream(bindingName), Collections.emptyList(),
Collections.emptyList());
}
private static HandlerDiscoverer.Result getResult(String id, URI uri,
HandlerFunction<ServerResponse> handlerFunction) {
HandlerFilterFunction<ServerResponse, ServerResponse> setId = setIdFilter(id);
HandlerFilterFunction<ServerResponse, ServerResponse> setRequest = setRequestUrlFilter(uri);
return new HandlerDiscoverer.Result(handlerFunction, Arrays.asList(setId, setRequest), Collections.emptyList());
}
private static HandlerFilterFunction<ServerResponse, ServerResponse> setIdFilter(String id) {
return (request, next) -> {
MvcUtils.setRouteId(request, id);
return next.handle(request);
};
}
private static HandlerFilterFunction<ServerResponse, ServerResponse> setRequestUrlFilter(URI uri) {
return (request, next) -> {
MvcUtils.setRequestUrl(request, uri);
return next.handle(request);
};
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.gateway.server.mvc.handler;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@@ -50,9 +51,16 @@ final class FunctionHandlerRequestProcessingHelper {
}
@SuppressWarnings({ "rawtypes", "unchecked" })
static ServerResponse processRequest(ServerRequest request, FunctionInvocationWrapper function, Object argument,
boolean eventStream, List<String> ignoredHeaders, List<String> requestOnlyHeaders) {
return processRequest(request, function, argument, eventStream, ignoredHeaders, requestOnlyHeaders, null);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
static ServerResponse processRequest(ServerRequest request, FunctionInvocationWrapper function, Object argument,
boolean eventStream, List<String> ignoredHeaders, List<String> requestOnlyHeaders,
Map<String, String> additionalHeaders) {
if (argument == null) {
argument = "";
}
@@ -70,42 +78,29 @@ final class FunctionHandlerRequestProcessingHelper {
builder = builder.setHeader(FunctionHandlerHeaderUtils.HTTP_REQUEST_PARAM,
request.params().toSingleValueMap());
}
if (!CollectionUtils.isEmpty(additionalHeaders)) {
builder.copyHeaders(additionalHeaders);
}
inputMessage = builder.copyHeaders(headers.toSingleValueMap()).build();
if (function.isRoutingFunction()) {
function.setSkipOutputConversion(true);
}
if (logger.isDebugEnabled()) {
logger.debug("Sending request to " + function + " with argument: " + inputMessage);
}
Object result = function.apply(inputMessage);
if (function.isConsumer()) {
/*
* if (result instanceof Publisher) { Mono.from((Publisher)
* result).subscribe(); }
*/
return HttpMethod.DELETE.equals(request.method()) ? ServerResponse.ok().build()
: ServerResponse.accepted()
.headers(h -> h.addAll(sanitize(headers, ignoredHeaders, requestOnlyHeaders)))
.build();
// Mono.empty() :
// Mono.just(ResponseEntity.accepted().headers(FunctionHandlerHeaderUtils.sanitize(headers,
// ignoredHeaders, requestOnlyHeaders)).build());
}
BodyBuilder responseOkBuilder = ServerResponse.ok()
.headers(h -> h.addAll(sanitize(headers, ignoredHeaders, requestOnlyHeaders)));
// FIXME: Mono/Flux
/*
* Publisher pResult; if (result instanceof Publisher) { pResult = (Publisher)
* result; if (eventStream) { return Flux.from(pResult); }
*
* if (pResult instanceof Flux) { pResult = ((Flux) pResult).onErrorContinue((e,
* v) -> { logger.error("Failed to process value: " + v, (Throwable) e);
* }).collectList(); } pResult = Mono.from(pResult); } else { pResult =
* Mono.just(result); }
*/
// return Mono.from(pResult).map(v -> {
if (result instanceof Iterable i) {
List aggregatedResult = (List) StreamSupport.stream(i.spliterator(), false).map(m -> {
return m instanceof Message ? processMessage(responseOkBuilder, (Message<?>) m, ignoredHeaders) : m;
@@ -118,7 +113,6 @@ final class FunctionHandlerRequestProcessingHelper {
else {
return responseOkBuilder.body(result);
}
// });
}
private static Object processMessage(BodyBuilder responseOkBuilder, Message<?> message,

View File

@@ -37,20 +37,39 @@ public class HandlerDiscoverer extends AbstractGatewayDiscoverer {
private final HandlerFunction<ServerResponse> handlerFunction;
private final List<HandlerFilterFunction<ServerResponse, ServerResponse>> filters;
private final List<HandlerFilterFunction<ServerResponse, ServerResponse>> lowerPrecedenceFilters;
private final List<HandlerFilterFunction<ServerResponse, ServerResponse>> higherPrecedenceFilters;
@Deprecated
public Result(HandlerFunction<ServerResponse> handlerFunction,
List<HandlerFilterFunction<ServerResponse, ServerResponse>> filters) {
this(handlerFunction, Collections.emptyList(), filters);
}
public Result(HandlerFunction<ServerResponse> handlerFunction,
List<HandlerFilterFunction<ServerResponse, ServerResponse>> lowerPrecedenceFilters,
List<HandlerFilterFunction<ServerResponse, ServerResponse>> higherPrecedenceFilters) {
this.handlerFunction = handlerFunction;
this.filters = Objects.requireNonNullElse(filters, Collections.emptyList());
this.lowerPrecedenceFilters = Objects.requireNonNullElse(lowerPrecedenceFilters, Collections.emptyList());
this.higherPrecedenceFilters = Objects.requireNonNullElse(higherPrecedenceFilters, Collections.emptyList());
}
public HandlerFunction<ServerResponse> getHandlerFunction() {
return handlerFunction;
}
@Deprecated
public List<HandlerFilterFunction<ServerResponse, ServerResponse>> getFilters() {
return filters;
return getHigherPrecedenceFilters();
}
public List<HandlerFilterFunction<ServerResponse, ServerResponse>> getLowerPrecedenceFilters() {
return lowerPrecedenceFilters;
}
public List<HandlerFilterFunction<ServerResponse, ServerResponse>> getHigherPrecedenceFilters() {
return higherPrecedenceFilters;
}
}

View File

@@ -19,15 +19,21 @@ package org.springframework.cloud.gateway.server.mvc.handler;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import jakarta.servlet.ServletException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.FunctionProperties;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.context.config.RoutingFunction;
import org.springframework.cloud.gateway.server.mvc.GatewayMvcClassPathWarningAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.config.RouteProperties;
import org.springframework.cloud.stream.function.StreamOperations;
@@ -43,37 +49,58 @@ import static org.springframework.cloud.gateway.server.mvc.handler.FunctionHandl
public abstract class HandlerFunctions {
private static final Log log = LogFactory.getLog(GatewayMvcClassPathWarningAutoConfiguration.class);
private HandlerFunctions() {
}
// for properties
public static HandlerFunction<ServerResponse> fn(RouteProperties routeProperties) {
// fn:fnName
return fn(routeProperties.getUri().getSchemeSpecificPart());
}
public static HandlerFunction<ServerResponse> fn(String functionName) {
Assert.hasText(functionName, "'functionName' must not be empty");
return request -> {
String expandedFunctionName = MvcUtils.expand(request, functionName);
FunctionCatalog functionCatalog = MvcUtils.getApplicationContext(request).getBean(FunctionCatalog.class);
FunctionInvocationWrapper function = functionCatalog.lookup(expandedFunctionName,
request.headers().accept().stream().map(MimeType::toString).toArray(String[]::new));
String expandedFunctionName = MvcUtils.expand(request, functionName);
FunctionInvocationWrapper function;
Object body = null;
if (expandedFunctionName.contains("/")) {
String[] functionBodySplit = expandedFunctionName.split("/");
function = functionCatalog.lookup(functionBodySplit[0],
request.headers().accept().stream().map(MimeType::toString).toArray(String[]::new));
if (function != null && function.isSupplier()) {
log.warn("Supplier must not have any arguments. Supplier: '" + function.getFunctionDefinition()
+ "' has '" + functionBodySplit[1] + "' as an argument which is ignored.");
}
body = functionBodySplit[1];
}
else {
function = functionCatalog.lookup(expandedFunctionName,
request.headers().accept().stream().map(MimeType::toString).toArray(String[]::new));
}
/*
* If function can not be found in the current runtime, we will default to
* RoutingFunction which has additional logic to determine the function to
* invoke.
*/
Map<String, String> additionalRequestHeaders = new HashMap<>();
if (function == null) {
additionalRequestHeaders.put(FunctionProperties.FUNCTION_DEFINITION, expandedFunctionName);
function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME,
request.headers().accept().stream().map(MimeType::toString).toArray(String[]::new));
}
if (function != null) {
Object body = function.isSupplier() ? null : request.body(function.getRawInputType());
return processRequest(request, function, body, false, Collections.emptyList(), Collections.emptyList());
if (body == null) {
body = function.isSupplier() ? null : request.body(function.getRawInputType());
}
return processRequest(request, function, body, false, Collections.emptyList(), Collections.emptyList(),
additionalRequestHeaders);
}
return ServerResponse.notFound().build();
};
}
// for properties
public static HandlerFunction<ServerResponse> stream(RouteProperties routeProperties) {
// stream:bindingName
return stream(routeProperties.getUri().getSchemeSpecificPart());
}
public static HandlerFunction<ServerResponse> stream(String bindingName) {
Assert.hasText(bindingName, "'bindingName' must not be empty");
// TODO: validate bindingName
@@ -115,14 +142,17 @@ public abstract class HandlerFunctions {
// TODO: current discovery only goes by method name
// so last one wins, so put parameterless last
@Deprecated
public static HandlerFunction<ServerResponse> http(String uri) {
return http(URI.create(uri));
}
@Deprecated
public static HandlerFunction<ServerResponse> http(URI uri) {
return new LookupProxyExchangeHandlerFunction(uri);
}
@Deprecated
public static HandlerFunction<ServerResponse> https(URI uri) {
return new LookupProxyExchangeHandlerFunction(uri);
}
@@ -141,6 +171,7 @@ public abstract class HandlerFunctions {
static class LookupProxyExchangeHandlerFunction implements HandlerFunction<ServerResponse> {
@Deprecated
private final URI uri;
private AtomicReference<ProxyExchangeHandlerFunction> proxyExchangeHandlerFunction = new AtomicReference<>();
@@ -149,6 +180,7 @@ public abstract class HandlerFunctions {
this.uri = null;
}
@Deprecated
LookupProxyExchangeHandlerFunction(URI uri) {
this.uri = uri;
}
@@ -156,12 +188,15 @@ public abstract class HandlerFunctions {
@Override
public ServerResponse handle(ServerRequest serverRequest) {
if (uri != null) {
// TODO: in 2 places now, here and
// GatewayMvcPropertiesBeanDefinitionRegistrar
MvcUtils.putAttribute(serverRequest, MvcUtils.GATEWAY_REQUEST_URL_ATTR, uri);
// TODO: log warning of deprecated usage
MvcUtils.setRequestUrl(serverRequest, uri);
}
this.proxyExchangeHandlerFunction.compareAndSet(null, lookup(serverRequest));
return proxyExchangeHandlerFunction.get().handle(serverRequest);
return proxyExchangeHandlerFunction.updateAndGet(function -> {
if (function == null) {
return lookup(serverRequest);
}
return function;
}).handle(serverRequest);
}
private static ProxyExchangeHandlerFunction lookup(ServerRequest request) {
@@ -179,12 +214,13 @@ public abstract class HandlerFunctions {
}
@Deprecated
public static class HandlerSupplier
implements org.springframework.cloud.gateway.server.mvc.handler.HandlerSupplier {
@Override
public Collection<Method> get() {
return Arrays.asList(HandlerFunctions.class.getMethods());
return Collections.emptyList();
}
}

View File

@@ -23,7 +23,7 @@ org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\
org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.FilterSupplier
org.springframework.cloud.gateway.server.mvc.handler.HandlerSupplier=\
org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.HandlerSupplier,\
org.springframework.cloud.gateway.server.mvc.handler.DefaultHandlerSupplier,\
org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerHandlerSupplier
org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier=\

View File

@@ -1,4 +1,5 @@
org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration
org.springframework.cloud.gateway.server.mvc.GatewayMvcClassPathWarningAutoConfiguration
org.springframework.cloud.gateway.server.mvc.handler.GatewayMultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
org.springframework.cloud.gateway.server.mvc.config.DefaultFunctionConfiguration

View File

@@ -79,6 +79,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@@ -110,6 +111,7 @@ import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFu
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.requestHeaderToRequestUri;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.requestSize;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.routeId;
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions.rateLimit;
import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.circuitBreaker;
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeader;
@@ -140,7 +142,8 @@ import static org.springframework.web.servlet.function.RequestPredicates.POST;
import static org.springframework.web.servlet.function.RequestPredicates.path;
@SuppressWarnings("unchecked")
@SpringBootTest(properties = { "spring.cloud.gateway.mvc.http-client.type=jdk" },
@SpringBootTest(
properties = { "spring.cloud.gateway.mvc.http-client.type=jdk", "spring.cloud.gateway.function.enabled=false" },
webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
@ExtendWith(OutputCaptureExtension.class)
@@ -239,6 +242,7 @@ public class ServerMvcIntegrationTests {
public void stripPrefixWorks() {
restClient.get()
.uri("/long/path/to/get")
.header("Host", "www.stripprefix.org")
.exchange()
.expectStatus()
.isOk()
@@ -246,6 +250,13 @@ public class ServerMvcIntegrationTests {
.consumeWith(res -> {
Map<String, Object> map = res.getResponseBody();
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsKeys(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER);
assertThat(headers).containsEntry(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
"/long/path/to");
assertThat(headers).containsEntry("X-Test", "stripPrefix");
});
}
@@ -264,10 +275,40 @@ public class ServerMvcIntegrationTests {
Map<String, Object> map = res.getResponseBody();
assertThat(map).containsEntry("data", "hello");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsKeys(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER);
assertThat(headers).containsEntry(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
"/long/path/to");
assertThat(headers).containsEntry("X-Test", "stripPrefixPost");
});
}
@Test
public void stripPrefixLbWorks() {
restClient.get()
.uri("/long/path/to/get")
.header("Host", "www.stripprefixlb.org")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
Map<String, Object> map = res.getResponseBody();
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsKeys(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER);
assertThat(headers).containsEntry(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
"/long/path/to");
assertThat(headers).containsEntry("X-Test", "stripPrefixLb");
});
}
@Test
public void setStatusGatewayRouterFunctionWorks() {
restClient.get()
@@ -384,7 +425,8 @@ public class ServerMvcIntegrationTests {
public void circuitBreakerInvalidFallbackThrowsException() {
// @formatter:off
Assertions.assertThatThrownBy(() -> route("testcircuitbreakergatewayfallback")
.route(path("/anything/circuitbreakergatewayfallback"), http(URI.create("https://nonexistantdomain.com1234")))
.route(path("/anything/circuitbreakergatewayfallback"), http())
.before(uri("https://nonexistantdomain.com1234"))
.filter(circuitBreaker("mycb2", URI.create("http://example.com")))
.build()).isInstanceOf(IllegalArgumentException.class);
// @formatter:on
@@ -987,6 +1029,11 @@ public class ServerMvcIntegrationTests {
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
protected static class TestConfiguration {
@Bean
StaticPortController staticPortController() {
return new StaticPortController();
}
@Bean
TestHandler testHandler() {
return new TestHandler();
@@ -1067,8 +1114,8 @@ public class ServerMvcIntegrationTests {
// @formatter:off
return route("testsetpath")
.route(POST("/mycustompath{extra}").and(host("**.setpathpost.org")), http())
.filter(new HttpbinUriResolver())
.filter(setPath("/{extra}"))
.filter(new HttpbinUriResolver())
.build();
// @formatter:on
}
@@ -1076,11 +1123,12 @@ public class ServerMvcIntegrationTests {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsStripPrefix() {
// @formatter:off
return route(GET("/long/path/to/get"), http())
.filter(new HttpbinUriResolver())
return route("teststripprefix")
.route(GET("/long/path/to/get").and(host("**.stripprefix.org")), http())
.filter(stripPrefix(3))
.filter(addRequestHeader("X-Test", "stripPrefix"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "teststripprefix");
.filter(new HttpbinUriResolver())
.build();
// @formatter:on
}
@@ -1089,9 +1137,21 @@ public class ServerMvcIntegrationTests {
// @formatter:off
return route("teststripprefixpost")
.route(POST("/long/path/to/post").and(host("**.stripprefixpost.org")), http())
.filter(new HttpbinUriResolver())
.filter(stripPrefix(3))
.filter(addRequestHeader("X-Test", "stripPrefixPost"))
.filter(new HttpbinUriResolver())
.build();
// @formatter:on
}
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsStripPrefixLb() {
// @formatter:off
return route("teststripprefix")
.route(GET("/long/path/to/get").and(host("**.stripprefixlb.org")), http())
.filter(stripPrefix(3))
.filter(addRequestHeader("X-Test", "stripPrefixLb"))
.filter(lb("httpbin"))
.build();
// @formatter:on
}
@@ -1143,7 +1203,8 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerFallback() {
// @formatter:off
return route("testcircuitbreakerfallback")
.route(path("/anything/circuitbreakerfallback"), http(URI.create("https://nonexistantdomain.com1234")))
.route(path("/anything/circuitbreakerfallback"), http())
.before(uri("https://nonexistantdomain.com1234"))
.filter(circuitBreaker("mycb1", "/hello"))
.build();
// @formatter:on
@@ -1153,7 +1214,8 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerFallbackToGatewayRoute() {
// @formatter:off
return route("testcircuitbreakergatewayfallback")
.route(path("/anything/circuitbreakergatewayfallback"), http(URI.create("https://nonexistantdomain.com1234")))
.route(path("/anything/circuitbreakergatewayfallback"), http())
.before(uri("https://nonexistantdomain.com1234"))
.filter(circuitBreaker("mycb2", URI.create("forward:/anything/gatewayfallback")))
.build()
.and(route("testgatewayfallback")
@@ -1426,8 +1488,8 @@ public class ServerMvcIntegrationTests {
return route("requestheadertorequesturi")
.route(cloudFoundryRouteService().and(host("**.requestheadertorequesturi.org")), http())
//.before(new HttpbinUriResolver()) NO URI RESOLVER!
.before(requestHeaderToRequestUri("X-CF-Forwarded-Url"))
.filter(setPath("/hello"))
.before(requestHeaderToRequestUri("X-CF-Forwarded-Url"))
.build();
// @formatter:on
}
@@ -1644,6 +1706,16 @@ public class ServerMvcIntegrationTests {
}
@RestController
protected static class StaticPortController {
@GetMapping(path = "/anything/staticport", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> messageEvents() {
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build();
}
}
@RestController
protected static class EventController {

View File

@@ -53,7 +53,8 @@ import org.springframework.web.servlet.function.ServerRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.server.mvc.test.TestUtils.getMap;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "spring.cloud.gateway.function.enabled=false" })
@ActiveProfiles("propertiesbeandefinitionregistrartests")
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
@@ -122,7 +123,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
predicate.accept(new AbstractRequestPredicatesVisitor() {
@Override
public void path(String pattern) {
assertThat(pattern).isEqualTo("/anything/listRoute3");
assertThat(pattern).isEqualTo("/extra/anything/listRoute3");
}
@Override
@@ -181,7 +182,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
@SuppressWarnings("unchecked")
public void lbRouteWorks() {
restClient.get()
.uri("/anything/listRoute3")
.uri("/extra/anything/listRoute3")
.header("MyHeaderName", "MyHeaderVal")
.exchange()
.expectStatus()

View File

@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author raccoonback
* @author Jens Mallien
*/
class BeforeFilterFunctionsTests {
@@ -157,6 +158,18 @@ class BeforeFilterFunctionsTests {
assertThat(result.uri().toString()).hasToString("http://localhost/path/%C3%A9/last?foo=replacement");
}
@Test
void stripPrefixWithPort() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost:77/depth1/depth2/depth3")
.buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ServerRequest result = BeforeFilterFunctions.stripPrefix(2).apply(request);
assertThat(result.uri().toString()).hasToString("http://localhost:77/depth3");
}
@Test
void stripPrefixWithEncodedPath() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/depth1/depth2/depth3/é")
@@ -183,4 +196,53 @@ class BeforeFilterFunctionsTests {
assertThat(result.uri().toString()).hasToString("http://localhost/depth3?baz%5B%5D=qux%5B%5D");
}
@Test
void rewritePath() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get").buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ServerRequest modified = BeforeFilterFunctions.rewritePath("get", "modified").apply(request);
assertThat(modified.uri().getRawPath()).isEqualTo("/modified");
}
@Test
void rewritePathWithSpace() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get/path/with spaces")
.buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ServerRequest modified = BeforeFilterFunctions.rewritePath("get", "modified").apply(request);
assertThat(modified.uri().getRawPath()).isEqualTo("/modified/path/with%20spaces");
}
@Test
void rewritePathWithEnDash() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get/path/withendashes")
.buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ServerRequest modified = BeforeFilterFunctions.rewritePath("get", "modified").apply(request);
assertThat(modified.uri().getRawPath()).isEqualTo("/modified/path/with%E2%80%93en%E2%80%93dashes");
}
@Test
void rewritePathWithEnDashAndSpace() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders
.get("http://localhost/get/path/withendashes and spaces")
.buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ServerRequest modified = BeforeFilterFunctions.rewritePath("get", "modified").apply(request);
assertThat(modified.uri().getRawPath())
.isEqualTo("/modified/path/with%E2%80%93en%E2%80%93dashes%20and%20spaces");
}
}

View File

@@ -56,8 +56,8 @@ import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFun
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@SuppressWarnings("unchecked")
@SpringBootTest(properties = {}, webEnvironment = WebEnvironment.RANDOM_PORT)
@SpringBootTest(properties = { "spring.cloud.gateway.function.enabled=false" },
webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
public class RetryFilterFunctionTests {

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.server.mvc.filter;
import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.stripPrefix;
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.uri;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
import static org.springframework.cloud.gateway.server.mvc.test.TestUtils.getMap;
@SuppressWarnings("unchecked")
@SpringBootTest(properties = {}, webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("stripprefixstaticport")
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
public class StripPrefixStaticPortTests {
@Autowired
TestRestClient restClient;
@BeforeAll
static void beforeAll() {
HttpbinTestcontainers.initializeSystemProperties();
}
@Test
public void stripPrefixStaticPort() {
restClient.get()
.uri("/long/path/to/anything/staticport")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
Map<String, Object> map = res.getResponseBody();
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers)
.containsKeys(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER)
.containsEntry(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER, "/long/path/to");
});
}
@Test
public void stripPrefixStaticPortDsl() {
restClient.get()
.uri("/long/path/to/anything/staticportdsl")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
Map<String, Object> map = res.getResponseBody();
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers)
.containsKeys(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER)
.containsEntry(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER, "/long/path/to");
});
}
@SpringBootConfiguration
@EnableAutoConfiguration
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
protected static class TestConfiguration {
@Bean
StaticPortController staticPortController() {
return new StaticPortController();
}
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsStripPrefixStaticPortDsl(Environment env) {
// @formatter:off
return route("teststripprefixstaticportdsl")
.GET("/long/path/to/anything/staticportdsl", http())
.filter(uri(env.getProperty("strip.prefix.static.uri")))
.filter(stripPrefix(3))
.build();
// @formatter:on
}
}
@RestController
protected static class StaticPortController {
@GetMapping(path = "/anything/staticport", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> messageEvents() {
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build();
}
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.server.mvc.handler;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(properties = {}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DefaultRouteFunctionHandlerTests {
@Autowired
private TestRestClient restClient;
@Test
public void testSupplierWorks() {
restClient.get()
.uri("/hello")
.accept(MediaType.TEXT_PLAIN)
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("hello");
}
@Test
public void testFunctionWorksGET() {
restClient.get()
.uri("/upper/bob")
.accept(MediaType.TEXT_PLAIN)
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("BOB");
}
@Test
public void testFunctionWorksPOST() {
restClient.post()
.uri("/upper")
.accept(MediaType.APPLICATION_JSON)
.bodyValue("bob")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("BOB");
}
@Test
public void testConsumerWorksGET() {
restClient.get().uri("/consume/hello").accept(MediaType.TEXT_PLAIN).exchange().expectStatus().isAccepted();
assertThat(TestConfiguration.consumerInvoked).isTrue();
}
@Test
public void testConsumerWorksPOST() {
restClient.post()
.uri("/consume")
.accept(MediaType.APPLICATION_JSON)
.bodyValue("hello")
.exchange()
.expectStatus()
.isAccepted();
assertThat(TestConfiguration.consumerInvoked).isTrue();
}
@SpringBootConfiguration
@EnableAutoConfiguration
protected static class TestConfiguration {
static boolean consumerInvoked;
@Bean
Function<String, String> upper() {
return s -> s.toUpperCase(Locale.ROOT);
}
@Bean
Consumer<String> consume() {
return s -> {
consumerInvoked = false;
assertThat(s).isEqualTo("hello");
consumerInvoked = true;
};
}
@Bean
Supplier<String> hello() {
return () -> "hello";
}
}
}

View File

@@ -18,8 +18,6 @@ package org.springframework.cloud.gateway.server.mvc.test;
import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.utility.DockerImageName;
@@ -35,8 +33,6 @@ public class HttpbinTestcontainers implements ApplicationContextInitializer<Conf
// https://hub.docker.com/r/mccutchen/go-httpbin
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mccutchen/go-httpbin");
static final Logger logger = LoggerFactory.getLogger(HttpbinTestcontainers.class);
/**
* Default httpbin port.
*/
@@ -59,7 +55,6 @@ public class HttpbinTestcontainers implements ApplicationContextInitializer<Conf
MutablePropertySources sources = context.getEnvironment().getPropertySources();
if (!sources.contains("httpbinTestcontainer")) {
boolean running = container.isRunning();
Integer mappedPort = container.getMappedPort(DEFAULT_PORT);
HashMap<String, Object> map = new HashMap<>();
map.put("httpbin.port", String.valueOf(mappedPort));

View File

@@ -16,7 +16,9 @@
package org.springframework.cloud.gateway.server.mvc.test;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.function.Function;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
@@ -36,7 +38,15 @@ public class HttpbinUriResolver
String host = context.getEnvironment().getProperty("httpbin.host");
Assert.hasText(host, "httpbin.host is not set, did you initialize HttpbinTestcontainers?");
Assert.notNull(port, "httpbin.port is not set, did you initialize HttpbinTestcontainers?");
return URI.create(String.format("http://%s:%d", host, port));
URI original = request.uri();
try {
return new URI("http", original.getUserInfo(), host, port, original.getPath(), original.getQuery(),
original.getFragment());
}
catch (URISyntaxException e) {
throw new UndeclaredThrowableException(e);
}
}
@Override

View File

@@ -38,9 +38,12 @@ spring.cloud.gateway.mvc:
- id: listRoute3
uri: lb://httpbin
predicates:
- Path=/anything/listRoute3
- Path=/extra/anything/listRoute3
- Header=MyHeaderName,MyHeader.*
filters:
- name: StripPrefix
args:
parts: 1
- name: AddRequestHeader
args:
name: X-Test

View File

@@ -0,0 +1,12 @@
strip.prefix.static.uri: http://${httpbin.host}:${httpbin.port}
spring.cloud.gateway.mvc:
routes:
- id: strip_prefix_static_port_config
uri: ${strip.prefix.static.uri}
predicates:
- Path=/long/path/to/anything/staticport
filters:
- StripPrefix=3
logging:
level:
org.springframework.cloud.gateway.server.mvc: TRACE

View File

@@ -317,8 +317,12 @@ public class GatewayAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.forwarded.enabled", matchIfMissing = true)
public ForwardedHeadersFilter forwardedHeadersFilter() {
return new ForwardedHeadersFilter();
public ForwardedHeadersFilter forwardedHeadersFilter(Environment env, ServerProperties serverProperties) {
boolean forwardedByEnabled = env.getProperty("spring.cloud.gateway.forwarded.by.enabled", Boolean.class, false);
ForwardedHeadersFilter forwardedHeadersFilter = new ForwardedHeadersFilter();
forwardedHeadersFilter.setForwardedByEnabled(forwardedByEnabled);
forwardedHeadersFilter.setServerPort(serverProperties.getPort());
return forwardedHeadersFilter;
}
// HttpHeaderFilter beans

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.cloud.gateway.event.EnableBodyCachingEvent;
import org.springframework.cloud.gateway.support.AbstractConfigurable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
@@ -43,6 +44,13 @@ public abstract class AbstractGatewayFilterFactory<C> extends AbstractConfigurab
return this.publisher;
}
protected void enableBodyCaching(String routeId) {
if (routeId != null && getPublisher() != null) {
// send an event to enable caching
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
}
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;

View File

@@ -30,17 +30,18 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.retry.Backoff;
import reactor.retry.Jitter;
import reactor.retry.Repeat;
import reactor.retry.RepeatContext;
import reactor.retry.Retry;
import reactor.retry.RetryContext;
import org.springframework.cloud.gateway.event.EnableBodyCachingEvent;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.HasRouteId;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.cloud.gateway.support.TimeoutException;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatus.Series;
@@ -70,7 +71,7 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("retries", "statuses", "methods", "backoff.firstBackoff", "backoff.maxBackoff",
"backoff.factor", "backoff.basedOnPreviousValue");
"backoff.factor", "backoff.basedOnPreviousValue", "jitter.randomFactor", "timeout");
}
@Override
@@ -124,10 +125,16 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
if (backoff != null) {
statusCodeRepeat = statusCodeRepeat.backoff(getBackoff(backoff));
}
JitterConfig jitter = retryConfig.getJitter();
if (jitter != null) {
statusCodeRepeat = statusCodeRepeat.jitter(getJitter(jitter));
}
Duration timeout = retryConfig.getTimeout();
if (timeout != null) {
statusCodeRepeat = statusCodeRepeat.timeout(timeout);
}
}
// TODO: support timeout, backoff, jitter, etc... in Builder
Retry<ServerWebExchange> exceptionRetry = null;
if (!retryConfig.getExceptions().isEmpty()) {
Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
@@ -163,6 +170,14 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
if (backoff != null) {
exceptionRetry = exceptionRetry.backoff(getBackoff(backoff));
}
JitterConfig jitter = retryConfig.getJitter();
if (jitter != null) {
exceptionRetry = exceptionRetry.jitter(getJitter(jitter));
}
Duration timeout = retryConfig.getTimeout();
if (timeout != null) {
exceptionRetry = exceptionRetry.timeout(timeout);
}
}
GatewayFilter gatewayFilter = apply(retryConfig.getRouteId(), statusCodeRepeat, exceptionRetry);
@@ -180,6 +195,9 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
.append("statuses", retryConfig.getStatuses())
.append("methods", retryConfig.getMethods())
.append("exceptions", retryConfig.getExceptions())
.append("backoff", retryConfig.getBackoff())
.append("jitter", retryConfig.getJitter())
.append("timeout", retryConfig.getTimeout())
.toString();
}
};
@@ -204,6 +222,10 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
backoff.basedOnPreviousValue);
}
private Jitter getJitter(JitterConfig jitter) {
return Jitter.random(jitter.randomFactor);
}
public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) {
Integer iteration = exchange.getAttribute(RETRY_ITERATION_KEY);
@@ -229,10 +251,7 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
}
public GatewayFilter apply(String routeId, Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) {
if (routeId != null && getPublisher() != null) {
// send an event to enable caching
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
}
enableBodyCaching(routeId);
return (exchange, chain) -> {
trace("Entering retry-filter");
@@ -295,6 +314,10 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
private BackoffConfig backoff;
private JitterConfig jitter;
private Duration timeout;
public RetryConfig allMethods() {
return setMethods(HttpMethod.values());
}
@@ -307,6 +330,35 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
if (this.backoff != null) {
this.backoff.validate();
}
if (this.jitter != null) {
this.jitter.validate();
}
if (this.timeout != null) {
Assert.isTrue(!timeout.isNegative(), "timeout should be >= 0");
}
}
public Duration getTimeout() {
return timeout;
}
public RetryConfig setTimeout(Duration timeout) {
this.timeout = timeout;
return this;
}
public JitterConfig getJitter() {
return jitter;
}
public RetryConfig setJitter(JitterConfig jitter) {
this.jitter = jitter;
return this;
}
public RetryConfig setJitter(double randomFactor) {
this.jitter = new JitterConfig(randomFactor);
return this;
}
public BackoffConfig getBackoff() {
@@ -437,6 +489,47 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
this.basedOnPreviousValue = basedOnPreviousValue;
}
@Override
public String toString() {
return new ToStringCreator(this).append("firstBackoff", firstBackoff)
.append("maxBackoff", maxBackoff)
.append("factor", factor)
.append("basedOnPreviousValue", basedOnPreviousValue)
.toString();
}
}
public static class JitterConfig {
private double randomFactor = 0.5;
public void validate() {
Assert.isTrue(randomFactor >= 0 && randomFactor <= 1,
"random factor must be between 0 and 1 (default 0.5)");
}
public JitterConfig() {
}
public JitterConfig(double randomFactor) {
this.randomFactor = randomFactor;
}
public double getRandomFactor() {
return randomFactor;
}
public void setRandomFactor(double randomFactor) {
this.randomFactor = randomFactor;
}
@Override
public String toString() {
return new ToStringCreator(this).append("randomFactor", randomFactor).toString();
}
}
}

View File

@@ -89,6 +89,9 @@ public abstract class SpringCloudCircuitBreakerFilterFactory
@Override
public GatewayFilter apply(Config config) {
if (config.getFallbackUri() != null) {
enableBodyCaching(config.getRouteId());
}
ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(config.getId());
Set<HttpStatus> statuses = config.getStatusCodes()
.stream()

View File

@@ -20,11 +20,15 @@ import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -34,8 +38,18 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Olga Maciaszek-Sharma
* @author Tillmann Heigel
*/
public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
private Integer serverPort;
private final Log logger = LogFactory.getLog(getClass());
private boolean forwardedByEnabled = false;
/**
* Forwarded header.
*/
@@ -84,6 +98,14 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
return result;
}
public void setForwardedByEnabled(boolean forwardedByEnabled) {
this.forwardedByEnabled = forwardedByEnabled;
}
public void setServerPort(Integer serverPort) {
this.serverPort = serverPort;
}
@Override
public int getOrder() {
return 0;
@@ -134,13 +156,38 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
}
forwarded.put("for", forValue);
}
// TODO: support by?
if (forwardedByEnabled) {
addForwardedByHeader(forwarded);
}
updated.add(FORWARDED_HEADER, forwarded.toHeaderValue());
return updated;
}
private void addForwardedByHeader(Forwarded forwarded) {
try {
addForwardedBy(forwarded, InetAddress.getLocalHost());
}
catch (UnknownHostException e) {
this.logger.warn("Can not resolve host address, skipping Forwarded 'by' header", e);
}
}
/* visible for testing */ void addForwardedBy(Forwarded forwarded, InetAddress localAddress) {
if (localAddress != null) {
String byValue = localAddress.getHostAddress();
if (localAddress instanceof Inet6Address) {
byValue = "[" + byValue + "]";
}
if (serverPort != null && serverPort > 0) {
byValue = byValue + ":" + serverPort;
}
forwarded.put("by", byValue);
}
}
/* for testing */ static class Forwarded {
private static final char EQUALS = '=';

View File

@@ -389,6 +389,12 @@
"description": "Enables the ForwardedHeadersFilter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.forwarded.by.enabled",
"type": "java.lang.Boolean",
"description": "Enables the Forwarded: by header part.",
"defaultValue": "false"
},
{
"name": "spring.cloud.gateway.httpserver.wiretap",
"type": "java.lang.Boolean",

View File

@@ -109,6 +109,30 @@ public class RetryGatewayFilterFactoryIntegrationTests extends BaseWebClientTest
// @formatter:on
}
@Test
public void retryWithBackoffJitterTimeout() {
// @formatter:off
testClient.get()
.uri("/retry?key=retry-with-backoff-jitter-timeout&count=3")
.header(HttpHeaders.HOST, "www.retrywithbackoffjittertimeout.org")
.exchange()
.expectStatus().isOk()
.expectHeader().value("X-Retry-Count", CoreMatchers.equalTo("3"));
// @formatter:on
}
@Test
public void retryWithBackoffTimeout() {
// backoff > timeout
testClient.get()
.uri("/retry?key=retry-with-backoff-timeout&count=3")
.header(HttpHeaders.HOST, "www.retrywithbackofftimeout.org")
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(TestConfig.map.get("retry-with-backoff-timeout")).isNotNull().hasValue(2);
}
@Test
public void retryFilterGetJavaDsl() {
testClient.get()
@@ -363,6 +387,21 @@ public class RetryGatewayFilterFactoryIntegrationTests extends BaseWebClientTest
r -> r.host("**.retrywithbackoff.org").filters(f -> f.prefixPath("/httpbin").retry(config -> {
config.setRetries(2).setBackoff(Duration.ofMillis(100), null, 2, true);
})).uri(uri))
.route("retry_with_backoff_jitter_timeout_test", r -> r.host("**.retrywithbackoffjittertimeout.org")
.filters(f -> f.prefixPath("/httpbin").retry(config -> {
config.setRetries(3)
.setBackoff(Duration.ofMillis(50), Duration.ofMillis(100), 2, true)
.setJitter(0.1)
.setTimeout(Duration.ofMillis(1000));
}))
.uri(uri))
.route("retry_with_backoff_timeout_test", r -> r.host("**.retrywithbackofftimeout.org")
.filters(f -> f.prefixPath("/httpbin").retry(config -> {
config.setRetries(3)
.setBackoff(Duration.ofMillis(100), null, 2, true)
.setTimeout(Duration.ofMillis(200));
}))
.uri(uri))
.route("retry_with_loadbalancer",
r -> r.host("**.retrywithloadbalancer.org")

View File

@@ -21,6 +21,7 @@ import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.BodyInserters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.http.MediaType.APPLICATION_JSON;
@@ -243,4 +244,17 @@ public abstract class SpringCloudCircuitBreakerFilterFactoryTests extends BaseWe
.valueEquals(ROUTE_ID_HEADER, "circuitbreaker_resume_without_error");
}
@Test
public void filterPostFallback() {
testClient.post()
.uri("/post")
.body(BodyInserters.fromValue("hello"))
.header("Host", "www.circuitbreakerfallbackpost.org")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.json("{\"body\":\"hello\"}");
}
}

View File

@@ -38,6 +38,8 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -68,6 +70,11 @@ public class SpringCloudCircuitBreakerTestConfig {
return Collections.singletonMap("from", "circuitbreakerfallbackcontroller");
}
@PostMapping("/circuitbreakerPostFallbackController")
public Map<String, String> postFallbackController(@RequestBody String body) {
return Collections.singletonMap("body", body);
}
@GetMapping("/circuitbreakerUriFallbackController/**")
public Map<String, String> uriFallbackcontroller(ServerWebExchange exchange, @RequestParam("a") String a) {
return Collections.singletonMap("uri", exchange.getRequest().getURI().toString());

View File

@@ -25,6 +25,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter.Forwarded;
@@ -201,4 +202,43 @@ public class ForwardedHeadersFilterTests {
}
}
@Test
public void forwardedByForIpv4AddressIsAdded() throws UnknownHostException {
Forwarded forwarded = new Forwarded();
InetAddress ipv4Address = InetAddress.getByName("216.103.69.111");
ForwardedHeadersFilter forwardedHeadersFilter = new ForwardedHeadersFilter();
forwardedHeadersFilter.setForwardedByEnabled(true);
forwardedHeadersFilter.addForwardedBy(forwarded, ipv4Address);
Assertions.assertThat(forwarded.getValues()).containsEntry("by", "216.103.69.111");
}
@Test
public void forwardedByForIpv6AddressIsAdded() throws UnknownHostException {
Forwarded forwarded = new Forwarded();
InetAddress ipv6Address = InetAddress.getByName("abc4:babf:955f:1724:11bc:0153:275c:d36e");
ForwardedHeadersFilter forwardedHeadersFilter = new ForwardedHeadersFilter();
forwardedHeadersFilter.setForwardedByEnabled(true);
forwardedHeadersFilter.addForwardedBy(forwarded, ipv6Address);
Assertions.assertThat(forwarded.getValues())
.containsEntry("by", "\"[abc4:babf:955f:1724:11bc:153:275c:d36e]\"");
}
@Test
public void forwardedByIsNotAddedIfFeatureIsDisabled() throws UnknownHostException {
Forwarded forwarded = new Forwarded();
InetAddress ipv4Address = InetAddress.getByName("216.103.69.111");
ForwardedHeadersFilter forwardedHeadersFilter = new ForwardedHeadersFilter();
forwardedHeadersFilter.setForwardedByEnabled(false);
forwardedHeadersFilter.addForwardedBy(forwarded, ipv4Address);
Assertions.assertThat(forwarded.getValues()).containsEntry("by", "216.103.69.111");
}
}

View File

@@ -44,7 +44,7 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G
public class XForwardedHeadersFilterTests {
@Test
public void remoteAddressIsNull() throws Exception {
public void remoteAddressIsNull() {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.HOST, "myhost")
.build();

View File

@@ -54,7 +54,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Denis Cutic
* @author Andrey Muchnik
*/
@SpringBootTest(webEnvironment = RANDOM_PORT)
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = { "spring.cloud.gateway.function.enabled=false" })
@DirtiesContext
@Testcontainers
@Tag("DockerRequired")

View File

@@ -62,7 +62,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.cloud.gateway.test.TestUtils.getMap;
@SpringBootTest(webEnvironment = RANDOM_PORT)
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "spring.cloud.gateway.forwarded.by.enabled=true")
@DirtiesContext
@SuppressWarnings("unchecked")
@ExtendWith(OutputCaptureExtension.class)
@@ -125,7 +125,8 @@ class GatewayIntegrationTests extends BaseWebClientTests {
assertThat(headers.get(ForwardedHeadersFilter.FORWARDED_HEADER)).asString()
.contains("proto=http")
.contains("host=\"localhost:")
.contains("for=\"127.0.0.1:");
.contains("for=\"127.0.0.1:")
.contains("by=");
assertThat(headers.get(XForwardedHeadersFilter.X_FORWARDED_HOST_HEADER)).asString()
.isEqualTo("localhost:" + this.port);
assertThat(headers.get(XForwardedHeadersFilter.X_FORWARDED_PORT_HEADER)).asString()

View File

@@ -32,91 +32,91 @@ import java.net.URI
@SpringBootTest(classes = [Config::class])
class RouteDslTests {
@Autowired
lateinit var builder: RouteLocatorBuilder
@Autowired
lateinit var builder: RouteLocatorBuilder
@Test
fun sampleRouteDsl() {
val routeLocator = builder.routes {
route(id = "test") {
host("**.abc.org") and path("/image/png")
filters {
addResponseHeader("X-TestHeader", "foobar")
}
uri("http://httpbin.org:80")
}
@Test
fun sampleRouteDsl() {
val routeLocator = builder.routes {
route(id = "test") {
host("**.abc.org") and path("/image/png")
filters {
addResponseHeader("X-TestHeader", "foobar")
}
uri("http://httpbin.org:80")
}
route(id = "test2") {
path("/image/webp") or path("/image/anotherone")
filters {
addResponseHeader("X-AnotherHeader", "baz")
addResponseHeader("X-AnotherHeader-2", "baz-2")
}
uri("https://httpbin.org:443")
}
}
route(id = "test2") {
path("/image/webp") or path("/image/anotherone")
filters {
addResponseHeader("X-AnotherHeader", "baz")
addResponseHeader("X-AnotherHeader-2", "baz-2")
}
uri("https://httpbin.org:443")
}
}
StepVerifier
.create(routeLocator.routes)
.expectNextMatches({
it.id == "test" && it.filters.size == 1 && it.uri == URI.create("http://httpbin.org:80")
})
.expectNextMatches({
it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443")
})
.expectComplete()
.verify()
StepVerifier
.create(routeLocator.routes)
.expectNextMatches({
it.id == "test" && it.filters.size == 1 && it.uri == URI.create("http://httpbin.org:80")
})
.expectNextMatches({
it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443")
})
.expectComplete()
.verify()
val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp")
.header("Host", "test.abc.org").build())
val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp")
.header("Host", "test.abc.org").build())
val filteredRoutes = routeLocator.routes.filter({
sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id)
it.predicate.apply(sampleExchange).toMono().block()
})
val filteredRoutes = routeLocator.routes.filter({
sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id)
it.predicate.apply(sampleExchange).toMono().block()
})
StepVerifier.create(filteredRoutes)
.expectNextMatches({
it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443")
})
.expectComplete()
.verify()
}
StepVerifier.create(filteredRoutes)
.expectNextMatches({
it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443")
})
.expectComplete()
.verify()
}
@Test
fun dslWithFunctionParameters() {
val routerLocator = builder.routes {
route(id = "test1", order = 10, uri = "http://httpbin.org") {
host("**.abc.org")
}
route(id = "test2", order = 10, uri = "http://someurl") {
host("**.abc.org")
uri("http://override-url")
}
}
@Test
fun dslWithFunctionParameters() {
val routerLocator = builder.routes {
route(id = "test1", order = 10, uri = "http://httpbin.org") {
host("**.abc.org")
}
route(id = "test2", order = 10, uri = "http://someurl") {
host("**.abc.org")
uri("http://override-url")
}
}
StepVerifier.create(routerLocator.routes)
.expectNextMatches({
it.id == "test1" &&
it.uri == URI.create("http://httpbin.org:80") &&
it.order == 10 &&
it.predicate.apply(MockServerWebExchange
.from(MockServerHttpRequest
.get("/someuri").header("Host", "test.abc.org")))
.toMono().block()
})
.expectNextMatches({
it.id == "test2" &&
it.uri == URI.create("http://override-url:80") &&
it.order == 10 &&
it.predicate.apply(MockServerWebExchange
.from(MockServerHttpRequest
.get("/someuri").header("Host", "test.abc.org")))
.toMono().block()
})
.expectComplete()
.verify()
}
StepVerifier.create(routerLocator.routes)
.expectNextMatches({
it.id == "test1" &&
it.uri == URI.create("http://httpbin.org:80") &&
it.order == 10 &&
it.predicate.apply(MockServerWebExchange
.from(MockServerHttpRequest
.get("/someuri").header("Host", "test.abc.org")))
.toMono().block()
})
.expectNextMatches({
it.id == "test2" &&
it.uri == URI.create("http://override-url:80") &&
it.order == 10 &&
it.predicate.apply(MockServerWebExchange
.from(MockServerHttpRequest
.get("/someuri").header("Host", "test.abc.org")))
.toMono().block()
})
.expectComplete()
.verify()
}
}
@Configuration(proxyBeanMethods = false)

View File

@@ -104,6 +104,19 @@ spring:
name: fallbackcmd
fallbackUri: forward:/circuitbreakerFallbackController
# =====================================
- id: circuitbreaker_fallback_test_post
uri: ${test.uri}
predicates:
- Host=**.circuitbreakerfallbackpost.org
filters:
- name: CircuitBreaker
args:
name: fallbackcmd
statusCodes:
- 200
fallbackUri: forward:/circuitbreakerPostFallbackController
# =====================================
- id: circuitbreaker_fallback_test_variables
uri: ${test.uri}