diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc index b9b9abe5..b593ac00 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc @@ -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 ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/httpheadersfilters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/httpheadersfilters.adoc index e2a6ac82..49a227e1 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/httpheadersfilters.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/httpheadersfilters.adoc @@ -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]. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheader.adoc index 61974ef4..0b0921a1 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheader.adoc @@ -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 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 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}")); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheadersifnotpresent.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheadersifnotpresent.adoc index b4455a6b..fda40278 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheadersifnotpresent.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestheadersifnotpresent.adoc @@ -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 gatewayRouterFunctionsAddReqHeader() { - return route(GET("/red"), http("https://example.org")) - .before(addRequestHeadersIfNotPresent("X-Request-Color-1:blue","X-Request-Color-2:green")); + public RouterFunction 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 gatewayRouterFunctionsAddReqHeader() { - return route(GET("/red/{segment}"), http("https://example.org")) - .before(addRequestHeadersIfNotPresent("X-Request-red", "blue-{segment}")); + public RouterFunction 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} diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestparameter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestparameter.adoc index 6ce332b4..53d306ad 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestparameter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addrequestparameter.adoc @@ -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 gatewayRouterFunctionsAddReqHeader() { - return route("add_request_parameter_route") - .GET("/anything/addrequestparam", http("https://example.org")) - .before(addRequestParameter("red", "blue")) - .build(); + public RouterFunction 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 gatewayRouterFunctionsAddReqHeader() { - return route("add_request_parameter_route") - .route(host("{segment}.myhost.org"), http("https://example.org")) - .before(addRequestParameter("foo", "bar-{segment}")) - .build(); + public RouterFunction gatewayRouterFunctionsAddReqParameter() { + return route("add_request_parameter_route") + .route(host("{segment}.myhost.org"), http()) + .before(uri("https://example.org")) + .before(addRequestParameter("foo", "bar-{segment}")) + .build(); } } ---- \ No newline at end of file diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addresponseheader.adoc index e04ed408..0c678abd 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addresponseheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/addresponseheader.adoc @@ -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 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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc index 8e16c8f3..3b80b1e1 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/circuitbreaker-filter.adoc @@ -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 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 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 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()); } } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/deduperesponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/deduperesponseheader.adoc index 9659f2cf..5f8a7a23 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/deduperesponseheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/deduperesponseheader.adoc @@ -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 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(); } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc index 3b867001..24158600 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/fallback-headers.adoc @@ -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()); } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/loadbalancer.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/loadbalancer.adoc index 58859f9c..2135aaf2 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/loadbalancer.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/loadbalancer.adoc @@ -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`. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/maprequestheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/maprequestheader.adoc index 2f8f30b6..e85d08b9 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/maprequestheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/maprequestheader.adoc @@ -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 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(); } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyrequestbody.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyrequestbody.adoc index 963b26f0..a18dbff1 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyrequestbody.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyrequestbody.adoc @@ -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 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 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) { } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyresponsebody.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyresponsebody.adoc index eacfd095..da97b6e6 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyresponsebody.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/modifyresponsebody.adoc @@ -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 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 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 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(); } } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/prefixpath.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/prefixpath.adoc index 2313b6e5..5a4d1343 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/prefixpath.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/prefixpath.adoc @@ -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 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. diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/preservehostheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/preservehostheader.adoc index 518c0ac3..462cf01f 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/preservehostheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/preservehostheader.adoc @@ -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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/ratelimiter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/ratelimiter.adoc index cfde996c..7fa31982 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/ratelimiter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/ratelimiter.adoc @@ -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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/redirectto.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/redirectto.adoc index f1816d36..69f2d538 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/redirectto.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/redirectto.adoc @@ -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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestheader.adoc index c85e8d9a..03150906 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestheader.adoc @@ -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 gatewayRouterFunctionsremoveRequestHeader() { - return route("removerequestheader_route") - .GET("/**", http("https://example.org")) - .before(removeRequestHeader("X-Request-Foo")) - .build(); + public RouterFunction gatewayRouterFunctionsRemoveRequestHeader() { + return route("removerequestheader_route") + .GET("/**", http()) + .before(uri("https://example.org")) + .before(removeRequestHeader("X-Request-Foo")) + .build(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestparameter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestparameter.adoc index 5ff79940..e60eaa9d 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestparameter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removerequestparameter.adoc @@ -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 gatewayRouterFunctionsAddReqHeader() { - return route("removerequestparameter_route") - .GET("/**", http("https://example.org")) - .before(removeRequestParameter("red")) - .build(); + public RouterFunction gatewayRouterFunctionsRemoveRequestParameter() { + return route("removerequestparameter_route") + .GET("/**", http()) + .before(uri("https://example.org")) + .before(removeRequestParameter("red")) + .build(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removeresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removeresponseheader.adoc index fda397b1..d767aba0 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removeresponseheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/removeresponseheader.adoc @@ -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 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(); } } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestheadersize.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestheadersize.adoc index 5f8f4066..e94b2ff0 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestheadersize.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestheadersize.adoc @@ -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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestsize.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestsize.adoc index 29135ea8..7eb84314 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestsize.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/requestsize.adoc @@ -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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/retry.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/retry.adoc index 9bf2bf99..ab8d3fa6 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/retry.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/retry.adoc @@ -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 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 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] diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritelocationresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritelocationresponseheader.adoc index 28918320..6af573a1 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritelocationresponseheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritelocationresponseheader.adoc @@ -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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritepath.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritepath.adoc index 7247db46..d423af39 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritepath.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewritepath.adoc @@ -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 gatewayRouterFunctionsRewritePath() { - return route("rewritepath_route") - .GET("/red/**", http("https://example.org")) - .before(rewritePath("/red/(?.*)", "/${segment}")) - .build(); + return route("rewritepath_route") + .GET("/red/**", http()) + .before(uri("https://example.org")) + .before(rewritePath("/red/(?.*)", "/${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. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriterequestparameter.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriterequestparameter.adoc index ba7c38c5..7ad61594 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriterequestparameter.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriterequestparameter.adoc @@ -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 gatewayRouterFunctionsAddReqHeader() { - return route("add_request_parameter_route") - .GET("/anything/addrequestparam", http("https://example.org")) - .before(addRequestParameter("red", "blue")) - .build(); + public RouterFunction gatewayRouterFunctionsRewriteRequestParameter() { + return route("rewriterequestparameter_route") + .GET("products", http()) + .before(uri("https://example.org")) + .before(addRequestParameter("red", "blue")) + .build(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriteresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriteresponseheader.adoc index 7ba0f9b0..7e0cb2c9 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriteresponseheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/rewriteresponseheader.adoc @@ -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 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(); } } diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setpath.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setpath.adoc index 261345e6..73624a70 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setpath.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setpath.adoc @@ -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 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. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequestheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequestheader.adoc index f131486a..a2c7676b 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequestheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequestheader.adoc @@ -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 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 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(); } } ---- \ No newline at end of file diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequesthostheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequesthostheader.adoc index 3f8faad9..d12b23c6 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequesthostheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setrequesthostheader.adoc @@ -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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setresponseheader.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setresponseheader.adoc index ee7698e9..55375546 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setresponseheader.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setresponseheader.adoc @@ -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 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 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(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setstatus.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setstatus.adoc index c3022b52..bd9bbe25 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setstatus.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/setstatus.adoc @@ -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 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()); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/stripprefix.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/stripprefix.adoc index ee2d2efa..902974ce 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/stripprefix.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/stripprefix.adoc @@ -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 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. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc index 2bf618aa..086bd3eb 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc @@ -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 gatewayRouterFunctionsAddReqHeader() { - return route("resource") - .GET("/resource", http("http://localhost:9000")) - .filter(tokenRelay()) - .build(); + public RouterFunction gatewayRouterFunctionsTokenRelay() { + return route("resource") + .GET("/resource", http()) + .before(uri("https://localhost:9000")) + .filter(tokenRelay()) + .build(); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/gateway-request-predicates.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/gateway-request-predicates.adoc index 1bca61e6..5631cbf8 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/gateway-request-predicates.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/gateway-request-predicates.adoc @@ -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 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 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 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 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 gatewayRouterFunctionsCookie() { - return route("cookie_route") - .route(header("X-Request-Id", "\\d+"), http("https://example.org")) - .build(); + public RouterFunction 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 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 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 gatewayRouterFunctionsMethod() { - return route("method_route") - .GET("/mypath", http("https://example.org")) - .build(); + public RouterFunction 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 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 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 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 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()); } } ---- diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/java-routes-api.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/java-routes-api.adoc index d15b79e0..f653d36e 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/java-routes-api.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/java-routes-api.adoc @@ -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 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 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`. 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`. 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. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/writing-custom-predicates-and-filters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/writing-custom-predicates-and-filters.adoc index 6735aa08..597b00fe 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/writing-custom-predicates-and-filters.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/writing-custom-predicates-and-filters.adoc @@ -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 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 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 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 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(); } } ---- diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index a8eed641..ae7609d3 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -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. diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml index 461e4c70..85b3c76d 100644 --- a/spring-cloud-gateway-integration-tests/pom.xml +++ b/spring-cloud-gateway-integration-tests/pom.xml @@ -19,7 +19,7 @@ 4.3.0-SNAPSHOT .. - + grpc http2 diff --git a/spring-cloud-gateway-server-mvc/pom.xml b/spring-cloud-gateway-server-mvc/pom.xml index 6f7ff6b3..e9f6b80c 100644 --- a/spring-cloud-gateway-server-mvc/pom.xml +++ b/spring-cloud-gateway-server-mvc/pom.xml @@ -136,4 +136,4 @@ test - \ No newline at end of file + diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java index 72f30289..05c073ed 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java @@ -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 urls = (LinkedHashSet) request.attributes() + .computeIfAbsent(GATEWAY_ORIGINAL_REQUEST_URL_ATTR, s -> new LinkedHashSet<>()); + urls.add(url); + } + private record ByteArrayInputMessage(ServerRequest request, ByteArrayInputStream body) implements HttpInputMessage { @Override diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/DefaultFunctionConfiguration.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/DefaultFunctionConfiguration.java new file mode 100644 index 00000000..c90e3e58 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/DefaultFunctionConfiguration.java @@ -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 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 + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/RouterFunctionHolderFactory.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/RouterFunctionHolderFactory.java index 687bdec8..6bc52a52 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/RouterFunctionHolderFactory.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/RouterFunctionHolderFactory.java @@ -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 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 handlerFunction = null; + + // filters added by HandlerDiscoverer need to go last, so save them + List> lowerPrecedenceFilters = new ArrayList<>(); + List> higherPrecedenceFilters = new ArrayList<>(); if (response instanceof HandlerFunction) { handlerFunction = (HandlerFunction) 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 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(); diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java index a6665d06..6cd2eeed 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java @@ -189,6 +189,7 @@ public abstract class BeforeFilterFunctions { final UriTemplate uriTemplate = new UriTemplate(prefix); return request -> { + MvcUtils.addOriginalRequestUrl(request, request.uri()); Map 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 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 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 uri(String uri) { + return uri(URI.create(uri)); + } + + public static Function uri(URI uri) { + return request -> { + MvcUtils.setRequestUrl(request, uri); + return request; + }; + } + public static class FallbackHeadersConfig { private String executionExceptionTypeHeaderName = CB_EXECUTION_EXCEPTION_TYPE; diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java index c7cb910f..55b6602c 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java @@ -228,6 +228,15 @@ public interface FilterFunctions { return ofResponseProcessor(AfterFilterFunctions.setStatus(statusCode)); } + static HandlerFilterFunction uri(String uri) { + return ofRequestProcessor(BeforeFilterFunctions.uri(uri)); + } + + @Shortcut + static HandlerFilterFunction uri(URI uri) { + return ofRequestProcessor(BeforeFilterFunctions.uri(uri)); + } + class FilterSupplier extends SimpleFilterSupplier { public FilterSupplier() { diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/LoadBalancerFilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/LoadBalancerFilterFunctions.java index ab6d47dc..db7b1464 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/LoadBalancerFilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/LoadBalancerFilterFunctions.java @@ -63,6 +63,8 @@ public abstract class LoadBalancerFilterFunctions { public static HandlerFilterFunction lb(String serviceId, BiFunction reconstructUriFunction) { return (request, next) -> { + MvcUtils.addOriginalRequestUrl(request, request.uri()); + LoadBalancerClientFactory clientFactory = getApplicationContext(request) .getBean(LoadBalancerClientFactory.class); Set supportedLifecycleProcessors = LoadBalancerLifecycleValidator diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/LoadBalancerHandlerSupplier.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/LoadBalancerHandlerSupplier.java index 3cedfa35..c2a0ad58 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/LoadBalancerHandlerSupplier.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/LoadBalancerHandlerSupplier.java @@ -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()))); } diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java index db5e0539..bee93756 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java @@ -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 originalUris = null; // TODO: - // exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR); - URI requestUri = null; // TODO: - // exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); + LinkedHashSet 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); diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/DefaultHandlerSupplier.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/DefaultHandlerSupplier.java new file mode 100644 index 00000000..0adc142e --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/DefaultHandlerSupplier.java @@ -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 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 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 handlerFunction) { + HandlerFilterFunction setId = setIdFilter(id); + HandlerFilterFunction setRequest = setRequestUrlFilter(uri); + return new HandlerDiscoverer.Result(handlerFunction, Arrays.asList(setId, setRequest), Collections.emptyList()); + } + + private static HandlerFilterFunction setIdFilter(String id) { + return (request, next) -> { + MvcUtils.setRouteId(request, id); + return next.handle(request); + }; + } + + private static HandlerFilterFunction setRequestUrlFilter(URI uri) { + return (request, next) -> { + MvcUtils.setRequestUrl(request, uri); + return next.handle(request); + }; + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerRequestProcessingHelper.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerRequestProcessingHelper.java index 1990991b..fd4f158a 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerRequestProcessingHelper.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/FunctionHandlerRequestProcessingHelper.java @@ -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 ignoredHeaders, List 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 ignoredHeaders, List requestOnlyHeaders, + Map 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, diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerDiscoverer.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerDiscoverer.java index be0dd7cf..7569842d 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerDiscoverer.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerDiscoverer.java @@ -37,20 +37,39 @@ public class HandlerDiscoverer extends AbstractGatewayDiscoverer { private final HandlerFunction handlerFunction; - private final List> filters; + private final List> lowerPrecedenceFilters; + private final List> higherPrecedenceFilters; + + @Deprecated public Result(HandlerFunction handlerFunction, List> filters) { + this(handlerFunction, Collections.emptyList(), filters); + } + + public Result(HandlerFunction handlerFunction, + List> lowerPrecedenceFilters, + List> 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 getHandlerFunction() { return handlerFunction; } + @Deprecated public List> getFilters() { - return filters; + return getHigherPrecedenceFilters(); + } + + public List> getLowerPrecedenceFilters() { + return lowerPrecedenceFilters; + } + + public List> getHigherPrecedenceFilters() { + return higherPrecedenceFilters; } } diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerFunctions.java index 04cfc1a0..14ea609c 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/HandlerFunctions.java @@ -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 fn(RouteProperties routeProperties) { - // fn:fnName - return fn(routeProperties.getUri().getSchemeSpecificPart()); - } - public static HandlerFunction 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 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 stream(RouteProperties routeProperties) { - // stream:bindingName - return stream(routeProperties.getUri().getSchemeSpecificPart()); - } - public static HandlerFunction 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 http(String uri) { return http(URI.create(uri)); } + @Deprecated public static HandlerFunction http(URI uri) { return new LookupProxyExchangeHandlerFunction(uri); } + @Deprecated public static HandlerFunction https(URI uri) { return new LookupProxyExchangeHandlerFunction(uri); } @@ -141,6 +171,7 @@ public abstract class HandlerFunctions { static class LookupProxyExchangeHandlerFunction implements HandlerFunction { + @Deprecated private final URI uri; private AtomicReference 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 get() { - return Arrays.asList(HandlerFunctions.class.getMethods()); + return Collections.emptyList(); } } diff --git a/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring.factories b/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring.factories index ba2cd973..b7002f15 100644 --- a/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring.factories @@ -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=\ diff --git a/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 0b209bc3..94282943 100644 --- a/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-gateway-server-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -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 \ No newline at end of file +org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration +org.springframework.cloud.gateway.server.mvc.config.DefaultFunctionConfiguration \ No newline at end of file diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java index 1e91f7b6..f72b1943 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java @@ -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 map = res.getResponseBody(); Map 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 map = res.getResponseBody(); assertThat(map).containsEntry("data", "hello"); Map 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 map = res.getResponseBody(); + Map 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 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 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 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 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 { diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrarTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrarTests.java index f4f7675c..41f6bff8 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrarTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrarTests.java @@ -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() diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java index 7393acae..2f23adc2 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java @@ -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/with–en–dashes") + .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/with–en–dashes 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"); + } + } diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java index edae11e4..d93dfc57 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java @@ -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 { diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/StripPrefixStaticPortTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/StripPrefixStaticPortTests.java new file mode 100644 index 00000000..4052a3f6 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/StripPrefixStaticPortTests.java @@ -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 map = res.getResponseBody(); + Map 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 map = res.getResponseBody(); + Map 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 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(); + } + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/DefaultRouteFunctionHandlerTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/DefaultRouteFunctionHandlerTests.java new file mode 100644 index 00000000..14f7c956 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/handler/DefaultRouteFunctionHandlerTests.java @@ -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 upper() { + return s -> s.toUpperCase(Locale.ROOT); + } + + @Bean + Consumer consume() { + return s -> { + consumerInvoked = false; + assertThat(s).isEqualTo("hello"); + consumerInvoked = true; + }; + } + + @Bean + Supplier hello() { + return () -> "hello"; + } + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinTestcontainers.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinTestcontainers.java index e1dc98a4..95345fce 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinTestcontainers.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinTestcontainers.java @@ -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 map = new HashMap<>(); map.put("httpbin.port", String.valueOf(mappedPort)); diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinUriResolver.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinUriResolver.java index c46e1f25..93b6bb51 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinUriResolver.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinUriResolver.java @@ -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 diff --git a/spring-cloud-gateway-server-mvc/src/test/resources/application-propertiesbeandefinitionregistrartests.yml b/spring-cloud-gateway-server-mvc/src/test/resources/application-propertiesbeandefinitionregistrartests.yml index 5270b8cb..a91fe14d 100644 --- a/spring-cloud-gateway-server-mvc/src/test/resources/application-propertiesbeandefinitionregistrartests.yml +++ b/spring-cloud-gateway-server-mvc/src/test/resources/application-propertiesbeandefinitionregistrartests.yml @@ -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 diff --git a/spring-cloud-gateway-server-mvc/src/test/resources/application-stripprefixstaticport.yml b/spring-cloud-gateway-server-mvc/src/test/resources/application-stripprefixstaticport.yml new file mode 100644 index 00000000..c27795c3 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/resources/application-stripprefixstaticport.yml @@ -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 diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index f95112cf..5d399d95 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -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 diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AbstractGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AbstractGatewayFilterFactory.java index e94a2ede..2413fbda 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AbstractGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AbstractGatewayFilterFactory.java @@ -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 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; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java index d8046e27..f073796d 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java @@ -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 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 exceptionRetry = null; if (!retryConfig.getExceptions().isEmpty()) { Predicate> retryContextPredicate = context -> { @@ -163,6 +170,14 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory repeat, Retry 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= 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= 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(); + + } + } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java index 0cbc3eca..823eac83 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java @@ -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 statuses = config.getStatusCodes() .stream() diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java index b95cb191..e55871f3 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java @@ -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 = '='; diff --git a/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 4fb4e6b8..1aea9f85 100644 --- a/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -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", diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java index 192411b1..fb1439d1 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java @@ -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") diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java index 1966fa9e..87c26cc6 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java @@ -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\"}"); + } + } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java index 4487c19f..76a54365 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java @@ -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 postFallbackController(@RequestBody String body) { + return Collections.singletonMap("body", body); + } + @GetMapping("/circuitbreakerUriFallbackController/**") public Map uriFallbackcontroller(ServerWebExchange exchange, @RequestParam("a") String a) { return Collections.singletonMap("uri", exchange.getRequest().getURI().toString()); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java index 73cb0003..0bc91049 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java @@ -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"); + } + } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java index 5bfcd7e8..44698524 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java @@ -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(); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiterTests.java index 9181ce03..2be6617a 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiterTests.java @@ -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") diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index f998d8e0..2ca53a27 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -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() diff --git a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt index ce14c50a..03cbacfb 100644 --- a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt +++ b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt @@ -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) diff --git a/spring-cloud-gateway-server/src/test/resources/application.yml b/spring-cloud-gateway-server/src/test/resources/application.yml index d5798cdf..ac989a01 100644 --- a/spring-cloud-gateway-server/src/test/resources/application.yml +++ b/spring-cloud-gateway-server/src/test/resources/application.yml @@ -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}