Merge branch 'main' into pr/3751

This commit is contained in:
Ryan Baxter
2025-05-23 11:20:52 -04:00
206 changed files with 7106 additions and 1193 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
distribution: 'liberica'
java-version: '17'
cache: 'maven'
- name: Build with Maven

View File

@@ -1,7 +1,7 @@
[[addresponseheader-gatewayfilter-factory]]
= `AddResponseHeader` `GatewayFilter` Factory
The `AddResponseHeader` `GatewayFilter` Factory takes a `name` and `value` parameter.
The `AddResponseHeader` `GatewayFilter` Factory takes three parameters: `name`, `value` and `override`(default value is `true`) .
The following example configures an `AddResponseHeader` `GatewayFilter`:
.application.yml
@@ -15,9 +15,12 @@ spring:
uri: https://example.org
filters:
- AddResponseHeader=X-Response-Red, Blue
- AddResponseHeader=X-Response-Black, White, false
----
This adds `X-Response-Red:Blue` header to the downstream response's headers for all matching requests.
and if the response already contains the `X-Response-Black` header, this will not add the `X-Response-Black: White`
header to the downstream response's headers for all matching requests.
`AddResponseHeader` is aware of URI variables used to match a path or host.
URI variables may be used in the value and are expanded at runtime.

View File

@@ -10,7 +10,7 @@ The following headers (shown with their default values) are added:
* `X-Frame-Options (DENY)`
* `X-Content-Type-Options (nosniff)`
* `Referrer-Policy (no-referrer)`
* `Content-Security-Policy (default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline)'`
* `Content-Security-Policy (default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline')`
* `X-Download-Options (noopen)`
* `X-Permitted-Cross-Domain-Policies (none)`
@@ -29,10 +29,87 @@ The following properties are available:
To disable the default values set the `spring.cloud.gateway.filter.secure-headers.disable` property with comma-separated values.
The following example shows how to do so:
[source]
.application.yml
[source,yaml]
----
spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transport-security
spring:
cloud:
gateway:
filter:
secure-headers:
disable: x-frame-options,strict-transport-security
----
NOTE: The lowercase full name of the secure header needs to be used to disable it..
To apply the `SecureHeaders` filter to a specific route, add the filter to the list of filters of that route.
You can customize the route filter using arguments. Route configuration overrides the global default configuration for this route.
.application.yml
[source,yaml]
----
- id: secureheaders_route
uri: http://example.org
predicates:
- Path=/**
filters:
- name: SecureHeaders
args:
disable: x-frame-options
----
NOTE: The lowercase full name of the secure header needs to be used to disable it.
== Further options
You may opt in to add the `Permissions-Policy` header to the response. Permissions Policy is a security header
that allows web developers to manage which browser features a website can utilize. Please see
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy[Permissions-Policy] and
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#directives[Directives] to configure it
for your environment.
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
filter:
secure-headers:
enable: permissions-policy
permissions-policy : geolocation=(self "https://example.com")
----
In the above https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy/geolocation[example]
the Geolocation API is disabled within all browsing contexts except for its own origin and those whose origin is "https://example.com".
The Permissions-Policy may be configured separately for each route.
.application.yml
[source,yaml]
----
- id: secureheaders_route
uri: http://anotherexample.org
predicates:
- Path=/**
filters:
- name: SecureHeaders
args:
disable: x-frame-options
enable: permissions-policy
permissions-policy : geolocation=("https://anotherexample.org")
----
WARNING: When you enable Permissions-Policy and do not explicitly configure any directives, a default value will be applied.
Specifically, this default value disables a wide range of standardized and experimental features.
This behavior might not be appropriate for your specific environment or use case.
Permissions-Policy default value when enabled and no explicit configuration:
`Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(),
display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(),
fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(),
payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(),
web-share=(), xr-spatial-tracking=()`
NOTE: You can check the Permissions Policy feature list for Chrome with https://developer.chrome.com/docs/privacy-security/permissions-policy#chrome_devtools_integration[DevTool Integration].
When you configure the header value for your environment, make sure to check the browser console for syntax errors.

View File

@@ -0,0 +1,26 @@
[[seturi-gatewayfilter-factory]]
= `SetRequestUri` `GatewayFilter` Factory
The `SetRequestUri` `GatewayFilter` factory takes a `uri` parameter.
It offers a simple way to manipulate the request uri by allowing templated segments of the path.
This uses the URI templates from Spring Framework.
Multiple matching segments are allowed.
The following listing configures a `SetRequestUri` `GatewayFilter`:
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: seturi_route
uri: no://op
predicates:
- Path=/{appId}/**
filters:
- SetRequestUri=http://{appId}.example.com
----
For a request path of `/red-application/blue`, this sets the uri to `http://red-application.example.com` before making the downstream request and the final url, including path is going to be `http://red-application.example.com/red-application/blue`

View File

@@ -192,6 +192,8 @@ This route matches if the request path was, for example: `/red/1` or `/red/1/` o
If `matchTrailingSlash` is set to `false`, then request path `/red/1/` will not be matched.
If you have set `spring.webflux.base-path` property, this will influence the path matching. The property value will be automatically prepended to the path patterns. For example, with `spring.webflux.base-path=/app` and a path pattern of `/red/\{segment\}`, the full pattern used for matching would be `/app/red/\{segment\}`.
This predicate extracts the URI template variables (such as `segment`, defined in the preceding example) as a map of names and values and places it in the `ServerWebExchange.getAttributes()` with a key defined in `ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`.
Those values are then available for use by <<gateway-route-filters,`GatewayFilter` factories>>

View File

@@ -5,7 +5,7 @@
To include Spring Cloud Gateway Server Web MVC in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-gateway-server-webmvc`.
See the https://projects.spring.io/spring-cloud/[Spring Cloud Project page] for details on setting up your build system with the current Spring Cloud Release Train.
If you include the starter, but you do not want the gateway to be enabled, set `spring.cloud.gateway.mvc.enabled=false`.
If you include the starter, but you do not want the gateway to be enabled, set `spring.cloud.gateway.server.webmvc.enabled=false`.
IMPORTANT: Spring Cloud Gateway Server MVC is built on https://spring.io/projects/spring-boot#learn[Spring Boot] and https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html[Spring WebMvc.fn].
As a consequence, many of the asynchronous or reactive libraries may not apply when you use Spring Cloud Gateway Server MVC.

View File

@@ -240,13 +240,12 @@ The above route will add a `X-Response-Id` header to the response. Note the use
== How To Register Custom Predicates and Filters for Configuration
To use custom Predicates and Filters in external configuration you need to create a special Supplier class and register it in `META-INF/spring.factories`.
To use custom Predicates and Filters in external configuration you need to create a special Supplier class and register it a bean in the application context.
=== Registering Custom Predicates
To register custom predicates you need to implement `PredicateSupplier`. The `PredicateDiscoverer` looks for static methods that return `RequestPredicates` to register.
SampleFilterSupplier.java
[source,java]
----
@@ -254,7 +253,6 @@ package com.example;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier;
@Configuration
class SamplePredicateSupplier implements PredicateSupplier {
@Override
@@ -265,7 +263,24 @@ class SamplePredicateSupplier implements PredicateSupplier {
}
----
You then need to add the class in `META-INF/spring.factories`.
To register the `PredicateSupplier` for use in config files, you then need to add the class as a bean as in the example below:
.PredicateConfiguration.java
[source,java]
----
package com.example;
@Configuration
class PredicateConfiguration {
@Bean
public SamplePredicateSupplier samplePredicateSupplier() {
return new SamplePredicateSupplier();
}
}
----
The requirement to add the class to `META-INF/spring.factories` is deprecated and will be removed in the next major release.
.META-INF/spring.factories
[source]
@@ -285,7 +300,6 @@ package com.example;
import org.springframework.cloud.gateway.server.mvc.filter.SimpleFilterSupplier;
@Configuration
class SampleFilterSupplier extends SimpleFilterSupplier {
public SampleFilterSupplier() {
@@ -294,7 +308,24 @@ class SampleFilterSupplier extends SimpleFilterSupplier {
}
----
You then need to add the class in `META-INF/spring.factories`.
To register the `FilterSupplier` for use in config files, you then need to add the class as a bean as in the example below:
.FilterConfiguration.java
[source,java]
----
package com.example;
@Configuration
class FilterConfiguration {
@Bean
public SampleFilterSupplier sampleFilterSupplier() {
return new SampleFilterSupplier();
}
}
----
The requirement to add the class to `META-INF/spring.factories` is deprecated and will be removed in the next major release.
.META-INF/spring.factories
[source]

View File

@@ -1,7 +1,9 @@
|===
|Name | Default | Description
|spring.cloud.gateway | |
|spring.cloud.gateway.default-filters | | List of filter definitions that are applied to every route.
|spring.cloud.gateway.discovery.locator | |
|spring.cloud.gateway.discovery.locator.enabled | `+++false+++` | Flag that enables DiscoveryClient gateway integration.
|spring.cloud.gateway.discovery.locator.filters | |
|spring.cloud.gateway.discovery.locator.include-expression | `+++true+++` | SpEL expression that will evaluate whether to include a service in gateway integration or not, defaults to: true.
@@ -19,7 +21,9 @@
|spring.cloud.gateway.filter.fallback-headers.enabled | `+++true+++` | Enables the fallback-headers filter.
|spring.cloud.gateway.filter.hystrix.enabled | `+++true+++` | Enables the hystrix filter.
|spring.cloud.gateway.filter.json-to-grpc.enabled | `+++true+++` | Enables the JSON to gRPC filter.
|spring.cloud.gateway.filter.local-response-cache | |
|spring.cloud.gateway.filter.local-response-cache.enabled | `+++false+++` | Enables the local-response-cache filter.
|spring.cloud.gateway.filter.local-response-cache.request | |
|spring.cloud.gateway.filter.local-response-cache.request.no-cache-strategy | `+++skip-update-cache-entry+++` |
|spring.cloud.gateway.filter.local-response-cache.size | | Maximum size of the cache to evict entries for this route (in KB, MB and GB).
|spring.cloud.gateway.filter.local-response-cache.time-to-live | `+++5m+++` | Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours).
@@ -29,6 +33,7 @@
|spring.cloud.gateway.filter.prefix-path.enabled | `+++true+++` | Enables the prefix-path filter.
|spring.cloud.gateway.filter.preserve-host-header.enabled | `+++true+++` | Enables the preserve-host-header filter.
|spring.cloud.gateway.filter.redirect-to.enabled | `+++true+++` | Enables the redirect-to filter.
|spring.cloud.gateway.filter.remove-hop-by-hop | |
|spring.cloud.gateway.filter.remove-hop-by-hop.headers | |
|spring.cloud.gateway.filter.remove-hop-by-hop.order | `+++0+++` |
|spring.cloud.gateway.filter.remove-request-header.enabled | `+++true+++` | Enables the remove-request-header filter.
@@ -36,6 +41,7 @@
|spring.cloud.gateway.filter.remove-response-header.enabled | `+++true+++` | Enables the remove-response-header filter.
|spring.cloud.gateway.filter.request-header-size.enabled | `+++true+++` | Enables the request-header-size filter.
|spring.cloud.gateway.filter.request-header-to-request-uri.enabled | `+++true+++` | Enables the request-header-to-request-uri filter.
|spring.cloud.gateway.filter.request-rate-limiter | |
|spring.cloud.gateway.filter.request-rate-limiter.default-key-resolver | |
|spring.cloud.gateway.filter.request-rate-limiter.default-rate-limiter | |
|spring.cloud.gateway.filter.request-rate-limiter.enabled | `+++true+++` | Enables the request-rate-limiter filter.
@@ -47,12 +53,17 @@
|spring.cloud.gateway.filter.rewrite-request-parameter.enabled | `+++true+++` | Enables the rewrite-request-parameter filter.
|spring.cloud.gateway.filter.rewrite-response-header.enabled | `+++true+++` | Enables the rewrite-response-header filter.
|spring.cloud.gateway.filter.save-session.enabled | `+++true+++` | Enables the save-session filter.
|spring.cloud.gateway.filter.secure-headers | |
|spring.cloud.gateway.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
|spring.cloud.gateway.filter.secure-headers.content-type-options | `+++nosniff+++` |
|spring.cloud.gateway.filter.secure-headers.default-headers | |
|spring.cloud.gateway.filter.secure-headers.disable | |
|spring.cloud.gateway.filter.secure-headers.disabled-headers | |
|spring.cloud.gateway.filter.secure-headers.download-options | `+++noopen+++` |
|spring.cloud.gateway.filter.secure-headers.enabled | `+++true+++` | Enables the secure-headers filter.
|spring.cloud.gateway.filter.secure-headers.enabled-headers | |
|spring.cloud.gateway.filter.secure-headers.frame-options | `+++DENY+++` |
|spring.cloud.gateway.filter.secure-headers.permissions-policy | `+++accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()+++` |
|spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
|spring.cloud.gateway.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
|spring.cloud.gateway.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
@@ -76,13 +87,16 @@
|spring.cloud.gateway.global-filter.remove-cached-body.enabled | `+++true+++` | Enables the remove-cached-body global filter.
|spring.cloud.gateway.global-filter.route-to-request-url.enabled | `+++true+++` | Enables the route-to-request-url global filter.
|spring.cloud.gateway.global-filter.websocket-routing.enabled | `+++true+++` | Enables the websocket-routing global filter.
|spring.cloud.gateway.globalcors | |
|spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping | `+++false+++` | If global CORS config should be added to the URL handler.
|spring.cloud.gateway.globalcors.cors-configurations | |
|spring.cloud.gateway.handler-mapping.order | `+++1+++` | The order of RoutePredicateHandlerMapping.
|spring.cloud.gateway.httpclient | |
|spring.cloud.gateway.httpclient.compression | `+++false+++` | Enables compression for Netty HttpClient.
|spring.cloud.gateway.httpclient.connect-timeout | | The connect timeout in millis, the default is 30s.
|spring.cloud.gateway.httpclient.max-header-size | | The max response header size.
|spring.cloud.gateway.httpclient.max-initial-line-length | | The max initial line length.
|spring.cloud.gateway.httpclient.pool | |
|spring.cloud.gateway.httpclient.pool.acquire-timeout | | Only for type FIXED, the maximum time in millis to wait for acquiring.
|spring.cloud.gateway.httpclient.pool.eviction-interval | `+++0+++` | Perform regular eviction checks in the background at a specified interval. Disabled by default ({@link Duration#ZERO})
|spring.cloud.gateway.httpclient.pool.leasing-strategy | `+++fifo+++` | Configures the leasing strategy for the pool (fifo or lifo), defaults to FIFO which is Netty's default.
@@ -92,6 +106,7 @@
|spring.cloud.gateway.httpclient.pool.metrics | `+++false+++` | Enables channel pools metrics to be collected and registered in Micrometer. Disabled by default.
|spring.cloud.gateway.httpclient.pool.name | `+++proxy+++` | The channel pool map name, defaults to proxy.
|spring.cloud.gateway.httpclient.pool.type | `+++elastic+++` | Type of pool for HttpClient to use (elastic, fixed or disabled).
|spring.cloud.gateway.httpclient.proxy | |
|spring.cloud.gateway.httpclient.proxy.host | | Hostname for proxy configuration of Netty HttpClient.
|spring.cloud.gateway.httpclient.proxy.non-proxy-hosts-pattern | | Regular expression (Java) for a configured list of hosts. that should be reached directly, bypassing the proxy
|spring.cloud.gateway.httpclient.proxy.password | | Password for proxy configuration of Netty HttpClient.
@@ -99,6 +114,7 @@
|spring.cloud.gateway.httpclient.proxy.type | `+++http+++` | proxyType for proxy configuration of Netty HttpClient (http, socks4 or socks5).
|spring.cloud.gateway.httpclient.proxy.username | | Username for proxy configuration of Netty HttpClient.
|spring.cloud.gateway.httpclient.response-timeout | | The response timeout.
|spring.cloud.gateway.httpclient.ssl | |
|spring.cloud.gateway.httpclient.ssl.close-notify-flush-timeout | `+++3000ms+++` | SSL close_notify flush timeout. Default to 3000 ms.
|spring.cloud.gateway.httpclient.ssl.close-notify-read-timeout | `+++0+++` | SSL close_notify read timeout. Default to 0 ms.
|spring.cloud.gateway.httpclient.ssl.handshake-timeout | `+++10000ms+++` | SSL handshake timeout. Default to 10000 ms
@@ -110,11 +126,14 @@
|spring.cloud.gateway.httpclient.ssl.ssl-bundle | | The name of the SSL bundle to use.
|spring.cloud.gateway.httpclient.ssl.trusted-x509-certificates | | Trusted certificates for verifying the remote endpoint's certificate.
|spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager | `+++false+++` | Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production.
|spring.cloud.gateway.httpclient.websocket | |
|spring.cloud.gateway.httpclient.websocket.max-frame-payload-length | | Max frame payload length.
|spring.cloud.gateway.httpclient.websocket.proxy-ping | `+++true+++` | Proxy ping frames to downstream services, defaults to true.
|spring.cloud.gateway.httpclient.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpClient.
|spring.cloud.gateway.httpserver.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpServer.
|spring.cloud.gateway.loadbalancer | |
|spring.cloud.gateway.loadbalancer.use404 | `+++false+++` |
|spring.cloud.gateway.metrics | |
|spring.cloud.gateway.metrics.enabled | `+++false+++` | Enables the collection of metrics data.
|spring.cloud.gateway.metrics.prefix | `+++spring.cloud.gateway+++` | The prefix of all metrics emitted by gateway.
|spring.cloud.gateway.metrics.tags | | Tags map that added to metrics.
@@ -125,8 +144,9 @@
|spring.cloud.gateway.mvc.http-client.ssl-bundle | | The name of the SSL bundle to use.
|spring.cloud.gateway.mvc.http-client.type | `+++jdk+++` | The HttpClient type. Defaults to JDK.
|spring.cloud.gateway.mvc.remove-content-length-request-headers-filter.enabled | `+++true+++` | Enables the remove-content-length-request-headers-filter.
|spring.cloud.gateway.mvc.remove-hop-by-hop-request-headers-filter.enabled | `+++true+++` | Enables the forwarded-request-headers-filter.
|spring.cloud.gateway.mvc.remove-hop-by-hop-response-headers-filter.enabled | `+++true+++` | Enables the forwarded-request-headers-filter.
|spring.cloud.gateway.mvc.remove-hop-by-hop-request-headers-filter.enabled | `+++true+++` | Enables the remove-hop-by-hop-request-headers-filter.
|spring.cloud.gateway.mvc.remove-hop-by-hop-response-headers-filter.enabled | `+++true+++` | Enables the remove-hop-by-hop-response-headers-filter.
|spring.cloud.gateway.mvc.remove-http2-status-response-headers-filter.enabled | `+++true+++` | Enables the remove-http2-status-response-headers-filter.
|spring.cloud.gateway.mvc.routes | | List of Routes.
|spring.cloud.gateway.mvc.routes-map | | Map of Routes.
|spring.cloud.gateway.mvc.streaming-buffer-size | `+++16384+++` | Buffer size for streaming media mime-types.
@@ -161,6 +181,7 @@
|spring.cloud.gateway.predicate.remote-addr.enabled | `+++true+++` | Enables the remote-addr predicate.
|spring.cloud.gateway.predicate.weight.enabled | `+++true+++` | Enables the weight predicate.
|spring.cloud.gateway.predicate.xforwarded-remote-addr.enabled | `+++true+++` | Enables the xforwarded-remote-addr predicate.
|spring.cloud.gateway.redis-rate-limiter | |
|spring.cloud.gateway.redis-rate-limiter.burst-capacity-header | `+++X-RateLimit-Burst-Capacity+++` | The name of the header that returns the burst capacity configuration.
|spring.cloud.gateway.redis-rate-limiter.config | |
|spring.cloud.gateway.redis-rate-limiter.include-headers | `+++true+++` | Whether or not to include headers containing rate limiter information, defaults to true.
@@ -172,8 +193,200 @@
|spring.cloud.gateway.route-filter-cache-enabled | `+++false+++` | Enables the route filter cache, defaults to false.
|spring.cloud.gateway.route-refresh-listener.enabled | `+++true+++` | If RouteRefreshListener should be turned on.
|spring.cloud.gateway.routes | | List of Routes.
|spring.cloud.gateway.server.webflux.default-filters | | List of filter definitions that are applied to every route.
|spring.cloud.gateway.server.webflux.discovery.locator.enabled | `+++false+++` | Flag that enables DiscoveryClient gateway integration.
|spring.cloud.gateway.server.webflux.discovery.locator.filters | |
|spring.cloud.gateway.server.webflux.discovery.locator.include-expression | `+++true+++` | SpEL expression that will evaluate whether to include a service in gateway integration or not, defaults to: true.
|spring.cloud.gateway.server.webflux.discovery.locator.lower-case-service-id | `+++false+++` | Option to lower case serviceId in predicates and filters, defaults to false. Useful with eureka when it automatically uppercases serviceId. so MYSERIVCE, would match /myservice/**
|spring.cloud.gateway.server.webflux.discovery.locator.predicates | |
|spring.cloud.gateway.server.webflux.discovery.locator.route-id-prefix | | The prefix for the routeId, defaults to discoveryClient.getClass().getSimpleName() + "_". Service Id will be appended to create the routeId.
|spring.cloud.gateway.server.webflux.discovery.locator.url-expression | `+++'lb://'+serviceId+++` | SpEL expression that create the uri for each route, defaults to: 'lb://'+serviceId.
|spring.cloud.gateway.server.webflux.enabled | `+++true+++` | Enables gateway functionality.
|spring.cloud.gateway.server.webflux.fail-on-route-definition-error | `+++true+++` | Option to fail on route definition errors, defaults to true. Otherwise, a warning is logged.
|spring.cloud.gateway.server.webflux.filter.add-request-header.enabled | `+++true+++` | Enables the add-request-header filter.
|spring.cloud.gateway.server.webflux.filter.add-request-parameter.enabled | `+++true+++` | Enables the add-request-parameter filter.
|spring.cloud.gateway.server.webflux.filter.add-response-header.enabled | `+++true+++` | Enables the add-response-header filter.
|spring.cloud.gateway.server.webflux.filter.circuit-breaker.enabled | `+++true+++` | Enables the circuit-breaker filter.
|spring.cloud.gateway.server.webflux.filter.dedupe-response-header.enabled | `+++true+++` | Enables the dedupe-response-header filter.
|spring.cloud.gateway.server.webflux.filter.fallback-headers.enabled | `+++true+++` | Enables the fallback-headers filter.
|spring.cloud.gateway.server.webflux.filter.hystrix.enabled | `+++true+++` | Enables the hystrix filter.
|spring.cloud.gateway.server.webflux.filter.json-to-grpc.enabled | `+++true+++` | Enables the JSON to gRPC filter.
|spring.cloud.gateway.server.webflux.filter.local-response-cache.enabled | `+++false+++` | Enables the local-response-cache filter.
|spring.cloud.gateway.server.webflux.filter.local-response-cache.request.no-cache-strategy | `+++skip-update-cache-entry+++` |
|spring.cloud.gateway.server.webflux.filter.local-response-cache.size | | Maximum size of the cache to evict entries for this route (in KB, MB and GB).
|spring.cloud.gateway.server.webflux.filter.local-response-cache.time-to-live | `+++5m+++` | Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours).
|spring.cloud.gateway.server.webflux.filter.map-request-header.enabled | `+++true+++` | Enables the map-request-header filter.
|spring.cloud.gateway.server.webflux.filter.modify-request-body.enabled | `+++true+++` | Enables the modify-request-body filter.
|spring.cloud.gateway.server.webflux.filter.modify-response-body.enabled | `+++true+++` | Enables the modify-response-body filter.
|spring.cloud.gateway.server.webflux.filter.prefix-path.enabled | `+++true+++` | Enables the prefix-path filter.
|spring.cloud.gateway.server.webflux.filter.preserve-host-header.enabled | `+++true+++` | Enables the preserve-host-header filter.
|spring.cloud.gateway.server.webflux.filter.redirect-to.enabled | `+++true+++` | Enables the redirect-to filter.
|spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop.headers | |
|spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop.order | `+++0+++` |
|spring.cloud.gateway.server.webflux.filter.remove-request-header.enabled | `+++true+++` | Enables the remove-request-header filter.
|spring.cloud.gateway.server.webflux.filter.remove-request-parameter.enabled | `+++true+++` | Enables the remove-request-parameter filter.
|spring.cloud.gateway.server.webflux.filter.remove-response-header.enabled | `+++true+++` | Enables the remove-response-header filter.
|spring.cloud.gateway.server.webflux.filter.request-header-size.enabled | `+++true+++` | Enables the request-header-size filter.
|spring.cloud.gateway.server.webflux.filter.request-header-to-request-uri.enabled | `+++true+++` | Enables the request-header-to-request-uri filter.
|spring.cloud.gateway.server.webflux.filter.request-rate-limiter.default-key-resolver | |
|spring.cloud.gateway.server.webflux.filter.request-rate-limiter.default-rate-limiter | |
|spring.cloud.gateway.server.webflux.filter.request-rate-limiter.enabled | `+++true+++` | Enables the request-rate-limiter filter.
|spring.cloud.gateway.server.webflux.filter.request-size.enabled | `+++true+++` | Enables the request-size filter.
|spring.cloud.gateway.server.webflux.filter.retry.enabled | `+++true+++` | Enables the retry filter.
|spring.cloud.gateway.server.webflux.filter.rewrite-location-response-header.enabled | `+++true+++` | Enables the rewrite-location-response-header filter.
|spring.cloud.gateway.server.webflux.filter.rewrite-location.enabled | `+++true+++` | Enables the rewrite-location filter.
|spring.cloud.gateway.server.webflux.filter.rewrite-path.enabled | `+++true+++` | Enables the rewrite-path filter.
|spring.cloud.gateway.server.webflux.filter.rewrite-request-parameter.enabled | `+++true+++` | Enables the rewrite-request-parameter filter.
|spring.cloud.gateway.server.webflux.filter.rewrite-response-header.enabled | `+++true+++` | Enables the rewrite-response-header filter.
|spring.cloud.gateway.server.webflux.filter.save-session.enabled | `+++true+++` | Enables the save-session filter.
|spring.cloud.gateway.server.webflux.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.content-type-options | `+++nosniff+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.default-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.disable | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.disabled-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.download-options | `+++noopen+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.enabled | `+++true+++` | Enables the secure-headers filter.
|spring.cloud.gateway.server.webflux.filter.secure-headers.enabled-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.frame-options | `+++DENY+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.permissions-policy | `+++accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.xss-protection-header | `+++1 ; mode=block+++` |
|spring.cloud.gateway.server.webflux.filter.set-path.enabled | `+++true+++` | Enables the set-path filter.
|spring.cloud.gateway.server.webflux.filter.set-request-header.enabled | `+++true+++` | Enables the set-request-header filter.
|spring.cloud.gateway.server.webflux.filter.set-request-host-header.enabled | `+++true+++` | Enables the set-request-host-header filter.
|spring.cloud.gateway.server.webflux.filter.set-response-header.enabled | `+++true+++` | Enables the set-response-header filter.
|spring.cloud.gateway.server.webflux.filter.set-status.enabled | `+++true+++` | Enables the set-status filter.
|spring.cloud.gateway.server.webflux.filter.strip-prefix.enabled | `+++true+++` | Enables the strip-prefix filter.
|spring.cloud.gateway.server.webflux.forwarded.by.enabled | `+++false+++` | Enables the Forwarded: by header part.
|spring.cloud.gateway.server.webflux.forwarded.enabled | `+++true+++` | Enables the ForwardedHeadersFilter.
|spring.cloud.gateway.server.webflux.global-filter.adapt-cached-body.enabled | `+++true+++` | Enables the adapt-cached-body global filter.
|spring.cloud.gateway.server.webflux.global-filter.forward-path.enabled | `+++true+++` | Enables the forward-path global filter.
|spring.cloud.gateway.server.webflux.global-filter.forward-routing.enabled | `+++true+++` | Enables the forward-routing global filter.
|spring.cloud.gateway.server.webflux.global-filter.load-balancer-client.enabled | `+++true+++` | Enables the load-balancer-client global filter.
|spring.cloud.gateway.server.webflux.global-filter.local-response-cache.enabled | `+++true+++` | Enables the local-response-cache filter for all routes, it allows to add a specific configuration at route level using LocalResponseCache filter.
|spring.cloud.gateway.server.webflux.global-filter.netty-routing.enabled | `+++true+++` | Enables the netty-routing global filter.
|spring.cloud.gateway.server.webflux.global-filter.netty-write-response.enabled | `+++true+++` | Enables the netty-write-response global filter.
|spring.cloud.gateway.server.webflux.global-filter.reactive-load-balancer-client.enabled | `+++true+++` | Enables the reactive-load-balancer-client global filter.
|spring.cloud.gateway.server.webflux.global-filter.remove-cached-body.enabled | `+++true+++` | Enables the remove-cached-body global filter.
|spring.cloud.gateway.server.webflux.global-filter.route-to-request-url.enabled | `+++true+++` | Enables the route-to-request-url global filter.
|spring.cloud.gateway.server.webflux.global-filter.websocket-routing.enabled | `+++true+++` | Enables the websocket-routing global filter.
|spring.cloud.gateway.server.webflux.globalcors.add-to-simple-url-handler-mapping | `+++false+++` | If global CORS config should be added to the URL handler.
|spring.cloud.gateway.server.webflux.globalcors.cors-configurations | |
|spring.cloud.gateway.server.webflux.handler-mapping.order | `+++1+++` | The order of RoutePredicateHandlerMapping.
|spring.cloud.gateway.server.webflux.httpclient.compression | `+++false+++` | Enables compression for Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpclient.connect-timeout | | The connect timeout in millis, the default is 30s.
|spring.cloud.gateway.server.webflux.httpclient.max-header-size | | The max response header size.
|spring.cloud.gateway.server.webflux.httpclient.max-initial-line-length | | The max initial line length.
|spring.cloud.gateway.server.webflux.httpclient.pool.acquire-timeout | | Only for type FIXED, the maximum time in millis to wait for acquiring.
|spring.cloud.gateway.server.webflux.httpclient.pool.eviction-interval | `+++0+++` | Perform regular eviction checks in the background at a specified interval. Disabled by default ({@link Duration#ZERO})
|spring.cloud.gateway.server.webflux.httpclient.pool.leasing-strategy | `+++fifo+++` | Configures the leasing strategy for the pool (fifo or lifo), defaults to FIFO which is Netty's default.
|spring.cloud.gateway.server.webflux.httpclient.pool.max-connections | | Only for type FIXED, the maximum number of connections before starting pending acquisition on existing ones.
|spring.cloud.gateway.server.webflux.httpclient.pool.max-idle-time | | Time in millis after which the channel will be closed. If NULL, there is no max idle time.
|spring.cloud.gateway.server.webflux.httpclient.pool.max-life-time | | Duration after which the channel will be closed. If NULL, there is no max life time.
|spring.cloud.gateway.server.webflux.httpclient.pool.metrics | `+++false+++` | Enables channel pools metrics to be collected and registered in Micrometer. Disabled by default.
|spring.cloud.gateway.server.webflux.httpclient.pool.name | `+++proxy+++` | The channel pool map name, defaults to proxy.
|spring.cloud.gateway.server.webflux.httpclient.pool.type | `+++elastic+++` | Type of pool for HttpClient to use (elastic, fixed or disabled).
|spring.cloud.gateway.server.webflux.httpclient.proxy.host | | Hostname for proxy configuration of Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpclient.proxy.non-proxy-hosts-pattern | | Regular expression (Java) for a configured list of hosts. that should be reached directly, bypassing the proxy
|spring.cloud.gateway.server.webflux.httpclient.proxy.password | | Password for proxy configuration of Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpclient.proxy.port | | Port for proxy configuration of Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpclient.proxy.type | `+++http+++` | proxyType for proxy configuration of Netty HttpClient (http, socks4 or socks5).
|spring.cloud.gateway.server.webflux.httpclient.proxy.username | | Username for proxy configuration of Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpclient.response-timeout | | The response timeout.
|spring.cloud.gateway.server.webflux.httpclient.ssl.close-notify-flush-timeout | `+++3000ms+++` | SSL close_notify flush timeout. Default to 3000 ms.
|spring.cloud.gateway.server.webflux.httpclient.ssl.close-notify-read-timeout | `+++0+++` | SSL close_notify read timeout. Default to 0 ms.
|spring.cloud.gateway.server.webflux.httpclient.ssl.handshake-timeout | `+++10000ms+++` | SSL handshake timeout. Default to 10000 ms
|spring.cloud.gateway.server.webflux.httpclient.ssl.key-password | | Key password, default is same as keyStorePassword.
|spring.cloud.gateway.server.webflux.httpclient.ssl.key-store | | Keystore path for Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpclient.ssl.key-store-password | | Keystore password.
|spring.cloud.gateway.server.webflux.httpclient.ssl.key-store-provider | | Keystore provider for Netty HttpClient, optional field.
|spring.cloud.gateway.server.webflux.httpclient.ssl.key-store-type | `+++JKS+++` | Keystore type for Netty HttpClient, default is JKS.
|spring.cloud.gateway.server.webflux.httpclient.ssl.ssl-bundle | | The name of the SSL bundle to use.
|spring.cloud.gateway.server.webflux.httpclient.ssl.trusted-x509-certificates | | Trusted certificates for verifying the remote endpoint's certificate.
|spring.cloud.gateway.server.webflux.httpclient.ssl.use-insecure-trust-manager | `+++false+++` | Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production.
|spring.cloud.gateway.server.webflux.httpclient.websocket.max-frame-payload-length | | Max frame payload length.
|spring.cloud.gateway.server.webflux.httpclient.websocket.proxy-ping | `+++true+++` | Proxy ping frames to downstream services, defaults to true.
|spring.cloud.gateway.server.webflux.httpclient.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpserver.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpServer.
|spring.cloud.gateway.server.webflux.loadbalancer.use404 | `+++false+++` |
|spring.cloud.gateway.server.webflux.metrics.enabled | `+++false+++` | Enables the collection of metrics data.
|spring.cloud.gateway.server.webflux.metrics.prefix | `+++spring.cloud.gateway+++` | The prefix of all metrics emitted by gateway.
|spring.cloud.gateway.server.webflux.metrics.tags | | Tags map that added to metrics.
|spring.cloud.gateway.server.webflux.observability.enabled | `+++true+++` | If Micrometer Observability support should be turned on.
|spring.cloud.gateway.server.webflux.predicate.after.enabled | `+++true+++` | Enables the after predicate.
|spring.cloud.gateway.server.webflux.predicate.before.enabled | `+++true+++` | Enables the before predicate.
|spring.cloud.gateway.server.webflux.predicate.between.enabled | `+++true+++` | Enables the between predicate.
|spring.cloud.gateway.server.webflux.predicate.cloud-foundry-route-service.enabled | `+++true+++` | Enables the cloud-foundry-route-service predicate.
|spring.cloud.gateway.server.webflux.predicate.cookie.enabled | `+++true+++` | Enables the cookie predicate.
|spring.cloud.gateway.server.webflux.predicate.header.enabled | `+++true+++` | Enables the header predicate.
|spring.cloud.gateway.server.webflux.predicate.host.enabled | `+++true+++` | Enables the host predicate.
|spring.cloud.gateway.server.webflux.predicate.host.include-port | `+++true+++` | Include the port in matching the host name.
|spring.cloud.gateway.server.webflux.predicate.method.enabled | `+++true+++` | Enables the method predicate.
|spring.cloud.gateway.server.webflux.predicate.path.enabled | `+++true+++` | Enables the path predicate.
|spring.cloud.gateway.server.webflux.predicate.query.enabled | `+++true+++` | Enables the query predicate.
|spring.cloud.gateway.server.webflux.predicate.read-body.enabled | `+++true+++` | Enables the read-body predicate.
|spring.cloud.gateway.server.webflux.predicate.remote-addr.enabled | `+++true+++` | Enables the remote-addr predicate.
|spring.cloud.gateway.server.webflux.predicate.weight.enabled | `+++true+++` | Enables the weight predicate.
|spring.cloud.gateway.server.webflux.predicate.xforwarded-remote-addr.enabled | `+++true+++` | Enables the xforwarded-remote-addr predicate.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.burst-capacity-header | `+++X-RateLimit-Burst-Capacity+++` | The name of the header that returns the burst capacity configuration.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.config | |
|spring.cloud.gateway.server.webflux.redis-rate-limiter.include-headers | `+++true+++` | Whether or not to include headers containing rate limiter information, defaults to true.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.remaining-header | `+++X-RateLimit-Remaining+++` | The name of the header that returns number of remaining requests during the current second.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.replenish-rate-header | `+++X-RateLimit-Replenish-Rate+++` | The name of the header that returns the replenish rate configuration.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.requested-tokens-header | `+++X-RateLimit-Requested-Tokens+++` | The name of the header that returns the requested tokens configuration.
|spring.cloud.gateway.server.webflux.redis-route-definition-repository.enabled | `+++true+++` | If RedisRouteDefinitionRepository should be enabled.
|spring.cloud.gateway.server.webflux.restrictive-property-accessor.enabled | `+++true+++` | Restricts method and property access in SpEL.
|spring.cloud.gateway.server.webflux.route-filter-cache-enabled | `+++false+++` | Enables the route filter cache, defaults to false.
|spring.cloud.gateway.server.webflux.route-refresh-listener.enabled | `+++true+++` | If RouteRefreshListener should be turned on.
|spring.cloud.gateway.server.webflux.routes | | List of Routes.
|spring.cloud.gateway.server.webflux.set-status.original-status-header-name | | The name of the header which contains http code of the proxied request.
|spring.cloud.gateway.server.webflux.streaming-media-types | |
|spring.cloud.gateway.server.webflux.x-forwarded.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.for-append | `+++true+++` | If appending X-Forwarded-For as a list is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.for-enabled | `+++true+++` | If X-Forwarded-For is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.host-append | `+++true+++` | If appending X-Forwarded-Host as a list is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.host-enabled | `+++true+++` | If X-Forwarded-Host is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.order | `+++0+++` | The order of the XForwardedHeadersFilter.
|spring.cloud.gateway.server.webflux.x-forwarded.port-append | `+++true+++` | If appending X-Forwarded-Port as a list is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.port-enabled | `+++true+++` | If X-Forwarded-Port is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.prefix-append | `+++true+++` | If appending X-Forwarded-Prefix as a list is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.prefix-enabled | `+++true+++` | If X-Forwarded-Prefix is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.proto-append | `+++true+++` | If appending X-Forwarded-Proto as a list is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.proto-enabled | `+++true+++` | If X-Forwarded-Proto is enabled.
|spring.cloud.gateway.server.webmvc.form-filter.enabled | `+++true+++` | Enables the form-filter.
|spring.cloud.gateway.server.webmvc.forwarded-request-headers-filter.enabled | `+++true+++` | Enables the forwarded-request-headers-filter.
|spring.cloud.gateway.server.webmvc.http-client.connect-timeout | | The HttpClient connect timeout.
|spring.cloud.gateway.server.webmvc.http-client.read-timeout | | The HttpClient read timeout.
|spring.cloud.gateway.server.webmvc.http-client.ssl-bundle | | The name of the SSL bundle to use.
|spring.cloud.gateway.server.webmvc.http-client.type | `+++jdk+++` | The HttpClient type. Defaults to JDK.
|spring.cloud.gateway.server.webmvc.remove-content-length-request-headers-filter.enabled | `+++true+++` | Enables the remove-content-length-request-headers-filter.
|spring.cloud.gateway.server.webmvc.remove-hop-by-hop-request-headers-filter.enabled | `+++true+++` | Enables the remove-hop-by-hop-request-headers-filter.
|spring.cloud.gateway.server.webmvc.remove-hop-by-hop-response-headers-filter.enabled | `+++true+++` | Enables the remove-hop-by-hop-response-headers-filter.
|spring.cloud.gateway.server.webmvc.remove-http2-status-response-headers-filter.enabled | `+++true+++` | Enables the remove-http2-status-response-headers-filter.
|spring.cloud.gateway.server.webmvc.routes | | List of Routes.
|spring.cloud.gateway.server.webmvc.routes-map | | Map of Routes.
|spring.cloud.gateway.server.webmvc.streaming-buffer-size | `+++16384+++` | Buffer size for streaming media mime-types.
|spring.cloud.gateway.server.webmvc.streaming-media-types | | Mime-types that are streaming.
|spring.cloud.gateway.server.webmvc.transfer-encoding-normalization-request-headers-filter.enabled | `+++true+++` | Enables the transfer-encoding-normalization-request-headers-filter.
|spring.cloud.gateway.server.webmvc.weight-calculator-filter.enabled | `+++true+++` | Enables the weight-calculator-filter.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.for-append | `+++true+++` | If appending X-Forwarded-For as a list is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.for-enabled | `+++true+++` | If X-Forwarded-For is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.host-append | `+++true+++` | If appending X-Forwarded-Host as a list is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.host-enabled | `+++true+++` | If X-Forwarded-Host is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.order | `+++0+++` | The order of the XForwardedHeadersFilter.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.port-append | `+++true+++` | If appending X-Forwarded-Port as a list is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.port-enabled | `+++true+++` | If X-Forwarded-Port is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.prefix-append | `+++true+++` | If appending X-Forwarded-Prefix as a list is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.prefix-enabled | `+++true+++` | If X-Forwarded-Prefix is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.proto-append | `+++true+++` | If appending X-Forwarded-Proto as a list is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.proto-enabled | `+++true+++` | If X-Forwarded-Proto is enabled.
|spring.cloud.gateway.set-status | |
|spring.cloud.gateway.set-status.original-status-header-name | | The name of the header which contains http code of the proxied request.
|spring.cloud.gateway.streaming-media-types | |
|spring.cloud.gateway.x-forwarded | |
|spring.cloud.gateway.x-forwarded.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|spring.cloud.gateway.x-forwarded.for-append | `+++true+++` | If appending X-Forwarded-For as a list is enabled.
|spring.cloud.gateway.x-forwarded.for-enabled | `+++true+++` | If X-Forwarded-For is enabled.

View File

@@ -5,6 +5,6 @@
"@antora/collector-extension": "1.0.1",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.4",
"@springio/asciidoctor-extensions": "1.0.0-alpha.16"
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
}
}

View File

@@ -12,7 +12,7 @@
<properties>
<protoc.version>3.25.1</protoc.version>
<grpc.version>1.71.0</grpc.version>
<grpc.version>1.72.0</grpc.version>
</properties>
<parent>

View File

@@ -20,7 +20,7 @@ management:
spring:
cloud:
gateway:
gateway.server.webflux:
httpserver:
wiretap: true
httpclient:

View File

@@ -13,7 +13,7 @@ server:
spring:
cloud:
gateway:
gateway.server.webflux:
# httpserver:
# wiretap: true
httpclient:

View File

@@ -9,5 +9,5 @@ server:
spring:
cloud:
gateway:
gateway.server.webflux:
enabled: false

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>httpclient</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Gateway HttpClient Integration Test</name>
<description>Spring Cloud Gateway HttpClient Integration Test</description>
<properties>
</properties>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
<version>4.3.0-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,132 @@
/*
* 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.tests.httpclient;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.util.Timeout;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath;
import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb;
import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
/**
* @author jiangyuan
*/
@SpringBootConfiguration
@EnableAutoConfiguration
@LoadBalancerClient(name = "myservice", configuration = MyServiceConf.class)
public class HttpClientApplication {
public static void main(String[] args) {
SpringApplication.run(HttpClientApplication.class, args);
}
@Bean
public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(2);
connectionManager.setDefaultMaxPerRoute(2);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(
RequestConfig.custom().setConnectionRequestTimeout(Timeout.of(Duration.ofMillis(3000))).build())
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
return factory;
}
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsRetry() {
return route("test-retry").GET("/retry", http())
.filter(lb("myservice"))
.filter(prefixPath("/do"))
.filter(retry(3))
.build();
}
@RestController
protected static class RetryController {
Log log = LogFactory.getLog(getClass());
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
@GetMapping("/do/retry")
public ResponseEntity<String> retry(@RequestParam("key") String key,
@RequestParam(name = "count", defaultValue = "3") int count,
@RequestParam(name = "failStatus", required = false) Integer failStatus) {
AtomicInteger num = map.computeIfAbsent(key, s -> new AtomicInteger());
int i = num.incrementAndGet();
log.warn("Retry count: " + i);
String body = String.valueOf(i);
if (i < count) {
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
if (failStatus != null) {
httpStatus = HttpStatus.resolve(failStatus);
}
return ResponseEntity.status(httpStatus).header("X-Retry-Count", body).body("temporarily broken");
}
return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body);
}
}
}
class MyServiceConf {
@Value("${local.server.port}")
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("myservice",
new DefaultServiceInstance("myservice-1", "myservice", "localhost", port, false));
}
}

View File

@@ -0,0 +1,3 @@
logging:
level:
org.springframework.cloud.gateway: TRACE

View File

@@ -0,0 +1,42 @@
/*
* 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.tests.httpclient;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* @author jiangyuan
*/
@SpringBootTest(classes = HttpClientApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class HttpClientApplicationTests {
@LocalServerPort
private int port;
@Test
public void retryWorks() {
WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build();
client.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3");
}
}

View File

@@ -48,7 +48,7 @@ public class MvcFailureAnalyzerApplicationTests {
@Test
public void exceptionNotThrownWhenDisabled(CapturedOutput output) {
assertThatCode(() -> new SpringApplication(MvcFailureAnalyzerApplication.class)
.run("--spring.cloud.gateway.enabled=false", "--server.port=0")).doesNotThrowAnyException();
.run("--spring.cloud.gateway.server.webflux.enabled=false", "--server.port=0")).doesNotThrowAnyException();
assertThat(output).doesNotContain(MvcFoundOnClasspathFailureAnalyzer.MESSAGE,
MvcFoundOnClasspathFailureAnalyzer.ACTION);
}

View File

@@ -24,6 +24,7 @@
<module>grpc</module>
<module>http2</module>
<module>mvc-failure-analyzer</module>
<module>httpclient</module>
</modules>
<build>

View File

@@ -41,6 +41,11 @@
<version>4.5.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2016-2019 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.mvc.config;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.mvc.ProxyExchange;
import org.springframework.http.HttpHeaders;
/**
* Configuration properties for the {@link ProxyExchange} argument handler in
* <code>@RequestMapping</code> methods.
*
* @author Dave Syer
* @author Tim Ysewyn
* @author Joris Kuipers
*
*/
@ConfigurationProperties(ProxyExchangeWebMvcProperties.PREFIX)
public class ProxyExchangeWebMvcProperties {
/**
* Properties prefix.
*/
public static final String PREFIX = "spring.cloud.gateway.proxy-exchange.webmvc";
/**
* Contains headers that are considered sensitive by default.
*/
public static Set<String> DEFAULT_SENSITIVE = Set.of("cookie", "authorization");
/**
* Contains headers that are skipped by default.
*/
public static Set<String> DEFAULT_SKIPPED = Set.of("content-length", "host");
/**
* Fixed header values that will be added to all downstream requests.
*/
private Map<String, String> headers = new LinkedHashMap<>();
/**
* A set of header names that should be sent downstream by default.
*/
private Set<String> autoForward = new HashSet<>();
/**
* A set of sensitive header names that will not be sent downstream by default.
*/
private Set<String> sensitive = DEFAULT_SENSITIVE;
/**
* A set of header names that will not be sent downstream because they could be
* problematic.
*/
private Set<String> skipped = DEFAULT_SKIPPED;
public Map<String, String> getHeaders() {
return headers;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public Set<String> getAutoForward() {
return autoForward;
}
public void setAutoForward(Set<String> autoForward) {
this.autoForward = autoForward;
}
public Set<String> getSensitive() {
return sensitive;
}
public void setSensitive(Set<String> sensitive) {
this.sensitive = sensitive;
}
public Set<String> getSkipped() {
return skipped;
}
public void setSkipped(Set<String> skipped) {
this.skipped = skipped;
}
public HttpHeaders convertHeaders() {
HttpHeaders headers = new HttpHeaders();
for (String key : this.headers.keySet()) {
headers.set(key, this.headers.get(key));
}
return headers;
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.cloud.gateway.mvc.ProxyExchange;
import org.springframework.http.HttpHeaders;
@@ -32,8 +33,10 @@ import org.springframework.http.HttpHeaders;
* @author Dave Syer
* @author Tim Ysewyn
* @author Joris Kuipers
*
* @author Spencer Gibb
* @deprecated {@link ProxyExchangeWebMvcProperties}
*/
@Deprecated
@ConfigurationProperties("spring.cloud.gateway.proxy")
public class ProxyProperties {
@@ -68,6 +71,7 @@ public class ProxyProperties {
*/
private Set<String> skipped = DEFAULT_SKIPPED;
@DeprecatedConfigurationProperty(replacement = ProxyExchangeWebMvcProperties.PREFIX + ".headers", since = "4.3.0")
public Map<String, String> getHeaders() {
return headers;
}
@@ -76,6 +80,8 @@ public class ProxyProperties {
this.headers = headers;
}
@DeprecatedConfigurationProperty(replacement = ProxyExchangeWebMvcProperties.PREFIX + ".auto-forward",
since = "4.3.0")
public Set<String> getAutoForward() {
return autoForward;
}
@@ -84,6 +90,7 @@ public class ProxyProperties {
this.autoForward = autoForward;
}
@DeprecatedConfigurationProperty(replacement = ProxyExchangeWebMvcProperties.PREFIX + ".sensitive", since = "4.3.0")
public Set<String> getSensitive() {
return sensitive;
}
@@ -92,6 +99,7 @@ public class ProxyProperties {
this.sensitive = sensitive;
}
@DeprecatedConfigurationProperty(replacement = ProxyExchangeWebMvcProperties.PREFIX + ".skipped", since = "4.3.0")
public Set<String> getSkipped() {
return skipped;
}

View File

@@ -57,7 +57,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@ConditionalOnClass({ HandlerMethodReturnValueHandler.class })
@EnableConfigurationProperties(ProxyProperties.class)
@EnableConfigurationProperties({ ProxyExchangeWebMvcProperties.class, ProxyProperties.class })
public class ProxyResponseAutoConfiguration implements WebMvcConfigurer {
@Autowired
@@ -66,7 +66,7 @@ public class ProxyResponseAutoConfiguration implements WebMvcConfigurer {
@Bean
@ConditionalOnMissingBean
public ProxyExchangeArgumentResolver proxyExchangeArgumentResolver(Optional<RestTemplateBuilder> optional,
ProxyProperties proxy) {
ProxyExchangeWebMvcProperties properties) {
RestTemplateBuilder builder = optional.orElse(new RestTemplateBuilder());
RestTemplate template = builder.build();
template.setErrorHandler(new NoOpResponseErrorHandler());
@@ -77,14 +77,14 @@ public class ProxyResponseAutoConfiguration implements WebMvcConfigurer {
}
});
ProxyExchangeArgumentResolver resolver = new ProxyExchangeArgumentResolver(template);
resolver.setHeaders(proxy.convertHeaders());
resolver.setAutoForwardedHeaders(proxy.getAutoForward());
resolver.setHeaders(properties.convertHeaders());
resolver.setAutoForwardedHeaders(properties.getAutoForward());
Set<String> excludedHeaderNames = new HashSet<>();
if (proxy.getSensitive() != null) {
excludedHeaderNames.addAll(proxy.getSensitive());
if (properties.getSensitive() != null) {
excludedHeaderNames.addAll(properties.getSensitive());
}
if (proxy.getSkipped() != null) {
excludedHeaderNames.addAll(proxy.getSkipped());
if (properties.getSkipped() != null) {
excludedHeaderNames.addAll(properties.getSkipped());
}
resolver.setExcluded(excludedHeaderNames);
return resolver;

View File

@@ -0,0 +1,52 @@
/*
* 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.mvc.config;
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.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("unchecked")
@SpringBootTest(properties = {}, webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("propertiesmigrationtests")
public class ProxyExchangeWebmvcPropertiesMigrationTests {
@Autowired
ProxyExchangeWebMvcProperties properties;
@Test
public void deprecatedRoutePropertiesWork() {
assertThat(properties.getHeaders()).hasSize(2);
assertThat(properties.getAutoForward()).hasSize(2);
assertThat(properties.getSensitive()).hasSize(2);
assertThat(properties.getSkipped()).hasSize(3);
}
@SpringBootConfiguration
@EnableAutoConfiguration
protected static class TestConfiguration {
}
}

View File

@@ -0,0 +1,10 @@
spring.cloud.gateway.proxy:
headers:
X-Foo: xfooval
X-Bar: xbarval
auto-forward: X-FWD1, X-FWD2
sensitive: X-S1, X-S21
skipped: X-SK1, X-SK2, X-SK3
logging:
level:
org.springframework.cloud.gateway.server.mvc: TRACE

View File

@@ -0,0 +1,50 @@
test:
hostport: httpbin.org:80
# hostport: localhost:5000
uri: http://${test.hostport}
#uri: lb://httpbin
spring:
cloud:
gateway.server.webflux:
filter:
default-filters:
#- PrefixPath=/httpbin
#- AddResponseHeader=X-Response-Default-Foo, Default-Bar
routes:
# =====================================
# to run server
# $ wscat --listen 9000
# to run client
# $ wscat --connect ws://localhost:8080/echo
- id: websocket_test
uri: ws://localhost:9000
order: 9000
predicates:
- Path=/echo
# =====================================
- id: default_path_to_httpbin_secureheaders
uri: ${test.uri}
order: 10000
predicates:
- Path=/**
filters:
- name: SecureHeaders
args:
disable: x-frame-options
enable: permissions-policy
permissions-policy : geolocation=("https://example.net")
logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG
reactor.netty: DEBUG
management.endpoints.web.exposure.include: '*'

View File

@@ -9,7 +9,7 @@ spring:
jmx:
enabled: false
cloud:
gateway:
gateway.server.webflux:
default-filters:
- PrefixPath=/httpbin
- AddResponseHeader=X-Response-Default-Foo, Default-Bar

View File

@@ -107,6 +107,16 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
<exclusions>
<exclusion>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<!-- Third party test dependencies -->
@@ -136,4 +146,30 @@
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>github_actions</id>
<!-- TODO: github actions fails with restricted header host -->
<activation>
<property>
<name>env.GITHUB_ACTIONS</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<jdk.httpclient.allowRestrictedHeaders>host</jdk.httpclient.allowRestrictedHeaders>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -22,11 +22,12 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(GatewayServerMvcAutoConfiguration.class)
@ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".enabled", matchIfMissing = true)
public class GatewayMvcClassPathWarningAutoConfiguration {
private static final Log log = LogFactory.getLog(GatewayMvcClassPathWarningAutoConfiguration.class);

View File

@@ -18,23 +18,26 @@ package org.springframework.cloud.gateway.server.mvc;
import java.util.Map;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.client.AbstractHttpRequestFactoryProperties.Factory;
import org.springframework.boot.autoconfigure.http.client.HttpClientAutoConfiguration;
import org.springframework.boot.autoconfigure.http.client.HttpClientProperties.Factory;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.cloud.gateway.server.mvc.common.ArgumentSupplierBeanPostProcessor;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcAotRuntimeHintsRegistrar;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcPropertiesBeanDefinitionRegistrar;
import org.springframework.cloud.gateway.server.mvc.config.RouterFunctionHolderFactory;
import org.springframework.cloud.gateway.server.mvc.filter.FilterAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.filter.FilterBeanFactoryDiscoverer;
import org.springframework.cloud.gateway.server.mvc.filter.FormFilter;
import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.RequestHttpHeadersFilter;
@@ -47,9 +50,12 @@ import org.springframework.cloud.gateway.server.mvc.filter.TransferEncodingNorma
import org.springframework.cloud.gateway.server.mvc.filter.WeightCalculatorFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctionAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchange;
import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchangeHandlerFunction;
import org.springframework.cloud.gateway.server.mvc.handler.RestClientProxyExchange;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateBeanFactoryDiscoverer;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscoverer;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
@@ -70,8 +76,9 @@ import org.springframework.web.client.RestClient;
* @author Jürgen Wißkirchen
*/
@AutoConfiguration(after = { HttpClientAutoConfiguration.class, RestTemplateAutoConfiguration.class,
RestClientAutoConfiguration.class })
@ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true)
RestClientAutoConfiguration.class, FilterAutoConfiguration.class, HandlerFunctionAutoConfiguration.class,
PredicateAutoConfiguration.class })
@ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".enabled", matchIfMissing = true)
@Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class)
@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class)
public class GatewayServerMvcAutoConfiguration {
@@ -83,8 +90,11 @@ public class GatewayServerMvcAutoConfiguration {
}
@Bean
public RouterFunctionHolderFactory routerFunctionHolderFactory(Environment env) {
return new RouterFunctionHolderFactory(env);
public RouterFunctionHolderFactory routerFunctionHolderFactory(Environment env, BeanFactory beanFactory,
FilterBeanFactoryDiscoverer filterBeanFactoryDiscoverer,
PredicateBeanFactoryDiscoverer predicateBeanFactoryDiscoverer) {
return new RouterFunctionHolderFactory(env, beanFactory, filterBeanFactoryDiscoverer,
predicateBeanFactoryDiscoverer);
}
@Bean
@@ -206,17 +216,19 @@ public class GatewayServerMvcAutoConfiguration {
static final boolean REACTOR_NETTY = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", null);
static final boolean JDK = ClassUtils.isPresent("java.net.http.HttpClient", null);
static final boolean HIGHER_PRIORITY = APACHE || JETTY || REACTOR_NETTY;
static final String SPRING_REDIRECTS_PROPERTY = "spring.http.client.redirects";
static final String SPRING_HTTP_FACTORY_PROPERTY = "spring.http.client.factory";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Redirects redirects = environment.getProperty("spring.http.client.redirects", Redirects.class);
HttpRedirects redirects = environment.getProperty(SPRING_REDIRECTS_PROPERTY, HttpRedirects.class);
if (redirects == null) {
// the user hasn't set anything, change the default
environment.getPropertySources()
.addFirst(new MapPropertySource("gatewayHttpClientProperties",
Map.of("spring.http.client.redirects", Redirects.DONT_FOLLOW)));
Map.of(SPRING_REDIRECTS_PROPERTY, HttpRedirects.DONT_FOLLOW)));
}
Factory factory = environment.getProperty("spring.http.client.factory", Factory.class);
Factory factory = environment.getProperty(SPRING_HTTP_FACTORY_PROPERTY, Factory.class);
boolean setJdkHttpClientProperties = false;
if (factory == null && !HIGHER_PRIORITY) {

View File

@@ -0,0 +1,38 @@
/*
* 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.common;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
public abstract class BeanFactoryGatewayDiscoverer extends AbstractGatewayDiscoverer {
protected final BeanFactory beanFactory;
protected BeanFactoryGatewayDiscoverer(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
protected <T> List<T> loadSuppliers(Class<T> supplierClass) {
ObjectProvider<T> beanProvider = beanFactory.getBeanProvider(supplierClass);
return beanProvider.orderedStream().toList();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* 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.
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -40,11 +41,14 @@ import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import static org.springframework.web.servlet.function.RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
@@ -63,6 +67,11 @@ public abstract class MvcUtils {
*/
public static final String CLIENT_RESPONSE_INPUT_STREAM_ATTR = qualify("cachedClientResponseBody");
/**
* Client response key.
*/
public static final String CLIENT_RESPONSE_ATTR = qualify("cachedClientResponse");
/**
* CircuitBreaker execution exception attribute name.
*/
@@ -263,6 +272,17 @@ public abstract class MvcUtils {
urls.add(url);
}
public static MultiValueMap<String, String> encodeQueryParams(MultiValueMap<String, String> params) {
MultiValueMap<String, String> encodedQueryParams = new LinkedMultiValueMap<>(params.size());
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
for (String value : entry.getValue()) {
encodedQueryParams.add(UriUtils.encode(entry.getKey(), StandardCharsets.UTF_8),
UriUtils.encode(value, StandardCharsets.UTF_8));
}
}
return CollectionUtils.unmodifiableMultiValueMap(encodedQueryParams);
}
private record ByteArrayInputMessage(ServerRequest request, ByteArrayInputStream body) implements HttpInputMessage {
@Override

View File

@@ -29,9 +29,9 @@ import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions
import org.springframework.cloud.gateway.server.mvc.filter.BodyFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.FilterAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerHandlerSupplier;
import org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions;
@@ -46,11 +46,12 @@ import org.springframework.util.ClassUtils;
*/
public class GatewayMvcAotRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
// TODO: fix AOT HINTS
private static final Set<Class<?>> FUNCTION_PROVIDERS = Set.of(HandlerFunctions.class,
LoadBalancerHandlerSupplier.class, FilterFunctions.class, BeforeFilterFunctions.class,
AfterFilterFunctions.class, TokenRelayFilterFunctions.class, BodyFilterFunctions.class,
CircuitBreakerFilterFunctions.class, GatewayRouterFunctions.class, LoadBalancerFilterFunctions.class,
GatewayRequestPredicates.class, Bucket4jFilterFunctions.class);
FilterAutoConfiguration.LoadBalancerHandlerConfiguration.class, FilterFunctions.class,
BeforeFilterFunctions.class, AfterFilterFunctions.class, TokenRelayFilterFunctions.class,
BodyFilterFunctions.class, CircuitBreakerFilterFunctions.class, GatewayRouterFunctions.class,
LoadBalancerFilterFunctions.class, GatewayRequestPredicates.class, Bucket4jFilterFunctions.class);
private static final Set<Class<?>> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class,
RouteProperties.class);

View File

@@ -36,7 +36,7 @@ public class GatewayMvcProperties {
/**
* Properties prefix.
*/
public static final String PREFIX = "spring.cloud.gateway.mvc";
public static final String PREFIX = "spring.cloud.gateway.server.webmvc";
/**
* List of Routes.

View File

@@ -0,0 +1,193 @@
/*
* 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 java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.context.properties.source.IterableConfigurationPropertySource;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.boot.origin.PropertySourceOrigin;
import org.springframework.boot.origin.TextResourceOrigin;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
class GatewayServerWebMvcPropertiesMigrationListener implements ApplicationListener<SpringApplicationEvent> {
private static final Log logger = LogFactory.getLog(GatewayServerWebMvcPropertiesMigrationListener.class);
private static final String PROPERTIES_MIGRATOR_CLASS = "org.springframework.boot.context.properties.migrator.PropertiesMigrationListener";
private static final String DEPRECATED_ROOT = "spring.cloud.gateway.mvc";
private static final String DEPRECATED_ROUTES_LIST_KEY = DEPRECATED_ROOT + ".routes";
private static final String DEPRECATED_ROUTES_MAP_KEY = DEPRECATED_ROOT + ".routes-map";
private static final String DEPRECATED_ROUTESMAP_KEY = DEPRECATED_ROOT + ".routesMap";
private static final String GATEWAY_PROPERTY_SOURCE_PREFIX = "migrategatewaymvc";
private static final String NEW_ROUTES_LIST_KEY = GatewayMvcProperties.PREFIX + ".routes";
private static final String NEW_ROUTES_MAP_KEY = GatewayMvcProperties.PREFIX + ".routes-map";
private final List<Migration> routesMigrations = new ArrayList<>();
@Override
public void onApplicationEvent(SpringApplicationEvent event) {
// only run if spring-boot-properties-migrator is on the classpath
if (!ClassUtils.isPresent(PROPERTIES_MIGRATOR_CLASS, null)) {
return;
}
if (event instanceof ApplicationPreparedEvent preparedEvent) {
onApplicationPreparedEvent(preparedEvent);
}
if (event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) {
logLegacyPropertiesReport();
}
}
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
// find deprecated keys
ConfigurableEnvironment env = event.getApplicationContext().getEnvironment();
ConfigurationPropertySources.get(env).forEach(propertySource -> {
routesMigrations.addAll(migrate(env, propertySource, GATEWAY_PROPERTY_SOURCE_PREFIX + "routes-",
DEPRECATED_ROUTES_LIST_KEY, NEW_ROUTES_LIST_KEY));
routesMigrations.addAll(migrate(env, propertySource, GATEWAY_PROPERTY_SOURCE_PREFIX + "routes-map-",
DEPRECATED_ROUTES_MAP_KEY, NEW_ROUTES_MAP_KEY));
routesMigrations.addAll(migrate(env, propertySource, GATEWAY_PROPERTY_SOURCE_PREFIX + "routesMap-",
DEPRECATED_ROUTES_MAP_KEY, NEW_ROUTES_MAP_KEY));
});
}
private List<Migration> migrate(ConfigurableEnvironment env, ConfigurationPropertySource propertySource,
String propertySourcePrefix, String deprecatedKey, String newKeyPrefix) {
List<Migration> migrations = new ArrayList<>();
if (propertySource instanceof IterableConfigurationPropertySource iterableSource) {
ConfigurationPropertyName routesParentName = ConfigurationPropertyName.of(deprecatedKey);
List<ConfigurationPropertyName> matchingConfigProps = iterableSource.filter(n -> {
if (n.getNumberOfElements() < routesParentName.getNumberOfElements()) {
return false;
}
ConfigurationPropertyName chop = n.chop(routesParentName.getNumberOfElements());
return routesParentName.equals(chop);
}).stream().toList();
if (!matchingConfigProps.isEmpty()) {
String originalPropertySourceName;
if (propertySource.getUnderlyingSource() instanceof PropertySource<?> underlyingSource) {
originalPropertySourceName = underlyingSource.getName();
}
else {
originalPropertySourceName = propertySource.getUnderlyingSource().toString();
}
String newPropertySourceName = propertySourcePrefix + originalPropertySourceName;
Map<String, OriginTrackedValue> content = new LinkedHashMap<>();
// migrate to new keys
for (ConfigurationPropertyName originalPropertyName : matchingConfigProps) {
ConfigurationPropertyName suffix = originalPropertyName
.subName(routesParentName.getNumberOfElements());
ConfigurationPropertyName newProperty = ConfigurationPropertyName.of(newKeyPrefix).append(suffix);
ConfigurationProperty configurationProperty = propertySource
.getConfigurationProperty(originalPropertyName);
Object value = configurationProperty.getValue();
OriginTrackedValue originTrackedValue = OriginTrackedValue.of(value,
configurationProperty.getOrigin());
content.put(newProperty.toString(), originTrackedValue);
migrations.add(new Migration(originalPropertySourceName, originalPropertyName,
configurationProperty, newProperty));
}
env.getPropertySources()
.addBefore(originalPropertySourceName,
new OriginTrackedMapPropertySource(newPropertySourceName, content));
}
}
return migrations;
}
private void logLegacyPropertiesReport() {
// log warnings
if (!routesMigrations.isEmpty()) {
LinkedMultiValueMap<String, Migration> content = new LinkedMultiValueMap<>();
routesMigrations.forEach(migration -> content.add(migration.originalPropertySourceName(), migration));
StringBuilder report = new StringBuilder();
report.append(String
.format("%nThe use of configuration keys that have been renamed was found in the environment:%n%n"));
content.forEach((name, properties) -> {
report.append(String.format("Property source '%s':%n", name));
// properties.sort(PropertyMigration.COMPARATOR);
properties.forEach((property) -> {
ConfigurationPropertyName originalPropertyName = property.originalPropertyName();
report.append(String.format("\tKey: %s%n", originalPropertyName));
Integer lineNumber = property.determineLineNumber();
if (lineNumber != null) {
report.append(String.format("\t\tLine: %d%n", lineNumber));
}
report.append(String.format("\t\tReplacement: %s%n", property.newProperty().toString()));
});
report.append(String.format("%n"));
});
report.append(String.format("%n"));
report.append("Each configuration key has been temporarily mapped to its "
+ "replacement for your convenience. To silence this warning, please "
+ "update your configuration to use the new keys.");
report.append(String.format("%n"));
logger.warn(report.toString());
}
}
private record Migration(String originalPropertySourceName, ConfigurationPropertyName originalPropertyName,
ConfigurationProperty originalProperty, ConfigurationPropertyName newProperty) {
private Integer determineLineNumber() {
Origin origin = originalProperty.getOrigin();
if (origin instanceof PropertySourceOrigin propertySourceOrigin) {
origin = propertySourceOrigin.getOrigin();
}
if (origin instanceof TextResourceOrigin textOrigin) {
if (textOrigin.getLocation() != null) {
return textOrigin.getLocation().getLine() + 1;
}
}
return null;
}
}
}

View File

@@ -28,10 +28,15 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.bind.handler.IgnoreTopLevelConverterNotFoundBindHandler;
@@ -39,8 +44,10 @@ 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.FilterBeanFactoryDiscoverer;
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.handler.HandlerFunctionDefinition;
import org.springframework.cloud.gateway.server.mvc.invoke.InvocationContext;
import org.springframework.cloud.gateway.server.mvc.invoke.OperationArgumentResolver;
import org.springframework.cloud.gateway.server.mvc.invoke.OperationParameter;
@@ -49,10 +56,13 @@ import org.springframework.cloud.gateway.server.mvc.invoke.ParameterValueMapper;
import org.springframework.cloud.gateway.server.mvc.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.cloud.gateway.server.mvc.invoke.reflect.OperationMethod;
import org.springframework.cloud.gateway.server.mvc.invoke.reflect.ReflectiveOperationInvoker;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateBeanFactoryDiscoverer;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscoverer;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.log.LogMessage;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.function.HandlerFilterFunction;
@@ -102,8 +112,37 @@ public class RouterFunctionHolderFactory {
private final ParameterValueMapper parameterValueMapper = new ConversionServiceParameterValueMapper();
private final BeanFactory beanFactory;
private final FilterBeanFactoryDiscoverer filterBeanFactoryDiscoverer;
private final PredicateBeanFactoryDiscoverer predicateBeanFactoryDiscoverer;
private final ConversionService conversionService;
@Deprecated
public RouterFunctionHolderFactory(Environment env) {
this(env, null, null, null);
}
public RouterFunctionHolderFactory(Environment env, BeanFactory beanFactory,
FilterBeanFactoryDiscoverer filterBeanFactoryDiscoverer,
PredicateBeanFactoryDiscoverer predicateBeanFactoryDiscoverer) {
this.env = env;
this.beanFactory = beanFactory;
this.filterBeanFactoryDiscoverer = filterBeanFactoryDiscoverer;
this.predicateBeanFactoryDiscoverer = predicateBeanFactoryDiscoverer;
if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) {
if (configurableBeanFactory.getConversionService() != null) {
this.conversionService = configurableBeanFactory.getConversionService();
}
else {
this.conversionService = DefaultConversionService.getSharedInstance();
}
}
else {
this.conversionService = DefaultConversionService.getSharedInstance();
}
}
/**
@@ -153,41 +192,65 @@ public class RouterFunctionHolderFactory {
// TODO: cache?
// translate handlerFunction
String scheme = routeProperties.getUri().getScheme();
Map<String, Object> handlerArgs = new HashMap<>();
Optional<NormalizedOperationMethod> handlerOperationMethod = findOperation(handlerOperations,
scheme.toLowerCase(Locale.ROOT), handlerArgs);
if (handlerOperationMethod.isEmpty()) {
// single RouteProperties param
handlerArgs.clear();
String routePropsKey = StringUtils.uncapitalize(RouteProperties.class.getSimpleName());
handlerArgs.put(routePropsKey, routeProperties);
handlerOperationMethod = findOperation(handlerOperations, scheme.toLowerCase(Locale.ROOT), handlerArgs);
if (handlerOperationMethod.isEmpty()) {
throw new IllegalStateException("Unable to find HandlerFunction for scheme: " + scheme);
}
}
NormalizedOperationMethod normalizedOpMethod = handlerOperationMethod.get();
Object response = invokeOperation(normalizedOpMethod, normalizedOpMethod.getNormalizedArgs());
HandlerFunction<ServerResponse> handlerFunction = null;
// filters added by HandlerDiscoverer need to go last, so save them
HandlerFunction<ServerResponse> handlerFunction = null;
List<HandlerFilterFunction<ServerResponse, ServerResponse>> lowerPrecedenceFilters = new ArrayList<>();
List<HandlerFilterFunction<ServerResponse, ServerResponse>> higherPrecedenceFilters = new ArrayList<>();
if (response instanceof HandlerFunction<?>) {
handlerFunction = (HandlerFunction<ServerResponse>) response;
}
else if (response instanceof HandlerDiscoverer.Result result) {
handlerFunction = result.getHandlerFunction();
lowerPrecedenceFilters.addAll(result.getLowerPrecedenceFilters());
higherPrecedenceFilters.addAll(result.getHigherPrecedenceFilters());
if (beanFactory != null) {
try {
// TODO: configurable bean name?
String name = scheme + "HandlerFunctionDefinition";
Function factory = beanFactory.getBean(name, Function.class);
HandlerFunctionDefinition definition = (HandlerFunctionDefinition) factory.apply(routeProperties);
handlerFunction = definition.handlerFunction();
lowerPrecedenceFilters.addAll(definition.lowerPrecedenceFilters());
higherPrecedenceFilters.addAll(definition.higherPrecedenceFilters());
}
catch (NoSuchBeanDefinitionException | BeanNotOfRequiredTypeException | ClassCastException e) {
log.trace(LogMessage.format("Unable to locate bean of HandlerFunction for scheme %s", scheme), e);
}
}
if (handlerFunction == null) {
throw new IllegalStateException(
"Unable to find HandlerFunction for scheme: " + scheme + " and response " + response);
Map<String, Object> handlerArgs = new HashMap<>();
Optional<NormalizedOperationMethod> handlerOperationMethod = findOperation(handlerOperations,
scheme.toLowerCase(Locale.ROOT), handlerArgs);
if (handlerOperationMethod.isEmpty()) {
// single RouteProperties param
handlerArgs.clear();
String routePropsKey = StringUtils.uncapitalize(RouteProperties.class.getSimpleName());
handlerArgs.put(routePropsKey, routeProperties);
handlerOperationMethod = findOperation(handlerOperations, scheme.toLowerCase(Locale.ROOT), handlerArgs);
if (handlerOperationMethod.isEmpty()) {
throw new IllegalStateException("Unable to find HandlerFunction for scheme: " + scheme);
}
}
NormalizedOperationMethod normalizedOpMethod = handlerOperationMethod.get();
Object response = invokeOperation(normalizedOpMethod, normalizedOpMethod.getNormalizedArgs());
if (response instanceof HandlerFunction<?>) {
handlerFunction = (HandlerFunction<ServerResponse>) response;
}
else if (response instanceof HandlerDiscoverer.Result result) {
handlerFunction = result.getHandlerFunction();
lowerPrecedenceFilters.addAll(result.getLowerPrecedenceFilters());
higherPrecedenceFilters.addAll(result.getHigherPrecedenceFilters());
}
if (handlerFunction == null) {
throw new IllegalStateException(
"Unable to find HandlerFunction for scheme: " + scheme + " and response " + response);
}
}
// translate predicates
MultiValueMap<String, OperationMethod> predicateOperations = predicateDiscoverer.getOperations();
MultiValueMap<String, OperationMethod> predicateOperations = new LinkedMultiValueMap<>();
if (predicateBeanFactoryDiscoverer != null) {
predicateOperations.addAll(predicateBeanFactoryDiscoverer.getOperations());
}
predicateOperations.addAll(predicateDiscoverer.getOperations());
final AtomicReference<RequestPredicate> predicate = new AtomicReference<>();
routeProperties.getPredicates().forEach(predicateProperties -> {
@@ -214,7 +277,11 @@ public class RouterFunctionHolderFactory {
lowerPrecedenceFilters.forEach(builder::filter);
// translate filters
MultiValueMap<String, OperationMethod> filterOperations = filterDiscoverer.getOperations();
MultiValueMap<String, OperationMethod> filterOperations = new LinkedMultiValueMap<>();
if (filterBeanFactoryDiscoverer != null) {
filterOperations.addAll(filterBeanFactoryDiscoverer.getOperations());
}
filterOperations.addAll(filterDiscoverer.getOperations());
routeProperties.getFilters().forEach(filterProperties -> {
Map<String, Object> args = new LinkedHashMap<>(filterProperties.getArgs());
translate(filterOperations, filterProperties.getName(), args, HandlerFilterFunction.class, builder::filter);
@@ -295,7 +362,7 @@ public class RouterFunctionHolderFactory {
return operationInvoker.invoke(context);
}
private static Object bindConfigurable(OperationMethod operationMethod, Map<String, Object> args,
private Object bindConfigurable(OperationMethod operationMethod, Map<String, Object> args,
OperationParameter operationParameter) {
Class<?> configurableType = operationParameter.getType();
Configurable configurable = operationMethod.getMethod().getAnnotation(Configurable.class);
@@ -305,8 +372,8 @@ public class RouterFunctionHolderFactory {
Bindable<?> bindable = Bindable.of(configurableType);
List<ConfigurationPropertySource> propertySources = Collections
.singletonList(new MapConfigurationPropertySource(args));
// TODO: potentially deal with conversion service
Binder binder = new Binder(propertySources, null, DefaultConversionService.getSharedInstance());
Binder binder = new Binder(propertySources, null, conversionService);
Object config = binder.bindOrCreate("", bindable, new IgnoreTopLevelConverterNotFoundBindHandler());
return config;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* 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.
@@ -24,6 +24,11 @@ import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.cloud.gateway.server.mvc.common.HttpStatusHolder;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.handler.GatewayServerResponse;
@@ -34,8 +39,16 @@ import org.springframework.util.StringUtils;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* @author raccoonback
*/
public abstract class AfterFilterFunctions {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private AfterFilterFunctions() {
}
@@ -160,6 +173,38 @@ public abstract class AfterFilterFunctions {
};
}
public static BiFunction<ServerRequest, ServerResponse, ServerResponse> removeJsonAttributesResponseBody(
List<String> fieldList, boolean deleteRecursively) {
List<String> immutableFieldList = List.copyOf(fieldList);
return modifyResponseBody(String.class, String.class, APPLICATION_JSON_VALUE, (request, response, body) -> {
String responseBody = body;
if (APPLICATION_JSON.isCompatibleWith(response.headers().getContentType())) {
try {
JsonNode jsonBodyContent = OBJECT_MAPPER.readValue(responseBody, JsonNode.class);
removeJsonAttributes(jsonBodyContent, immutableFieldList, deleteRecursively);
responseBody = OBJECT_MAPPER.writeValueAsString(jsonBodyContent);
}
catch (JsonProcessingException exception) {
throw new IllegalStateException("Failed to process JSON of response body.", exception);
}
}
return responseBody;
});
}
private static void removeJsonAttributes(JsonNode jsonNode, List<String> fieldNames, boolean deleteRecursively) {
if (jsonNode instanceof ObjectNode objectNode) {
objectNode.remove(fieldNames);
}
if (deleteRecursively) {
jsonNode.forEach(childNode -> removeJsonAttributes(childNode, fieldNames, true));
}
}
public enum DedupeStrategy {
/**

View File

@@ -45,7 +45,6 @@ import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
import org.springframework.web.util.UriUtils;
import static org.springframework.cloud.gateway.server.mvc.common.MvcUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
@@ -195,7 +194,7 @@ public abstract class BeforeFilterFunctions {
String newPath = uri.getRawPath() + request.uri().getRawPath();
URI prefixedUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(newPath).build().toUri();
URI prefixedUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(newPath).build(true).toUri();
return ServerRequest.from(request).uri(prefixedUri).build();
};
}
@@ -216,7 +215,7 @@ public abstract class BeforeFilterFunctions {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(request.params());
queryParams.remove(name);
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
MultiValueMap<String, String> encodedQueryParams = MvcUtils.encodeQueryParams(queryParams);
// remove from uri
URI newUri = UriComponentsBuilder.fromUri(request.uri())
@@ -351,7 +350,7 @@ public abstract class BeforeFilterFunctions {
queryParams.add(name, replacement);
}
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
MultiValueMap<String, String> encodedQueryParams = MvcUtils.encodeQueryParams(queryParams);
URI rewrittenUri = UriComponentsBuilder.fromUri(request.uri())
.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams))
.build(true)

View File

@@ -0,0 +1,107 @@
/*
* 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.Collections;
import java.util.function.Function;
import io.github.bucket4j.BucketConfiguration;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.gateway.server.mvc.config.RouteProperties;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctionDefinition;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
@AutoConfiguration
public class FilterAutoConfiguration {
@Bean
public FilterBeanFactoryDiscoverer filterBeanFactoryDiscoverer(BeanFactory beanFactory) {
return new FilterBeanFactoryDiscoverer(beanFactory);
}
@Bean
public FilterFunctions.FilterSupplier filterFunctionsSupplier() {
return new FilterFunctions.FilterSupplier();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(BucketConfiguration.class)
static class Bucket4jFilterConfiguration {
@Bean
public Bucket4jFilterFunctions.FilterSupplier bucket4jFilterFunctionsSupplier() {
return new Bucket4jFilterFunctions.FilterSupplier();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CircuitBreaker.class)
static class CircuitBreakerFilterConfiguration {
@Bean
public CircuitBreakerFilterFunctions.FilterSupplier circuitBreakerFilterFunctionsSupplier() {
return new CircuitBreakerFilterFunctions.FilterSupplier();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(LoadBalancerClient.class)
public static class LoadBalancerHandlerConfiguration {
@Bean
public Function<RouteProperties, HandlerFunctionDefinition> lbHandlerFunctionDefinition() {
return routeProperties -> new HandlerFunctionDefinition.Default("lb", HandlerFunctions.http(),
Collections.emptyList(),
Collections.singletonList(LoadBalancerFilterFunctions.lb(routeProperties.getUri().getHost())));
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RetryTemplate.class)
static class RetryFilterConfiguration {
@Bean
public RetryFilterFunctions.FilterSupplier retryFilterFunctionsSupplier() {
return new RetryFilterFunctions.FilterSupplier();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OAuth2AuthorizedClient.class)
static class TokenRelayFilterConfiguration {
@Bean
public TokenRelayFilterFunctions.FilterSupplier tokenRelayFilterFunctionsSupplier() {
return new TokenRelayFilterFunctions.FilterSupplier();
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2023 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 org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.gateway.server.mvc.common.BeanFactoryGatewayDiscoverer;
import org.springframework.web.servlet.function.HandlerFilterFunction;
public class FilterBeanFactoryDiscoverer extends BeanFactoryGatewayDiscoverer {
protected FilterBeanFactoryDiscoverer(BeanFactory beanFactory) {
super(beanFactory);
}
@Override
public void discover() {
doDiscover(FilterSupplier.class, HandlerFilterFunction.class);
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2013-2023 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.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.config.RouteProperties;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerDiscoverer;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerSupplier;
public class LoadBalancerHandlerSupplier implements HandlerSupplier {
@Override
public Collection<Method> get() {
return Arrays.asList(getClass().getMethods());
}
public static HandlerDiscoverer.Result lb(RouteProperties routeProperties) {
return lb(routeProperties.getUri());
}
public static HandlerDiscoverer.Result lb(URI uri) {
// TODO: how to do something other than http
return new HandlerDiscoverer.Result(HandlerFunctions.http(), Collections.emptyList(),
Collections.singletonList(LoadBalancerFilterFunctions.lb(uri.getHost())));
}
}

View File

@@ -33,6 +33,7 @@ import org.springframework.core.NestedRuntimeException;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.policy.CompositeRetryPolicy;
@@ -75,6 +76,7 @@ public abstract class RetryFilterFunctions {
if (config.isCacheBody()) {
MvcUtils.getOrCacheBody(request);
}
reset(request);
ServerResponse serverResponse = next.handle(request);
if (isRetryableStatusCode(serverResponse.statusCode(), config)
@@ -86,6 +88,14 @@ public abstract class RetryFilterFunctions {
});
}
private static void reset(ServerRequest request) throws IOException {
ClientHttpResponse clientHttpResponse = MvcUtils.getAttribute(request, MvcUtils.CLIENT_RESPONSE_ATTR);
if (clientHttpResponse != null) {
clientHttpResponse.close();
MvcUtils.putAttribute(request, MvcUtils.CLIENT_RESPONSE_ATTR, null);
}
}
private static boolean isRetryableStatusCode(HttpStatusCode httpStatus, RetryConfig config) {
return config.getSeries().stream().anyMatch(series -> HttpStatus.Series.resolve(httpStatus.value()) == series);
}

View File

@@ -22,9 +22,6 @@ import java.util.LinkedHashSet;
import java.util.List;
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;
@@ -32,9 +29,6 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.function.ServerRequest;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties.PREFIX;
@ConfigurationProperties("spring.cloud.gateway.x-forwarded")
public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestHttpHeadersFilter, Ordered {
/** Default http port. */
@@ -64,310 +58,15 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
/** X-Forwarded-Prefix Header. */
public static final String X_FORWARDED_PREFIX_HEADER = "X-Forwarded-Prefix";
/** The order of the XForwardedHeadersFilter. */
private int order = 0;
private final XForwardedRequestHeadersFilterProperties properties;
/** If the XForwardedHeadersFilter is enabled. */
private boolean enabled = true;
/** If X-Forwarded-For is enabled. */
private boolean forEnabled = true;
/** If X-Forwarded-Host is enabled. */
private boolean hostEnabled = true;
/** If X-Forwarded-Port is enabled. */
private boolean portEnabled = true;
/** If X-Forwarded-Proto is enabled. */
private boolean protoEnabled = true;
/** If X-Forwarded-Prefix is enabled. */
private boolean prefixEnabled = true;
/** If appending X-Forwarded-For as a list is enabled. */
private boolean forAppend = true;
/** If appending X-Forwarded-Host as a list is enabled. */
private boolean hostAppend = true;
/** If appending X-Forwarded-Port as a list is enabled. */
private boolean portAppend = true;
/** If appending X-Forwarded-Proto as a list is enabled. */
private boolean protoAppend = true;
/** If appending X-Forwarded-Prefix as a list is enabled. */
private boolean prefixAppend = true;
@Deprecated
public XForwardedRequestHeadersFilter() {
this(new XForwardedRequestHeadersFilterProperties());
public XForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties properties) {
this.properties = properties;
}
public XForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props) {
// TODO: remove individual properties in 4.2.0
// this.properties = properties;
PropertyMapper map = PropertyMapper.get();
map.from(props::getOrder).to(o -> this.order = o);
map.from(props::isEnabled).to(b -> this.enabled = b);
map.from(props::isForEnabled).to(b -> this.forEnabled = b);
map.from(props::isHostEnabled).to(b -> this.hostEnabled = b);
map.from(props::isPortEnabled).to(b -> this.portEnabled = b);
map.from(props::isProtoEnabled).to(b -> this.protoEnabled = b);
map.from(props::isPrefixEnabled).to(b -> this.prefixEnabled = b);
map.from(props::isForAppend).to(b -> this.forAppend = b);
map.from(props::isHostAppend).to(b -> this.hostAppend = b);
map.from(props::isPortAppend).to(b -> this.portAppend = b);
map.from(props::isProtoAppend).to(b -> this.protoAppend = b);
map.from(props::isPrefixAppend).to(b -> this.prefixAppend = b);
}
@DeprecatedConfigurationProperty(replacement = PREFIX + ".order")
@Override
public int getOrder() {
return this.order;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setOrder(int)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setOrder(int order) {
this.order = order;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isEnabled()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".enabled")
public boolean isEnabled() {
return enabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setEnabled(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isForEnabled()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".for-enabled")
public boolean isForEnabled() {
return forEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setForEnabled(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setForEnabled(boolean forEnabled) {
this.forEnabled = forEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isHostEnabled()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".host-enabled")
public boolean isHostEnabled() {
return hostEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setHostEnabled(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setHostEnabled(boolean hostEnabled) {
this.hostEnabled = hostEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isPortEnabled()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".port-enabled")
public boolean isPortEnabled() {
return portEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setPortEnabled(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setPortEnabled(boolean portEnabled) {
this.portEnabled = portEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isProtoEnabled()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".proto-enabled")
public boolean isProtoEnabled() {
return protoEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setProtoEnabled(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setProtoEnabled(boolean protoEnabled) {
this.protoEnabled = protoEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isPrefixEnabled()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".prefix-enabled")
public boolean isPrefixEnabled() {
return prefixEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setPrefixEnabled(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setPrefixEnabled(boolean prefixEnabled) {
this.prefixEnabled = prefixEnabled;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isForAppend()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".for-append")
public boolean isForAppend() {
return forAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setForAppend(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setForAppend(boolean forAppend) {
this.forAppend = forAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isHostAppend()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".host-append")
public boolean isHostAppend() {
return hostAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setHostAppend(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setHostAppend(boolean hostAppend) {
this.hostAppend = hostAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isPortAppend()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".port-append")
public boolean isPortAppend() {
return portAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setPortAppend(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setPortAppend(boolean portAppend) {
this.portAppend = portAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isProtoAppend()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".proto-append")
public boolean isProtoAppend() {
return protoAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setProtoAppend(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setProtoAppend(boolean protoAppend) {
this.protoAppend = protoAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#isPrefixAppend()} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = PREFIX + ".prefix-append")
public boolean isPrefixAppend() {
return prefixAppend;
}
/**
* @deprecated since 4.1.2 for removal in 4.2.0 in favor of
* {@link XForwardedRequestHeadersFilterProperties#setPrefixAppend(boolean)} )}
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.1.2", forRemoval = true)
public void setPrefixAppend(boolean prefixAppend) {
this.prefixAppend = prefixAppend;
return properties.getOrder();
}
@Override
@@ -380,17 +79,17 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
}
InetSocketAddress remoteAddress = request.remoteAddress().orElse(null);
if (isForEnabled() && remoteAddress != null && remoteAddress.getAddress() != null) {
if (properties.isForEnabled() && remoteAddress != null && remoteAddress.getAddress() != null) {
String remoteAddr = remoteAddress.getAddress().getHostAddress();
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend());
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, properties.isForAppend());
}
String proto = request.uri().getScheme();
if (isProtoEnabled()) {
write(updated, X_FORWARDED_PROTO_HEADER, proto, isProtoAppend());
if (properties.isProtoEnabled()) {
write(updated, X_FORWARDED_PROTO_HEADER, proto, properties.isProtoAppend());
}
if (isPrefixEnabled()) {
if (properties.isPrefixEnabled()) {
// If the path of the url that the gw is routing to is a subset
// (and ending part) of the url that it is routing from then the difference
// is the prefix e.g. if request original.com/prefix/get/ is routed
@@ -419,17 +118,17 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
}
}
if (isPortEnabled()) {
if (properties.isPortEnabled()) {
String port = String.valueOf(request.uri().getPort());
if (request.uri().getPort() < 0) {
port = String.valueOf(getDefaultPort(proto));
}
write(updated, X_FORWARDED_PORT_HEADER, port, isPortAppend());
write(updated, X_FORWARDED_PORT_HEADER, port, properties.isPortAppend());
}
if (isHostEnabled()) {
if (properties.isHostEnabled()) {
String host = toHostHeader(request);
write(updated, X_FORWARDED_HOST_HEADER, host, isHostAppend());
write(updated, X_FORWARDED_HOST_HEADER, host, properties.isHostAppend());
}
return updated;
@@ -440,7 +139,7 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
if (requestUriPath != null && (originalUriPath.endsWith(requestUriPath))) {
prefix = substringBeforeLast(originalUriPath, requestUriPath);
if (prefix != null && prefix.length() > 0 && prefix.length() <= originalUri.getPath().length()) {
write(updated, X_FORWARDED_PREFIX_HEADER, prefix, isPrefixAppend());
write(updated, X_FORWARDED_PREFIX_HEADER, prefix, properties.isPrefixAppend());
}
}
}

View File

@@ -56,6 +56,7 @@ public class ClientHttpRequestFactoryProxyExchange extends AbstractProxyExchange
InputStream body = clientHttpResponse.getBody();
// put the body input stream in a request attribute so filters can read it.
MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, body);
MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_ATTR, clientHttpResponse);
ServerResponse serverResponse = GatewayServerResponse.status(clientHttpResponse.getStatusCode())
.build((req, httpServletResponse) -> {
try (clientHttpResponse) {

View File

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

View File

@@ -0,0 +1,93 @@
/*
* 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.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.Function;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.config.RouteProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.function.HandlerFilterFunction;
import org.springframework.web.servlet.function.HandlerFunction;
import org.springframework.web.servlet.function.ServerResponse;
@AutoConfiguration
public class HandlerFunctionAutoConfiguration {
@Bean
public Function<RouteProperties, HandlerFunctionDefinition> fnHandlerFunctionDefinition() {
return routeProperties -> new HandlerFunctionDefinition.Default("fn",
HandlerFunctions.fn(routeProperties.getUri().getSchemeSpecificPart()));
}
@Bean
public Function<RouteProperties, HandlerFunctionDefinition> forwardHandlerFunctionDefinition() {
return routeProperties -> new HandlerFunctionDefinition.Default("forward",
HandlerFunctions.forward(routeProperties.getUri().getPath()));
}
@Bean
public Function<RouteProperties, HandlerFunctionDefinition> httpHandlerFunctionDefinition() {
return routeProperties -> getResult("http", routeProperties.getId(), routeProperties.getUri(),
HandlerFunctions.http());
}
@Bean
public Function<RouteProperties, HandlerFunctionDefinition> httpsHandlerFunctionDefinition() {
return routeProperties -> getResult("https", routeProperties.getId(), routeProperties.getUri(),
HandlerFunctions.https());
}
@Bean
public Function<RouteProperties, HandlerFunctionDefinition> noHandlerFunctionDefinition() {
return routeProperties -> getResult("no", routeProperties.getId(), routeProperties.getUri(),
HandlerFunctions.no());
}
@Bean
public Function<RouteProperties, HandlerFunctionDefinition> streamHandlerFunctionDefinition() {
return routeProperties -> new HandlerFunctionDefinition.Default("stream",
HandlerFunctions.stream(routeProperties.getUri().getSchemeSpecificPart()));
}
private static HandlerFunctionDefinition getResult(String scheme, String id, URI uri,
HandlerFunction<ServerResponse> handlerFunction) {
HandlerFilterFunction<ServerResponse, ServerResponse> setId = setIdFilter(id);
HandlerFilterFunction<ServerResponse, ServerResponse> setRequest = setRequestUrlFilter(uri);
return new HandlerFunctionDefinition.Default(scheme, handlerFunction, Arrays.asList(setId, setRequest),
Collections.emptyList());
}
private static HandlerFilterFunction<ServerResponse, ServerResponse> setIdFilter(String id) {
return (request, next) -> {
MvcUtils.setRouteId(request, id);
return next.handle(request);
};
}
private static HandlerFilterFunction<ServerResponse, ServerResponse> setRequestUrlFilter(URI uri) {
return (request, next) -> {
MvcUtils.setRequestUrl(request, uri);
return next.handle(request);
};
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.Collections;
import java.util.List;
import org.springframework.web.servlet.function.HandlerFilterFunction;
import org.springframework.web.servlet.function.HandlerFunction;
import org.springframework.web.servlet.function.ServerResponse;
public interface HandlerFunctionDefinition {
HandlerFunction<ServerResponse> handlerFunction();
List<HandlerFilterFunction<ServerResponse, ServerResponse>> lowerPrecedenceFilters();
List<HandlerFilterFunction<ServerResponse, ServerResponse>> higherPrecedenceFilters();
record Default(String scheme, HandlerFunction<ServerResponse> handlerFunction,
List<HandlerFilterFunction<ServerResponse, ServerResponse>> lowerPrecedenceFilters,
List<HandlerFilterFunction<ServerResponse, ServerResponse>> higherPrecedenceFilters)
implements
HandlerFunctionDefinition {
public Default(String scheme, HandlerFunction<ServerResponse> handlerFunction) {
this(scheme, handlerFunction, Collections.emptyList(), Collections.emptyList());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* 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.
@@ -37,6 +37,9 @@ import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author raccoonback
*/
public class ProxyExchangeHandlerFunction
implements HandlerFunction<ServerResponse>, ApplicationListener<ContextRefreshedEvent> {
@@ -84,14 +87,14 @@ public class ProxyExchangeHandlerFunction
@Override
public ServerResponse handle(ServerRequest serverRequest) {
URI uri = uriResolver.apply(serverRequest);
boolean encoded = containsEncodedQuery(serverRequest.uri(), serverRequest.params());
MultiValueMap<String, String> params = MvcUtils.encodeQueryParams(serverRequest.params());
// @formatter:off
URI url = UriComponentsBuilder.fromUri(serverRequest.uri())
.scheme(uri.getScheme())
.host(uri.getHost())
.port(uri.getPort())
.replaceQueryParams(serverRequest.params())
.build(encoded)
.replaceQueryParams(params)
.build(true)
.toUri();
// @formatter:on
@@ -131,29 +134,6 @@ public class ProxyExchangeHandlerFunction
return filtered;
}
private static boolean containsEncodedQuery(URI uri, MultiValueMap<String, String> params) {
String rawQuery = uri.getRawQuery();
boolean encoded = (rawQuery != null && rawQuery.contains("%"))
|| (uri.getRawPath() != null && uri.getRawPath().contains("%"));
// Verify if it is really fully encoded. Treat partial encoded as unencoded.
if (encoded) {
try {
UriComponentsBuilder.fromUri(uri).replaceQueryParams(params).build(true);
return true;
}
catch (IllegalArgumentException ignored) {
if (log.isTraceEnabled()) {
log.trace("Error in containsEncodedParts", ignored);
}
}
return false;
}
return false;
}
public interface URIResolver extends Function<ServerRequest, URI> {
}

View File

@@ -72,6 +72,7 @@ public class RestClientProxyExchange extends AbstractProxyExchange {
InputStream body = clientResponse.getBody();
// put the body input stream in a request attribute so filters can read it.
MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, body);
MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_ATTR, clientResponse);
ServerResponse serverResponse = GatewayServerResponse.status(clientResponse.getStatusCode())
.build((req, httpServletResponse) -> {
try (clientResponse) {

View File

@@ -272,7 +272,12 @@ public abstract class GatewayRequestPredicates {
@Override
public void accept(RequestPredicates.Visitor visitor) {
visitor.header(name, pattern.pattern());
if (pattern != null) {
visitor.header(name, pattern.pattern());
}
else {
visitor.header(name, "");
}
}
@Override

View File

@@ -0,0 +1,41 @@
/*
* 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.predicate;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class PredicateAutoConfiguration {
@Bean
public PredicateBeanFactoryDiscoverer predicateBeanFactoryDiscoverer(BeanFactory beanFactory) {
return new PredicateBeanFactoryDiscoverer(beanFactory);
}
@Bean
MvcPredicateSupplier mvcPredicateSupplier() {
return new MvcPredicateSupplier();
}
@Bean
GatewayRequestPredicates.PredicateSupplier gatewayRequestPredicateSupplier() {
return new GatewayRequestPredicates.PredicateSupplier();
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2023 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.predicate;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.gateway.server.mvc.common.BeanFactoryGatewayDiscoverer;
import org.springframework.web.servlet.function.RequestPredicate;
public class PredicateBeanFactoryDiscoverer extends BeanFactoryGatewayDiscoverer {
protected PredicateBeanFactoryDiscoverer(BeanFactory beanFactory) {
super(beanFactory);
}
@Override
public void discover() {
doDiscover(PredicateSupplier.class, RequestPredicate.class);
}
}

View File

@@ -1,46 +1,370 @@
{
"properties": [
{
"name": "spring.cloud.gateway.mvc.form-filter.enabled",
"name": "spring.cloud.gateway.server.webmvc.form-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the form-filter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.mvc.forwarded-request-headers-filter.enabled",
"name": "spring.cloud.gateway.server.webmvc.forwarded-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the forwarded-request-headers-filter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.mvc.remove-content-length-request-headers-filter.enabled",
"name": "spring.cloud.gateway.server.webmvc.remove-content-length-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the remove-content-length-request-headers-filter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.mvc.remove-hop-by-hop-request-headers-filter.enabled",
"name": "spring.cloud.gateway.server.webmvc.remove-hop-by-hop-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the forwarded-request-headers-filter.",
"description": "Enables the remove-hop-by-hop-request-headers-filter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.mvc.remove-hop-by-hop-response-headers-filter.enabled",
"name": "spring.cloud.gateway.server.webmvc.remove-hop-by-hop-response-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the forwarded-request-headers-filter.",
"description": "Enables the remove-hop-by-hop-response-headers-filter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.mvc.transfer-encoding-normalization-request-headers-filter.enabled",
"name": "spring.cloud.gateway.server.webmvc.remove-http2-status-response-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the remove-http2-status-response-headers-filter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.server.webmvc.transfer-encoding-normalization-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the transfer-encoding-normalization-request-headers-filter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.mvc.weight-calculator-filter.enabled",
"name": "spring.cloud.gateway.server.webmvc.weight-calculator-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the weight-calculator-filter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.mvc.form-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the form-filter.",
"defaultValue": "true",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.form-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.forwarded-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the forwarded-request-headers-filter.",
"defaultValue": "true",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.forwarded-request-headers-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.remove-content-length-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the remove-content-length-request-headers-filter.",
"defaultValue": "true",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.remove-content-length-request-headers-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.remove-hop-by-hop-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the remove-hop-by-hop-request-headers-filter.",
"defaultValue": "true",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.remove-hop-by-hop-request-headers-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.remove-hop-by-hop-response-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the remove-hop-by-hop-response-headers-filter.",
"defaultValue": "true",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.remove-hop-by-hop-response-headers-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.remove-http2-status-response-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the remove-http2-status-response-headers-filter.",
"defaultValue": "true",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.remove-http2-status-response-headers-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.routes",
"type": "java.util.List<org.springframework.cloud.gateway.server.mvc.config.RouteProperties>",
"description": "List of Routes.",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.routes",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.routes-map",
"type": "java.util.LinkedHashMap<java.lang.String,org.springframework.cloud.gateway.server.mvc.config.RouteProperties>",
"description": "Map of Routes.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.routes-map",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.streaming-buffer-size",
"type": "java.lang.Integer",
"description": "Buffer size for streaming media mime-types.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties",
"defaultValue": 16384,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.streaming-buffer-size",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.streaming-media-types",
"type": "java.util.List<org.springframework.http.MediaType>",
"description": "Mime-types that are streaming.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.streaming-media-types",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.transfer-encoding-normalization-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the transfer-encoding-normalization-request-headers-filter.",
"defaultValue": "true",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.transfer-encoding-normalization-request-headers-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.weight-calculator-filter.enabled",
"type": "java.lang.Boolean",
"description": "Enables the weight-calculator-filter.",
"defaultValue": "true",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.weight-calculator-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.enabled",
"type": "java.lang.Boolean",
"description": "If the XForwardedHeadersFilter is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.for-append",
"type": "java.lang.Boolean",
"description": "If appending X-Forwarded-For as a list is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.for-append",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.for-enabled",
"type": "java.lang.Boolean",
"description": "If X-Forwarded-For is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.for-enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.host-append",
"type": "java.lang.Boolean",
"description": "If appending X-Forwarded-Host as a list is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.host-append",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.host-enabled",
"type": "java.lang.Boolean",
"description": "If X-Forwarded-Host is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.host-enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.order",
"type": "java.lang.Integer",
"description": "The order of the XForwardedHeadersFilter.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": 0,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.order",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.port-append",
"type": "java.lang.Boolean",
"description": "If appending X-Forwarded-Port as a list is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.port-append",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.port-enabled",
"type": "java.lang.Boolean",
"description": "If X-Forwarded-Port is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.port-enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.prefix-append",
"type": "java.lang.Boolean",
"description": "If appending X-Forwarded-Prefix as a list is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.prefix-append",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.prefix-enabled",
"type": "java.lang.Boolean",
"description": "If X-Forwarded-Prefix is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.prefix-enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.proto-append",
"type": "java.lang.Boolean",
"description": "If appending X-Forwarded-Proto as a list is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.proto-append",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.proto-enabled",
"type": "java.lang.Boolean",
"description": "If X-Forwarded-Proto is enabled.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.proto-enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.http-client.connect-timeout",
"type": "java.time.Duration",
"description": "The HttpClient connect timeout.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties$HttpClient",
"deprecated": true,
"deprecation": {
"replacement": "spring.http.client.connect-timeout",
"since": "4.2.0"
}
},
{
"name": "spring.cloud.gateway.mvc.http-client.read-timeout",
"type": "java.time.Duration",
"description": "The HttpClient read timeout.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties$HttpClient",
"deprecated": true,
"deprecation": {
"replacement": "spring.http.client.read-timeout",
"since": "4.2.0"
}
},
{
"name": "spring.cloud.gateway.mvc.http-client.ssl-bundle",
"type": "java.lang.String",
"description": "The name of the SSL bundle to use.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties$HttpClient",
"deprecated": true,
"deprecation": {
"replacement": "spring.http.client.ssl.bundle",
"since": "4.2.0"
}
},
{
"name": "spring.cloud.gateway.mvc.http-client.type",
"type": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties$HttpClientType",
"description": "The HttpClient type. Defaults to JDK.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties$HttpClient",
"defaultValue": "jdk",
"deprecated": true,
"deprecation": {}
}
]
}

View File

@@ -15,21 +15,10 @@
#
#
org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\
org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions.FilterSupplier,\
org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.FilterSupplier,\
org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.FilterSupplier,\
org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions.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.DefaultHandlerSupplier,\
org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerHandlerSupplier
org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier=\
org.springframework.cloud.gateway.server.mvc.predicate.MvcPredicateSupplier,\
org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.PredicateSupplier
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration.GatewayHttpClientEnvironmentPostProcessor,\
org.springframework.cloud.gateway.server.mvc.common.MultipartEnvironmentPostProcessor
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.gateway.server.mvc.config.GatewayServerWebMvcPropertiesMigrationListener

View File

@@ -1,5 +1,8 @@
org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration
org.springframework.cloud.gateway.server.mvc.GatewayMvcClassPathWarningAutoConfiguration
org.springframework.cloud.gateway.server.mvc.filter.FilterAutoConfiguration
org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctionAutoConfiguration
org.springframework.cloud.gateway.server.mvc.handler.GatewayMultipartAutoConfiguration
org.springframework.cloud.gateway.server.mvc.predicate.PredicateAutoConfiguration
org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
org.springframework.cloud.gateway.server.mvc.config.DefaultFunctionConfiguration

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2013-2023 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;
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.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.filter.FormFilter;
import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.RemoveContentLengthRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHopByHopRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHopByHopResponseHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHttp2StatusResponseHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.TransferEncodingNormalizationRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.WeightCalculatorFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("unchecked")
@SpringBootTest(properties = {}, webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("propertiesmigrationtests")
public class GatewayMvcPropertiesMigrationTests {
@Autowired
ApplicationContext context;
@Autowired
GatewayMvcProperties properties;
@SuppressWarnings("rawtypes")
@Test
public void deprecatedFilterEnabledPropertiesWork() {
assertBeanDoesNotExist(FormFilter.class);
assertBeanDoesNotExist(ForwardedRequestHeadersFilter.class);
assertBeanDoesNotExist(RemoveContentLengthRequestHeadersFilter.class);
assertBeanDoesNotExist(RemoveHopByHopRequestHeadersFilter.class);
assertBeanDoesNotExist(RemoveHopByHopResponseHeadersFilter.class);
assertBeanDoesNotExist(RemoveHttp2StatusResponseHeadersFilter.class);
assertBeanDoesNotExist(TransferEncodingNormalizationRequestHeadersFilter.class);
assertBeanDoesNotExist(WeightCalculatorFilter.class);
assertBeanDoesNotExist(XForwardedRequestHeadersFilter.class);
}
@Test
public void deprecatedRoutePropertiesWork() {
assertThat(properties.getRoutes()).hasSize(2);
assertThat(properties.getRoutesMap()).hasSize(2);
}
private void assertBeanDoesNotExist(Class<?> type) {
assertThat(context.getBeanNamesForType(type)).isEmpty();
}
@SpringBootConfiguration
@EnableAutoConfiguration
protected static class TestConfiguration {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2024 the original author or authors.
* 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.
@@ -34,7 +34,9 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.SimpleClientHttpRequestFactoryBuilder;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.gateway.server.mvc.filter.FilterAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.filter.FormFilter;
import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.RemoveContentLengthRequestHeadersFilter;
@@ -44,6 +46,9 @@ import org.springframework.cloud.gateway.server.mvc.filter.RemoveHttp2StatusResp
import org.springframework.cloud.gateway.server.mvc.filter.TransferEncodingNormalizationRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.WeightCalculatorFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctionAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateAutoConfiguration;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.ConfigurableApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@@ -109,18 +114,19 @@ public class GatewayServerMvcAutoConfigurationTests {
@Test
void filterEnabledPropertiesWork() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GatewayServerMvcAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(FilterAutoConfiguration.class, PredicateAutoConfiguration.class,
HandlerFunctionAutoConfiguration.class, GatewayServerMvcAutoConfiguration.class,
HttpClientAutoConfiguration.class, RestTemplateAutoConfiguration.class,
RestClientAutoConfiguration.class, SslAutoConfiguration.class))
.withPropertyValues("spring.cloud.gateway.mvc.form-filter.enabled=false",
"spring.cloud.gateway.mvc.forwarded-request-headers-filter.enabled=false",
"spring.cloud.gateway.mvc.remove-content-length-request-headers-filter.enabled=false",
"spring.cloud.gateway.mvc.remove-hop-by-hop-request-headers-filter.enabled=false",
"spring.cloud.gateway.mvc.remove-hop-by-hop-response-headers-filter.enabled=false",
"spring.cloud.gateway.mvc.remove-http2-status-response-headers-filter.enabled=false",
"spring.cloud.gateway.mvc.transfer-encoding-normalization-request-headers-filter.enabled=false",
"spring.cloud.gateway.mvc.weight-calculator-filter.enabled=false",
"spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.enabled=false")
.withPropertyValues("spring.cloud.gateway.server.webmvc.form-filter.enabled=false",
"spring.cloud.gateway.server.webmvc.forwarded-request-headers-filter.enabled=false",
"spring.cloud.gateway.server.webmvc.remove-content-length-request-headers-filter.enabled=false",
"spring.cloud.gateway.server.webmvc.remove-hop-by-hop-request-headers-filter.enabled=false",
"spring.cloud.gateway.server.webmvc.remove-hop-by-hop-response-headers-filter.enabled=false",
"spring.cloud.gateway.server.webmvc.remove-http2-status-response-headers-filter.enabled=false",
"spring.cloud.gateway.server.webmvc.transfer-encoding-normalization-request-headers-filter.enabled=false",
"spring.cloud.gateway.server.webmvc.weight-calculator-filter.enabled=false",
"spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.enabled=false")
.run(context -> {
assertThat(context).doesNotHaveBean(FormFilter.class);
assertThat(context).doesNotHaveBean(ForwardedRequestHeadersFilter.class);
@@ -138,11 +144,8 @@ public class GatewayServerMvcAutoConfigurationTests {
@Test
void gatewayHttpClientPropertiesWork() {
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class)
.properties("spring.main.web-application-type=none",
"spring.cloud.gateway.mvc.http-client.connect-timeout=1s",
"spring.cloud.gateway.mvc.http-client.read-timeout=2s",
"spring.cloud.gateway.mvc.http-client.ssl-bundle=mybundle",
"spring.cloud.gateway.mvc.http-client.type=autodetect",
.properties("spring.main.web-application-type=none", "spring.http.client.connect-timeout=1s",
"spring.http.client.read-timeout=2s", "spring.http.client.ssl.bundle=mybundle",
"spring.ssl.bundle.pem.mybundle.keystore.certificate=" + cert,
"spring.ssl.bundle.pem.mybundle.keystore.key=" + key)
.run();
@@ -161,7 +164,8 @@ public class GatewayServerMvcAutoConfigurationTests {
@Test
void bootHttpClientPropertiesWork() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GatewayServerMvcAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(FilterAutoConfiguration.class, PredicateAutoConfiguration.class,
HandlerFunctionAutoConfiguration.class, GatewayServerMvcAutoConfiguration.class,
HttpClientAutoConfiguration.class, RestTemplateAutoConfiguration.class,
RestClientAutoConfiguration.class, SslAutoConfiguration.class))
.withPropertyValues("spring.http.client.connect-timeout=1s", "spring.http.client.read-timeout=2s",
@@ -184,6 +188,15 @@ public class GatewayServerMvcAutoConfigurationTests {
});
}
@Test
void settingHttpClientFactoryOldPropertyWorks() {
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class)
.properties("spring.main.web-application-type=none", "spring.http.client.factory=simple")
.run();
ClientHttpRequestFactoryBuilder<?> builder = context.getBean(ClientHttpRequestFactoryBuilder.class);
assertThat(builder).isInstanceOf(SimpleClientHttpRequestFactoryBuilder.class);
}
@Test
void settingHttpClientFactoryWorks() {
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class)
@@ -193,6 +206,27 @@ public class GatewayServerMvcAutoConfigurationTests {
assertThat(builder).isInstanceOf(SimpleClientHttpRequestFactoryBuilder.class);
}
@Test
void loadBalancerFunctionHandlerAdded() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(FilterAutoConfiguration.class, PredicateAutoConfiguration.class,
HandlerFunctionAutoConfiguration.class, GatewayServerMvcAutoConfiguration.class,
HttpClientAutoConfiguration.class, RestTemplateAutoConfiguration.class,
RestClientAutoConfiguration.class))
.run(context -> assertThat(context).hasBean("lbHandlerFunctionDefinition"));
}
@Test
void loadBalancerFunctionHandlerNotAddedWhenNoLoadBalancerClientOnClasspath() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(FilterAutoConfiguration.class, PredicateAutoConfiguration.class,
HandlerFunctionAutoConfiguration.class, GatewayServerMvcAutoConfiguration.class,
HttpClientAutoConfiguration.class, RestTemplateAutoConfiguration.class,
RestClientAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(LoadBalancerClient.class))
.run(context -> assertThat(context).doesNotHaveBean("lbHandlerFunctionDefinition"));
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class TestConfig {

View File

@@ -142,8 +142,7 @@ 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", "spring.cloud.gateway.function.enabled=false" },
@SpringBootTest(properties = { "spring.http.client.factory=jdk", "spring.cloud.gateway.function.enabled=false" },
webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
@ExtendWith(OutputCaptureExtension.class)

View File

@@ -0,0 +1,62 @@
/*
* 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;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.cloud.gateway.server.mvc.filter.FilterAutoConfiguration;
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.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
/**
* Integration tests for {@link FilterAutoConfiguration.LoadBalancerHandlerConfiguration}.
*
* @author Olga Maciaszek-Sharma
*
*/
@SpringBootTest(classes = { ServerMvcLoadBalancerIntegrationTests.Config.class, FilterAutoConfiguration.class },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
@ActiveProfiles("lb")
public class ServerMvcLoadBalancerIntegrationTests {
@LocalServerPort
int port;
@Autowired
TestRestClient testRestClient;
@Test
void shouldUseLbHandlerFunctionDefinitionToResolveHost() {
testRestClient.get().uri("http://localhost:" + port + "/test").exchange().expectStatus().isOk();
}
@SpringBootApplication
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
static class Config {
}
}

View File

@@ -43,7 +43,7 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
@SuppressWarnings("unchecked")
@SpringBootTest(properties = { "spring.cloud.gateway.mvc.http-client.type=jdk" },
@SpringBootTest(properties = { "spring.http.client.factory=jdk" },
webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
public class VanillaRouterFunctionTests {

View File

@@ -199,14 +199,13 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
void refreshWorks(ConfigurableApplicationContext context) {
Map<String, RouterFunction> routerFunctions = getRouterFunctions(context);
assertThat(routerFunctions).hasSize(6);
TestPropertyValues
.of("spring.cloud.gateway.mvc.routesMap.route3.uri=https://example3.com",
"spring.cloud.gateway.mvc.routesMap.route3.predicates[0].name=Path",
"spring.cloud.gateway.mvc.routesMap.route3.predicates[0].args.pattern=/anything/mapRoute3",
"spring.cloud.gateway.mvc.routesMap.route3.filters[0].Name=HttpbinUriResolver",
"spring.cloud.gateway.mvc.routesMap.route3.filters[1].Name=AddRequestHeader",
"spring.cloud.gateway.mvc.routesMap.route3.filters[1].args.name=X-Test",
"spring.cloud.gateway.mvc.routesMap.route3.filters[1].args.values=mapRoute3")
TestPropertyValues.of("spring.cloud.gateway.server.webmvc.routesMap.route3.uri=https://example3.com",
"spring.cloud.gateway.server.webmvc.routesMap.route3.predicates[0].name=Path",
"spring.cloud.gateway.server.webmvc.routesMap.route3.predicates[0].args.pattern=/anything/mapRoute3",
"spring.cloud.gateway.server.webmvc.routesMap.route3.filters[0].Name=HttpbinUriResolver",
"spring.cloud.gateway.server.webmvc.routesMap.route3.filters[1].Name=AddRequestHeader",
"spring.cloud.gateway.server.webmvc.routesMap.route3.filters[1].args.name=X-Test",
"spring.cloud.gateway.server.webmvc.routesMap.route3.filters[1].args.values=mapRoute3")
.applyTo(context);
ContextRefresher contextRefresher = context.getBean(ContextRefresher.class);
contextRefresher.refresh();

View File

@@ -0,0 +1,218 @@
/*
* Copyright 2025-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.List;
import java.util.Map;
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.HttpbinTestcontainers;
import org.springframework.cloud.gateway.server.mvc.test.HttpbinUriResolver;
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.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
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.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.modifyResponseBody;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.removeJsonAttributesResponseBody;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
/**
* @author raccoonback
*/
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
class AfterFilterFunctionsTests {
@Autowired
TestRestClient restClient;
@Test
void doesNotRemoveJsonAttributes() {
restClient.get()
.uri("/anything/does_not/remove_json_attributes")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).containsEntry("foo", "bar");
assertThat(res.getResponseBody()).containsEntry("baz", "qux");
});
}
@Test
void removeJsonAttributesToAvoidBeingRecursive() {
restClient.get()
.uri("/anything/remove_json_attributes_to_avoid_being_recursive")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).doesNotContainKey("foo");
assertThat(res.getResponseBody()).containsEntry("baz", "qux");
});
}
@Test
void removeJsonAttributesRecursively() {
restClient.get()
.uri("/anything/remove_json_attributes_recursively")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).containsKey("foo");
assertThat((Map<String, String>) res.getResponseBody().get("foo")).containsEntry("bar", "A");
assertThat(res.getResponseBody()).containsEntry("quux", "C");
assertThat(res.getResponseBody()).doesNotContainKey("qux");
});
}
@Test
void raisedErrorWhenRemoveJsonAttributes() {
restClient.get()
.uri("/anything/raised_error_when_remove_json_attributes")
.exchange()
.expectStatus()
.is5xxServerError()
.expectBody(String.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).isEqualTo("Failed to process JSON of response body.");
});
}
@SpringBootConfiguration
@EnableAutoConfiguration
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
protected static class TestConfiguration {
@Bean
public RouterFunction<ServerResponse> doesNotRemoveJsonAttributes() {
// @formatter:off
return route("does_not_remove_json_attributes")
.GET("/anything/does_not/remove_json_attributes", http())
.before(new HttpbinUriResolver())
.after(
removeJsonAttributesResponseBody(List.of("quux"), true)
)
.after(
modifyResponseBody(
String.class,
String.class,
MediaType.APPLICATION_JSON_VALUE,
(request, response, s) -> "{\"foo\": \"bar\", \"baz\": \"qux\"}"
)
)
.build();
// @formatter:on
}
@Bean
public RouterFunction<ServerResponse> removeJsonAttributesToAvoidBeingRecursively() {
// @formatter:off
return route("remove_json_attributes_to_avoid_being_recursive")
.GET("/anything/remove_json_attributes_to_avoid_being_recursive", http())
.before(new HttpbinUriResolver())
.after(
removeJsonAttributesResponseBody(List.of("foo"), false)
)
.after(
modifyResponseBody(
String.class,
String.class,
MediaType.APPLICATION_JSON_VALUE,
(request, response, s) -> "{\"foo\": \"bar\", \"baz\": \"qux\"}"
)
)
.build();
// @formatter:on
}
@Bean
public RouterFunction<ServerResponse> removeJsonAttributesRecursively() {
// @formatter:off
return route("remove_json_attributes_recursively")
.GET("/anything/remove_json_attributes_recursively", http())
.before(new HttpbinUriResolver())
.after(
removeJsonAttributesResponseBody(List.of("qux"), true)
)
.after(
modifyResponseBody(
String.class,
String.class,
MediaType.APPLICATION_JSON_VALUE,
(request, response, s) -> "{\"foo\": { \"bar\": \"A\", \"qux\": \"B\"}, \"quux\": \"C\", \"qux\": {\"corge\": \"D\"}}"
)
)
.build();
// @formatter:on
}
@Bean
public RouterFunction<ServerResponse> raisedErrorWhenRemoveJsonAttributes() {
// @formatter:off
return route("raised_error_when_remove_json_attributes")
.GET("/anything/raised_error_when_remove_json_attributes", http())
.before(new HttpbinUriResolver())
.after(
removeJsonAttributesResponseBody(List.of("qux"), true)
)
.after(
modifyResponseBody(
String.class,
String.class,
MediaType.APPLICATION_JSON_VALUE,
(request, response, s) -> "{\"invalid_json\": 123"
)
)
.build();
// @formatter:on
}
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<String> handleIllegalException(IllegalStateException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage());
}
}
}
}

View File

@@ -119,6 +119,7 @@ class BeforeFilterFunctionsTests {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path")
.param("foo[]", "bar")
.param("baz", "qux")
.param("quux", "corge+")
.buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
@@ -126,7 +127,24 @@ class BeforeFilterFunctionsTests {
ServerRequest result = BeforeFilterFunctions.rewriteRequestParameter("foo[]", "replacement[]").apply(request);
assertThat(result.param("foo[]")).isPresent().hasValue("replacement[]");
assertThat(result.uri().toString()).hasToString("http://localhost/path?baz=qux&foo%5B%5D=replacement%5B%5D");
assertThat(result.param("quux")).isPresent().hasValue("corge+");
assertThat(result.uri().toString())
.hasToString("http://localhost/path?quux=corge%2B&baz=qux&foo%5B%5D=replacement%5B%5D");
}
@Test
void rewritePlusSignRequestParameter() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path")
.param("foo", "bar")
.param("baz", "qux")
.buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ServerRequest result = BeforeFilterFunctions.rewriteRequestParameter("foo", "replacement+").apply(request);
assertThat(result.param("foo")).isPresent().hasValue("replacement+");
assertThat(result.uri().toString()).hasToString("http://localhost/path?baz=qux&foo=replacement%2B");
}
@Test
@@ -134,6 +152,7 @@ class BeforeFilterFunctionsTests {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path")
.param("foo", "bar")
.param("baz[]", "qux[]")
.param("quux", "corge+")
.buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
@@ -141,7 +160,8 @@ class BeforeFilterFunctionsTests {
ServerRequest result = BeforeFilterFunctions.rewriteRequestParameter("foo", "replacement").apply(request);
assertThat(result.param("foo")).isPresent().hasValue("replacement");
assertThat(result.uri().toString()).hasToString("http://localhost/path?baz%5B%5D=qux%5B%5D&foo=replacement");
assertThat(result.uri().toString())
.hasToString("http://localhost/path?baz%5B%5D=qux%5B%5D&quux=corge%2B&foo=replacement");
}
@Test
@@ -245,4 +265,26 @@ class BeforeFilterFunctionsTests {
.isEqualTo("/modified/path/with%E2%80%93en%E2%80%93dashes%20and%20spaces");
}
@Test
void prefixPath() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get").buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ServerRequest modified = BeforeFilterFunctions.prefixPath("/prefix").apply(request);
assertThat(modified.uri().getRawPath()).isEqualTo("/prefix/get");
}
@Test
void prefixEncodedPath() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get/é").buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ServerRequest modified = BeforeFilterFunctions.prefixPath("/pre fix").apply(request);
assertThat(modified.uri().getRawPath()).isEqualTo("/pre%20fix/get/%C3%A9");
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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 org.junit.jupiter.api.Test;
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.invoke.reflect.OperationMethod;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class FilterDiscovererTests {
@Test
void contextLoads() {
MultiValueMap<String, OperationMethod> operations = new FilterDiscoverer().getOperations();
assertThat(operations).isNotEmpty();
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class Config {
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2025-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.net.URI;
import java.util.Collections;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.server.mvc.common.AbstractProxyExchange;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.RequestHttpHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.ResponseHttpHeadersFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author raccoonback
*/
class ProxyExchangeHandlerFunctionTest {
@Test
void keepOriginalEncodingOfQueryParameter() {
TestProxyExchange proxyExchange = new TestProxyExchange();
ProxyExchangeHandlerFunction function = new ProxyExchangeHandlerFunction(proxyExchange, new ObjectProvider<>() {
@Override
public RequestHttpHeadersFilter getObject() throws BeansException {
return null;
}
@Override
public RequestHttpHeadersFilter getObject(Object... args) throws BeansException {
return null;
}
@Override
public RequestHttpHeadersFilter getIfAvailable() throws BeansException {
return null;
}
@Override
public RequestHttpHeadersFilter getIfUnique() throws BeansException {
return null;
}
@Override
public Stream<RequestHttpHeadersFilter> orderedStream() {
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
}
}, new ObjectProvider<>() {
@Override
public ResponseHttpHeadersFilter getObject() throws BeansException {
return null;
}
@Override
public ResponseHttpHeadersFilter getObject(Object... args) throws BeansException {
return null;
}
@Override
public ResponseHttpHeadersFilter getIfAvailable() throws BeansException {
return null;
}
@Override
public ResponseHttpHeadersFilter getIfUnique() throws BeansException {
return null;
}
@Override
public Stream<ResponseHttpHeadersFilter> orderedStream() {
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
}
});
function.onApplicationEvent(null);
MockHttpServletRequest servletRequest = MockMvcRequestBuilders
.get("http://localhost/é?foo=value1 value2&bar=value3=&qux=value4+")
.buildRequest(null);
servletRequest.setAttribute(MvcUtils.GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost:8080"));
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
function.handle(request);
URI uri = proxyExchange.getRequest().getUri();
assertThat(uri).hasToString("http://localhost:8080/%C3%A9?foo=value1%20value2&bar=value3%3D&qux=value4%2B")
.hasPath("")
.hasParameter("foo", "value1 value2")
.hasParameter("bar", "value3=")
.hasParameter("qux", "value4+");
}
private class TestProxyExchange extends AbstractProxyExchange {
private Request request;
protected TestProxyExchange() {
super(new GatewayMvcProperties());
}
@Override
public ServerResponse exchange(Request request) {
this.request = request;
return ServerResponse.ok().build();
}
public Request getRequest() {
return request;
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2013-2023 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.predicate;
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.invoke.reflect.OperationMethod;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PredicateBeanFactoryDiscovererTests {
@Autowired
PredicateBeanFactoryDiscoverer discoverer;
@Test
void contextLoads() {
MultiValueMap<String, OperationMethod> operations = discoverer.getOperations();
assertThat(operations).isNotEmpty();
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class Config {
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2013-2023 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.test;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier;
import org.springframework.web.servlet.function.RequestPredicate;
public class TestPredicateSupplier implements PredicateSupplier {
public static RequestPredicate alwaysTrue() {
return request -> true;
}
@Override
public Collection<Method> get() {
return List.of(TestPredicateSupplier.class.getMethods());
}
}

View File

@@ -1,2 +1,5 @@
org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier=\
org.springframework.cloud.gateway.server.mvc.test.TestPredicateSupplier
org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\
org.springframework.cloud.gateway.server.mvc.test.TestFilterSupplier

View File

@@ -1,4 +1,4 @@
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routesMap:
testsimplefunction:
uri: fn:upper

View File

@@ -0,0 +1,11 @@
spring:
cloud:
gateway:
mvc:
routes:
- id: test
uri: lb://httpbin
predicates:
- Path=/test/**
filters:
- StripPrefix=1

View File

@@ -1,4 +1,4 @@
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routesMap:
route1:
uri: https://example1.com

View File

@@ -0,0 +1,47 @@
spring.cloud.gateway.mvc:
form-filter.enabled: false
forwarded-request-headers-filter.enabled: false
remove-content-length-request-headers-filter.enabled: false
remove-hop-by-hop-request-headers-filter.enabled: false
remove-hop-by-hop-response-headers-filter.enabled: false
remove-http2-status-response-headers-filter.enabled: false
transfer-encoding-normalization-request-headers-filter.enabled: false
weight-calculator-filter.enabled: false
x-forwarded-request-headers-filter.enabled: false
routes:
- id: listRoute1
uri: https://examplel1.com
predicates:
- name: Method
args:
methods: GET
- name: Path
args:
pattern: /anything/listRoute1
filters:
- HttpbinUriResolver=
- AddRequestHeader=X-Test,listRoute1
- id: listRoute2
uri: https://examplel2.com
predicates:
- name: Method
args:
methods: GET
- name: Path
args:
pattern: /anything/listRoute2
filters:
- HttpbinUriResolver=
- AddRequestHeader=X-Test,listRoute2
routesMap:
route1:
uri: https://example1.com
predicates:
- Path=/anything/example1
route2:
uri: https://example2.com
predicates:
- Path=/anything/example2
logging:
level:
org.springframework.cloud.gateway.server.mvc: TRACE

View File

@@ -1,4 +1,4 @@
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routesMap:
testsimplestream:
uri: stream:hello-out-0

View File

@@ -1,5 +1,5 @@
strip.prefix.static.uri: http://${httpbin.host}:${httpbin.port}
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routes:
- id: strip_prefix_static_port_config
uri: ${strip.prefix.static.uri}

View File

@@ -1,4 +1,4 @@
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routes:
- id: weight_high_test
uri: https://examplel1.com

View File

@@ -16,7 +16,7 @@
<description>Spring Cloud Gateway Server</description>
<properties>
<main.basedir>${basedir}/..</main.basedir>
<grpc.version>1.71.0</grpc.version>
<grpc.version>1.72.0</grpc.version>
<context-propagation.version>1.0.0</context-propagation.version>
</properties>
@@ -192,6 +192,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
@@ -212,11 +217,6 @@
<artifactId>spring-cloud-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>

View File

@@ -57,6 +57,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.embedded.NettyWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundles;
@@ -110,6 +111,7 @@ import org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties;
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetRequestHostHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
@@ -192,9 +194,10 @@ import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyReques
* @author Mete Alpaslan Katırcıoğlu
* @author Alberto C. Ríos
* @author Olga Maciaszek-Sharma
* @author FuYiNan Guo
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class })
@AutoConfigureAfter({ GatewayReactiveLoadBalancerClientAutoConfiguration.class,
@@ -270,7 +273,7 @@ public class GatewayAutoConfiguration {
@Bean
@ConditionalOnClass(name = "org.springframework.cloud.client.discovery.event.HeartbeatMonitor")
@ConditionalOnProperty(prefix = GatewayProperties.PREFIX, name = ".route-refresh-listener.enabled",
@ConditionalOnProperty(prefix = GatewayProperties.PREFIX, name = "route-refresh-listener.enabled",
matchIfMissing = true)
public RouteRefreshListener routeRefreshListener(ApplicationEventPublisher publisher) {
return new RouteRefreshListener(publisher);
@@ -288,7 +291,7 @@ public class GatewayAutoConfiguration {
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.globalcors.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.globalcors.enabled", matchIfMissing = true)
public CorsGatewayFilterApplicationListener corsGatewayFilterApplicationListener(
GlobalCorsProperties globalCorsProperties, RoutePredicateHandlerMapping routePredicateHandlerMapping,
RouteLocator routeLocator) {
@@ -316,9 +319,10 @@ public class GatewayAutoConfiguration {
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.forwarded.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.forwarded.enabled", matchIfMissing = true)
public ForwardedHeadersFilter forwardedHeadersFilter(Environment env, ServerProperties serverProperties) {
boolean forwardedByEnabled = env.getProperty("spring.cloud.gateway.forwarded.by.enabled", Boolean.class, false);
boolean forwardedByEnabled = env.getProperty("spring.cloud.gateway.server.webflux.forwarded.by.enabled",
Boolean.class, false);
ForwardedHeadersFilter forwardedHeadersFilter = new ForwardedHeadersFilter();
forwardedHeadersFilter.setForwardedByEnabled(forwardedByEnabled);
forwardedHeadersFilter.setServerPort(serverProperties.getPort());
@@ -333,7 +337,7 @@ public class GatewayAutoConfiguration {
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.x-forwarded.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.x-forwarded.enabled", matchIfMissing = true)
public XForwardedHeadersFilter xForwardedHeadersFilter() {
return new XForwardedHeadersFilter();
}
@@ -466,7 +470,8 @@ public class GatewayAutoConfiguration {
@Bean
@ConditionalOnEnabledPredicate
public HostRoutePredicateFactory hostRoutePredicateFactory(Environment env) {
boolean includePort = env.getProperty("spring.cloud.gateway.predicate.host.include-port", Boolean.class, true);
boolean includePort = env.getProperty("spring.cloud.gateway.server.webflux.predicate.host.include-port",
Boolean.class, true);
return new HostRoutePredicateFactory(includePort);
}
@@ -478,8 +483,8 @@ public class GatewayAutoConfiguration {
@Bean
@ConditionalOnEnabledPredicate
public PathRoutePredicateFactory pathRoutePredicateFactory() {
return new PathRoutePredicateFactory();
public PathRoutePredicateFactory pathRoutePredicateFactory(WebFluxProperties webFluxProperties) {
return new PathRoutePredicateFactory(webFluxProperties);
}
@Bean
@@ -718,6 +723,12 @@ public class GatewayAutoConfiguration {
return new RequestHeaderToRequestUriGatewayFilterFactory();
}
@Bean
@ConditionalOnEnabledFilter
public SetRequestUriGatewayFilterFactory setRequestUriGatewayFilterFactory() {
return new SetRequestUriGatewayFilterFactory();
}
@Bean
@ConditionalOnEnabledFilter
public RequestSizeGatewayFilterFactory requestSizeGatewayFilterFactory() {
@@ -767,7 +778,7 @@ public class GatewayAutoConfiguration {
protected final Log logger = LogFactory.getLog(getClass());
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.httpserver.wiretap")
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.httpserver.wiretap")
public NettyWebServerFactoryCustomizer nettyServerWiretapCustomizer(Environment environment,
ServerProperties serverProperties) {
return new NettyWebServerFactoryCustomizer(environment, serverProperties) {
@@ -851,7 +862,8 @@ public class GatewayAutoConfiguration {
protected static class GatewayActuatorConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.actuator.verbose.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.actuator.verbose.enabled",
matchIfMissing = true)
@ConditionalOnAvailableEndpoint
public GatewayControllerEndpoint gatewayControllerEndpoint(List<GlobalFilter> globalFilters,
List<GatewayFilterFactory> gatewayFilters, List<RoutePredicateFactory> routePredicates,
@@ -881,7 +893,8 @@ public class GatewayAutoConfiguration {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = "spring.cloud.gateway.actuator.verbose.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.actuator.verbose.enabled",
matchIfMissing = true)
static class VerboseDisabled {
}
@@ -889,7 +902,7 @@ public class GatewayAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.enabled", matchIfMissing = true)
@ConditionalOnClass({ OAuth2AuthorizedClient.class, SecurityWebFilterChain.class, SecurityProperties.class })
@ConditionalOnEnabledFilter(TokenRelayGatewayFilterFactory.class)
protected static class TokenRelayConfiguration {

View File

@@ -29,7 +29,7 @@ import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(GatewayAutoConfiguration.class)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".enabled", matchIfMissing = true)
public class GatewayClassPathWarningAutoConfiguration {
private static final Log log = LogFactory.getLog(GatewayClassPathWarningAutoConfiguration.class);

View File

@@ -38,7 +38,7 @@ import org.springframework.web.reactive.DispatcherHandler;
@AutoConfigureAfter(ContextFunctionCatalogAutoConfiguration.class)
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, GatewayAutoConfiguration.class })
@ConditionalOnClass({ FunctionCatalog.class, DispatcherHandler.class })
@ConditionalOnProperty(name = "spring.cloud.gateway.function.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".function.enabled", matchIfMissing = true)
class GatewayFunctionAutoConfiguration {
@Bean

View File

@@ -21,7 +21,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Ryan Baxter
*/
@ConfigurationProperties("spring.cloud.gateway.loadbalancer")
@ConfigurationProperties(GatewayProperties.PREFIX + ".loadbalancer")
public class GatewayLoadBalancerProperties {
private boolean use404;

View File

@@ -28,7 +28,7 @@ import org.springframework.validation.annotation.Validated;
/**
* @author Ingyu Hwang
*/
@ConfigurationProperties("spring.cloud.gateway.metrics")
@ConfigurationProperties(GatewayProperties.PREFIX + ".metrics")
@Validated
public class GatewayMetricsProperties {

View File

@@ -42,7 +42,7 @@ public class GatewayProperties {
/**
* Properties prefix.
*/
public static final String PREFIX = "spring.cloud.gateway";
public static final String PREFIX = "spring.cloud.gateway.server.webflux";
private final Log logger = LogFactory.getLog(getClass());

View File

@@ -50,7 +50,7 @@ import org.springframework.web.reactive.DispatcherHandler;
@AutoConfigureBefore(GatewayAutoConfiguration.class)
@ConditionalOnBean(ReactiveRedisTemplate.class)
@ConditionalOnClass({ RedisTemplate.class, DispatcherHandler.class })
@ConditionalOnProperty(name = "spring.cloud.gateway.redis.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".redis.enabled", matchIfMissing = true)
class GatewayRedisAutoConfiguration {
@Bean
@@ -72,7 +72,7 @@ class GatewayRedisAutoConfiguration {
}
@Bean
@ConditionalOnProperty(value = "spring.cloud.gateway.redis-route-definition-repository.enabled",
@ConditionalOnProperty(value = GatewayProperties.PREFIX + ".redis-route-definition-repository.enabled",
havingValue = "true")
@ConditionalOnClass(ReactiveRedisTemplate.class)
public RedisRouteDefinitionRepository redisRouteDefinitionRepository(

View File

@@ -36,7 +36,7 @@ import org.springframework.web.reactive.DispatcherHandler;
* @author Ryan Baxter
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".enabled", matchIfMissing = true)
@AutoConfigureAfter({ ReactiveResilience4JAutoConfiguration.class })
@ConditionalOnClass({ DispatcherHandler.class, ReactiveResilience4JAutoConfiguration.class,
ReactiveCircuitBreakerFactory.class, ReactiveResilience4JCircuitBreakerFactory.class })

View File

@@ -0,0 +1,183 @@
/*
* 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.config;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.context.properties.source.IterableConfigurationPropertySource;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.boot.origin.PropertySourceOrigin;
import org.springframework.boot.origin.TextResourceOrigin;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
class GatewayServerWebfluxPropertiesMigrationListener implements ApplicationListener<SpringApplicationEvent> {
private static final Log logger = LogFactory.getLog(GatewayServerWebfluxPropertiesMigrationListener.class);
private static final String PROPERTIES_MIGRATOR_CLASS = "org.springframework.boot.context.properties.migrator.PropertiesMigrationListener";
private static final String DEPRECATED_ROOT = "spring.cloud.gateway";
private static final String DEPRECATED_ROUTES_LIST_KEY = DEPRECATED_ROOT + ".routes";
private static final String GATEWAY_PROPERTY_SOURCE_PREFIX = "migrategatewayflux";
private static final String NEW_ROUTES_LIST_KEY = GatewayProperties.PREFIX + ".routes";
private final List<Migration> routesMigrations = new ArrayList<>();
@Override
public void onApplicationEvent(SpringApplicationEvent event) {
// only run if spring-boot-properties-migrator is on the classpath
if (!ClassUtils.isPresent(PROPERTIES_MIGRATOR_CLASS, null)) {
return;
}
if (event instanceof ApplicationPreparedEvent preparedEvent) {
onApplicationPreparedEvent(preparedEvent);
}
if (event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) {
logLegacyPropertiesReport();
}
}
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
// find deprecated keys
ConfigurableEnvironment env = event.getApplicationContext().getEnvironment();
ConfigurationPropertySources.get(env).forEach(propertySource -> {
routesMigrations.addAll(migrate(env, propertySource, GATEWAY_PROPERTY_SOURCE_PREFIX + "routes-",
DEPRECATED_ROUTES_LIST_KEY, NEW_ROUTES_LIST_KEY));
});
}
private List<Migration> migrate(ConfigurableEnvironment env, ConfigurationPropertySource propertySource,
String propertySourcePrefix, String deprecatedKey, String newKeyPrefix) {
List<Migration> migrations = new ArrayList<>();
if (propertySource instanceof IterableConfigurationPropertySource iterableSource) {
ConfigurationPropertyName routesParentName = ConfigurationPropertyName.of(deprecatedKey);
List<ConfigurationPropertyName> matchingConfigProps = iterableSource.filter(n -> {
if (n.getNumberOfElements() < routesParentName.getNumberOfElements()) {
return false;
}
ConfigurationPropertyName chop = n.chop(routesParentName.getNumberOfElements());
return routesParentName.equals(chop);
}).stream().toList();
if (!matchingConfigProps.isEmpty()) {
String originalPropertySourceName;
if (propertySource.getUnderlyingSource() instanceof PropertySource<?> underlyingSource) {
originalPropertySourceName = underlyingSource.getName();
}
else {
originalPropertySourceName = propertySource.getUnderlyingSource().toString();
}
String newPropertySourceName = propertySourcePrefix + originalPropertySourceName;
Map<String, OriginTrackedValue> content = new LinkedHashMap<>();
// migrate to new keys
for (ConfigurationPropertyName originalPropertyName : matchingConfigProps) {
ConfigurationPropertyName suffix = originalPropertyName
.subName(routesParentName.getNumberOfElements());
ConfigurationPropertyName newProperty = ConfigurationPropertyName.of(newKeyPrefix).append(suffix);
ConfigurationProperty configurationProperty = propertySource
.getConfigurationProperty(originalPropertyName);
Object value = configurationProperty.getValue();
OriginTrackedValue originTrackedValue = OriginTrackedValue.of(value,
configurationProperty.getOrigin());
content.put(newProperty.toString(), originTrackedValue);
migrations.add(new Migration(originalPropertySourceName, originalPropertyName,
configurationProperty, newProperty));
}
env.getPropertySources()
.addBefore(originalPropertySourceName,
new OriginTrackedMapPropertySource(newPropertySourceName, content));
}
}
return migrations;
}
private void logLegacyPropertiesReport() {
// log warnings
if (!routesMigrations.isEmpty()) {
LinkedMultiValueMap<String, Migration> content = new LinkedMultiValueMap<>();
routesMigrations.forEach(migration -> content.add(migration.originalPropertySourceName(), migration));
StringBuilder report = new StringBuilder();
report.append(String
.format("%nThe use of configuration keys that have been renamed was found in the environment:%n%n"));
content.forEach((name, properties) -> {
report.append(String.format("Property source '%s':%n", name));
// properties.sort(PropertyMigration.COMPARATOR);
properties.forEach((property) -> {
ConfigurationPropertyName originalPropertyName = property.originalPropertyName();
report.append(String.format("\tKey: %s%n", originalPropertyName));
Integer lineNumber = property.determineLineNumber();
if (lineNumber != null) {
report.append(String.format("\t\tLine: %d%n", lineNumber));
}
report.append(String.format("\t\tReplacement: %s%n", property.newProperty().toString()));
});
report.append(String.format("%n"));
});
report.append(String.format("%n"));
report.append("Each configuration key has been temporarily mapped to its "
+ "replacement for your convenience. To silence this warning, please "
+ "update your configuration to use the new keys.");
report.append(String.format("%n"));
logger.warn(report.toString());
}
}
private record Migration(String originalPropertySourceName, ConfigurationPropertyName originalPropertyName,
ConfigurationProperty originalProperty, ConfigurationPropertyName newProperty) {
private Integer determineLineNumber() {
Origin origin = originalProperty.getOrigin();
if (origin instanceof PropertySourceOrigin propertySourceOrigin) {
origin = propertySourceOrigin.getOrigin();
}
if (origin instanceof TextResourceOrigin textOrigin) {
if (textOrigin.getLocation() != null) {
return textOrigin.getLocation().getLine() + 1;
}
}
return null;
}
}
}

View File

@@ -35,7 +35,7 @@ import org.springframework.web.reactive.DispatcherHandler;
@AutoConfigureAfter(BindingServiceConfiguration.class)
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, GatewayAutoConfiguration.class })
@ConditionalOnClass({ StreamBridge.class, DispatcherHandler.class })
@ConditionalOnProperty(name = "spring.cloud.gateway.stream.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".stream.enabled", matchIfMissing = true)
class GatewayStreamAutoConfiguration {
@Bean

View File

@@ -27,7 +27,7 @@ import org.springframework.web.cors.CorsConfiguration;
* Configuration properties for global configuration of cors. See
* {@link RoutePredicateHandlerMapping}
*/
@ConfigurationProperties("spring.cloud.gateway.globalcors")
@ConfigurationProperties(GatewayProperties.PREFIX + ".globalcors")
public class GlobalCorsProperties {
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();

View File

@@ -32,7 +32,7 @@ import org.springframework.validation.annotation.Validated;
/**
* Configuration properties for the Netty {@link reactor.netty.http.client.HttpClient}.
*/
@ConfigurationProperties("spring.cloud.gateway.httpclient")
@ConfigurationProperties(GatewayProperties.PREFIX + ".httpclient")
@Validated
public class HttpClientProperties {

View File

@@ -108,17 +108,19 @@ public class LocalResponseCacheAutoConfiguration {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(value = "spring.cloud.gateway.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnProperty(value = GatewayProperties.PREFIX + ".enabled", havingValue = "true",
matchIfMissing = true)
static class OnGatewayPropertyEnabled {
}
@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true")
@ConditionalOnProperty(value = GatewayProperties.PREFIX + ".filter.local-response-cache.enabled",
havingValue = "true")
static class OnLocalResponseCachePropertyEnabled {
}
@ConditionalOnProperty(name = "spring.cloud.gateway.global-filter.local-response-cache.enabled",
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".global-filter.local-response-cache.enabled",
havingValue = "true", matchIfMissing = true)
static class OnGlobalLocalResponseCachePropertyEnabled {

View File

@@ -30,7 +30,7 @@ import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SimpleUrlHandlerMapping.class)
@ConditionalOnProperty(name = "spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping",
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".globalcors.add-to-simple-url-handler-mapping",
matchIfMissing = false)
public class SimpleUrlHandlerMappingGlobalCorsAutoConfiguration {

View File

@@ -34,7 +34,7 @@ import static org.springframework.boot.autoconfigure.condition.ConditionMessage.
public abstract class OnEnabledComponent<T> extends SpringBootCondition implements ConfigurationCondition {
private static final String PREFIX = "spring.cloud.gateway.";
private static final String PREFIX = "spring.cloud.gateway.server.webflux.";
private static final String SUFFIX = ".enabled";

View File

@@ -24,7 +24,7 @@ import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.core.style.ToStringCreator;
@ConfigurationProperties("spring.cloud.gateway.discovery.locator")
@ConfigurationProperties("spring.cloud.gateway.server.webflux.discovery.locator")
public class DiscoveryLocatorProperties {
/** Flag that enables DiscoveryClient gateway integration. */

View File

@@ -45,7 +45,7 @@ import static org.springframework.cloud.gateway.support.NameUtils.normalizeRoute
* @author Spencer Gibb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.enabled", matchIfMissing = true)
@AutoConfigureBefore(GatewayAutoConfiguration.class)
@AutoConfigureAfter(CompositeDiscoveryClientAutoConfiguration.class)
@ConditionalOnClass({ DispatcherHandler.class, CompositeDiscoveryClientAutoConfiguration.class })
@@ -92,7 +92,7 @@ public class GatewayDiscoveryClientAutoConfiguration {
public static class ReactiveDiscoveryClientRouteDefinitionLocatorConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled")
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.discovery.locator.enabled")
public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
ReactiveDiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -254,7 +254,8 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
* @return the configured HttpClient.
*/
protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) {
Object connectTimeoutAttr = route.getMetadata().get(CONNECT_TIMEOUT_ATTR);
Object connectTimeoutAttr = route.getMetadata().get(CONNECT_TIMEOUT_ATTR) != null
? route.getMetadata().get(CONNECT_TIMEOUT_ATTR) : properties.getConnectTimeout();
if (connectTimeoutAttr != null) {
Integer connectTimeout = getInteger(connectTimeoutAttr);
return this.httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout);

View File

@@ -25,7 +25,7 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.event.RefreshRoutesResultEvent;
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
import org.springframework.cloud.gateway.route.Route;
@@ -34,14 +34,14 @@ import org.springframework.context.ApplicationListener;
import org.springframework.web.cors.CorsConfiguration;
/**
* This class updates Cors configuration each time a {@link RefreshRoutesEvent} is
* This class updates Cors configuration each time a {@link RefreshRoutesResultEvent} is
* consumed. The {@link Route}'s predicates are inspected for a
* {@link PathRoutePredicateFactory} and the first pattern is used.
*
* @author Fredrich Ombico
* @author Abel Salgado Romero
*/
public class CorsGatewayFilterApplicationListener implements ApplicationListener<RefreshRoutesEvent> {
public class CorsGatewayFilterApplicationListener implements ApplicationListener<RefreshRoutesResultEvent> {
private final GlobalCorsProperties globalCorsProperties;
@@ -61,7 +61,7 @@ public class CorsGatewayFilterApplicationListener implements ApplicationListener
}
@Override
public void onApplicationEvent(RefreshRoutesEvent event) {
public void onApplicationEvent(RefreshRoutesResultEvent event) {
routeLocator.getRoutes().collectList().subscribe(routes -> {
// pre-populate with pre-existing global cors configurations to combine with.
var corsConfigurations = new HashMap<>(globalCorsProperties.getCorsConfigurations());

View File

@@ -16,12 +16,17 @@
package org.springframework.cloud.gateway.filter.factory;
import java.util.Arrays;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
@@ -31,6 +36,23 @@ import static org.springframework.cloud.gateway.support.GatewayToStringStyler.fi
*/
public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
private static final String OVERRIDE_KEY = "override";
@Override
public Class getConfigClass() {
return Config.class;
}
@Override
public NameValueConfig newConfig() {
return new Config();
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(GatewayFilter.NAME_KEY, GatewayFilter.VALUE_KEY, OVERRIDE_KEY);
}
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@@ -41,6 +63,13 @@ public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGate
@Override
public String toString() {
if (config instanceof Config) {
return filterToStringCreator(AddResponseHeaderGatewayFilterFactory.this)
.append(GatewayFilter.NAME_KEY, config.getName())
.append(GatewayFilter.VALUE_KEY, config.getValue())
.append(OVERRIDE_KEY, ((Config) config).isOverride())
.toString();
}
return filterToStringCreator(AddResponseHeaderGatewayFilterFactory.this)
.append(config.getName(), config.getValue())
.toString();
@@ -49,12 +78,51 @@ public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGate
}
void addHeader(ServerWebExchange exchange, NameValueConfig config) {
final String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
HttpHeaders headers = exchange.getResponse().getHeaders();
// if response has been commited, no more response headers will bee added.
if (!exchange.getResponse().isCommitted()) {
headers.add(config.getName(), value);
final String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
HttpHeaders headers = exchange.getResponse().getHeaders();
boolean override = true; // default is true
if (config instanceof Config) {
override = ((Config) config).isOverride();
}
if (override) {
headers.add(config.getName(), value);
}
else {
boolean headerIsMissingOrBlank = headers.getOrEmpty(config.getName())
.stream()
.allMatch(h -> !StringUtils.hasText(h));
if (headerIsMissingOrBlank) {
headers.add(config.getName(), value);
}
}
}
}
public static class Config extends AbstractNameValueGatewayFilterFactory.NameValueConfig {
private boolean override = true;
public boolean isOverride() {
return override;
}
public Config setOverride(boolean override) {
this.override = override;
return this;
}
@Override
public String toString() {
return new ToStringCreator(this).append(NAME_KEY, name)
.append(VALUE_KEY, value)
.append(OVERRIDE_KEY, override)
.toString();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -22,7 +22,6 @@ import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import reactor.core.publisher.Mono;
@@ -34,6 +33,7 @@ import org.springframework.http.MediaType;
/**
* @author Marta Medio
* @author raccoonback
*/
public class RemoveJsonAttributesResponseBodyGatewayFilterFactory extends
AbstractGatewayFilterFactory<RemoveJsonAttributesResponseBodyGatewayFilterFactory.FieldListConfiguration> {
@@ -72,14 +72,14 @@ public class RemoveJsonAttributesResponseBodyGatewayFilterFactory extends
RewriteFunction<String, String> rewriteFunction = (exchange, body) -> {
if (MediaType.APPLICATION_JSON.isCompatibleWith(exchange.getResponse().getHeaders().getContentType())) {
try {
JsonNode jsonBodyContent = mapper.readValue(body, JsonNode.class);
JsonNode jsonNode = mapper.readValue(body, JsonNode.class);
removeJsonAttribute(jsonBodyContent, config.getFieldList(), config.isDeleteRecursively());
removeJsonAttributes(jsonNode, config.getFieldList(), config.isDeleteRecursively());
body = mapper.writeValueAsString(jsonBodyContent);
body = mapper.writeValueAsString(jsonNode);
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
return Mono.error(new IllegalStateException("Failed to process JSON of response body.", e));
}
}
return Mono.just(body);
@@ -93,22 +93,12 @@ public class RemoveJsonAttributesResponseBodyGatewayFilterFactory extends
private ObjectMapper mapper = new ObjectMapper();
private void removeJsonAttribute(JsonNode jsonBodyContent, List<String> fieldsToRemove, boolean deleteRecursively) {
if (deleteRecursively) {
for (JsonNode jsonNode : jsonBodyContent) {
if (jsonNode instanceof ObjectNode) {
((ObjectNode) jsonNode).remove(fieldsToRemove);
removeJsonAttribute(jsonNode, fieldsToRemove, true);
}
if (jsonNode instanceof ArrayNode) {
for (JsonNode node : jsonNode) {
removeJsonAttribute(node, fieldsToRemove, true);
}
}
}
private void removeJsonAttributes(JsonNode jsonNode, List<String> fieldNames, boolean deleteRecursively) {
if (jsonNode instanceof ObjectNode objectNode) {
objectNode.remove(fieldNames);
}
if (jsonBodyContent instanceof ObjectNode) {
((ObjectNode) jsonBodyContent).remove(fieldsToRemove);
if (deleteRecursively) {
jsonNode.forEach(childNode -> removeJsonAttributes(childNode, fieldNames, true));
}
}

View File

@@ -24,12 +24,12 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
@@ -59,7 +59,8 @@ public class RemoveRequestParameterGatewayFilterFactory
queryParams.remove(config.getName());
try {
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
MultiValueMap<String, String> encodedQueryParams = ServerWebExchangeUtils
.encodeQueryParams(queryParams);
URI newUri = UriComponentsBuilder.fromUri(request.getURI())
.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams))
.build(true)

View File

@@ -47,8 +47,11 @@ public class RemoveResponseHeaderGatewayFilterFactory
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> exchange.getResponse().getHeaders().remove(config.getName())));
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (!exchange.getResponse().isCommitted()) {
exchange.getResponse().getHeaders().remove(config.getName());
}
}));
}
@Override

View File

@@ -34,7 +34,7 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.s
* User Request Rate Limiter filter. See https://stripe.com/blog/rate-limiters and
* https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d#file-1-check_request_rate_limiter-rb-L11-L34.
*/
@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
@ConfigurationProperties("spring.cloud.gateway.server.webflux.filter.request-rate-limiter")
public class RequestRateLimiterGatewayFilterFactory
extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config> {

Some files were not shown because too many files have changed in this diff Show More