Merge branch 'spring-cloud:main' into setrequesturi

This commit is contained in:
Stepan Mikhailiuk
2025-05-22 12:27:12 +08:00
committed by GitHub
173 changed files with 5848 additions and 986 deletions

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)`

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

@@ -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,6 +53,7 @@
|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 | |
@@ -80,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.
@@ -96,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.
@@ -103,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
@@ -114,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.
@@ -129,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.
@@ -165,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.
@@ -176,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

@@ -7,7 +7,7 @@ test:
spring:
cloud:
gateway:
gateway.server.webflux:
filter:
default-filters:
#- PrefixPath=/httpbin

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 -->

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,7 +76,8 @@ import org.springframework.web.client.RestClient;
* @author Jürgen Wißkirchen
*/
@AutoConfiguration(after = { HttpClientAutoConfiguration.class, RestTemplateAutoConfiguration.class,
RestClientAutoConfiguration.class })
RestClientAutoConfiguration.class, FilterAutoConfiguration.class, HandlerFunctionAutoConfiguration.class,
PredicateAutoConfiguration.class })
@ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true)
@Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class)
@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class)
@@ -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

@@ -195,7 +195,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();
};
}

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

@@ -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

@@ -245,4 +245,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

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

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

@@ -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;
@@ -193,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,
@@ -271,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);
@@ -289,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) {
@@ -317,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());
@@ -334,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();
}
@@ -467,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);
}
@@ -479,8 +483,8 @@ public class GatewayAutoConfiguration {
@Bean
@ConditionalOnEnabledPredicate
public PathRoutePredicateFactory pathRoutePredicateFactory() {
return new PathRoutePredicateFactory();
public PathRoutePredicateFactory pathRoutePredicateFactory(WebFluxProperties webFluxProperties) {
return new PathRoutePredicateFactory(webFluxProperties);
}
@Bean
@@ -774,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) {
@@ -858,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,
@@ -888,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 {
}
@@ -896,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

@@ -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

@@ -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> {

View File

@@ -27,7 +27,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Spencer Gibb, Thirunavukkarasu Ravichandran, Jörg Richter
*/
@ConfigurationProperties("spring.cloud.gateway.filter.secure-headers")
@ConfigurationProperties("spring.cloud.gateway.server.webflux.filter.secure-headers")
public class SecureHeadersProperties {
/**

View File

@@ -35,7 +35,7 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.s
/**
* @author Spencer Gibb
*/
@ConfigurationProperties("spring.cloud.gateway.set-status")
@ConfigurationProperties("spring.cloud.gateway.server.webflux.set-status")
public class SetStatusGatewayFilterFactory extends AbstractGatewayFilterFactory<SetStatusGatewayFilterFactory.Config> {
/**

View File

@@ -40,7 +40,8 @@ import org.springframework.validation.annotation.Validated;
* @author Marta Medio
* @author Ignacio Lozano
*/
@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true")
@ConditionalOnProperty(value = "spring.cloud.gateway.server.webflux.filter.local-response-cache.enabled",
havingValue = "true")
public class LocalResponseCacheGatewayFilterFactory
extends AbstractGatewayFilterFactory<LocalResponseCacheGatewayFilterFactory.RouteCacheConfiguration> {

View File

@@ -30,7 +30,7 @@ import org.springframework.util.unit.DataSize;
@ConfigurationProperties(prefix = LocalResponseCacheProperties.PREFIX)
public class LocalResponseCacheProperties {
static final String PREFIX = "spring.cloud.gateway.filter.local-response-cache";
static final String PREFIX = "spring.cloud.gateway.server.webflux.filter.local-response-cache";
private static final Log LOGGER = LogFactory.getLog(LocalResponseCacheProperties.class);

View File

@@ -30,7 +30,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
@ConfigurationProperties("spring.cloud.gateway.filter.remove-hop-by-hop")
@ConfigurationProperties("spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop")
public class RemoveHopByHopHeadersFilter implements HttpHeadersFilter, Ordered {
/**

View File

@@ -32,7 +32,7 @@ import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
@ConfigurationProperties("spring.cloud.gateway.x-forwarded")
@ConfigurationProperties("spring.cloud.gateway.server.webflux.x-forwarded")
public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
/** Default http port. */

View File

@@ -31,6 +31,7 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.BeansException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.context.ApplicationContext;
@@ -50,7 +51,7 @@ import org.springframework.validation.annotation.Validated;
* @author Denis Cutic
* @author Andrey Muchnik
*/
@ConfigurationProperties("spring.cloud.gateway.redis-rate-limiter")
@ConfigurationProperties(GatewayProperties.PREFIX + ".redis-rate-limiter")
public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config> implements ApplicationContextAware {
/**

View File

@@ -77,8 +77,7 @@ public interface AsyncPredicate<T> extends Function<T, Publisher<Boolean>>, HasC
@Override
public void accept(Visitor visitor) {
if (delegate instanceof GatewayPredicate) {
GatewayPredicate gatewayPredicate = (GatewayPredicate) delegate;
if (delegate instanceof GatewayPredicate gatewayPredicate) {
gatewayPredicate.accept(visitor);
}
}

View File

@@ -58,12 +58,7 @@ public class CookieRoutePredicateFactory extends AbstractRoutePredicateFactory<C
if (cookies == null) {
return false;
}
for (HttpCookie cookie : cookies) {
if (cookie.getValue().matches(config.regexp)) {
return true;
}
}
return false;
return cookies.stream().anyMatch(cookie -> cookie.getValue().matches(config.regexp));
}
@Override

View File

@@ -47,8 +47,8 @@ public interface GatewayPredicate extends Predicate<ServerWebExchange>, HasConfi
static GatewayPredicate wrapIfNeeded(Predicate<? super ServerWebExchange> other) {
GatewayPredicate right;
if (other instanceof GatewayPredicate) {
right = (GatewayPredicate) other;
if (other instanceof GatewayPredicate gatewayPredicate) {
right = gatewayPredicate;
}
else {
right = new GatewayPredicateWrapper(other);
@@ -72,8 +72,7 @@ public interface GatewayPredicate extends Predicate<ServerWebExchange>, HasConfi
@Override
public void accept(Visitor visitor) {
if (delegate instanceof GatewayPredicate) {
GatewayPredicate gatewayPredicate = (GatewayPredicate) delegate;
if (delegate instanceof GatewayPredicate gatewayPredicate) {
gatewayPredicate.accept(visitor);
}
}

View File

@@ -24,8 +24,10 @@ import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.server.PathContainer;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPattern.PathMatchInfo;
@@ -41,6 +43,7 @@ import static org.springframework.http.server.PathContainer.parsePath;
/**
* @author Spencer Gibb
* @author Dhawal Kapil
* @author FuYiNan Guo
*/
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
@@ -50,8 +53,20 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Pat
private PathPatternParser pathPatternParser = new PathPatternParser();
private final WebFluxProperties webFluxProperties;
/**
* @deprecated {@link #PathRoutePredicateFactory(WebFluxProperties)}
*/
@Deprecated
public PathRoutePredicateFactory() {
super(Config.class);
this.webFluxProperties = new WebFluxProperties();
}
public PathRoutePredicateFactory(WebFluxProperties webFluxProperties) {
super(Config.class);
this.webFluxProperties = webFluxProperties;
}
private static void traceMatch(String prefix, Object desired, Object actual, boolean match) {
@@ -82,7 +97,16 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Pat
synchronized (this.pathPatternParser) {
pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchTrailingSlash());
config.getPatterns().forEach(pattern -> {
PathPattern pathPattern = this.pathPatternParser.parse(pattern);
String basePath = webFluxProperties.getBasePath();
boolean basePathIsNotBlank = StringUtils.hasText(basePath);
String pathPatternStr = pattern;
if (basePathIsNotBlank) {
if (pattern.length() > 1 && !pattern.startsWith("/")) {
basePath += ("/");
}
pathPatternStr = basePath + pattern;
}
PathPattern pathPattern = this.pathPatternParser.parse(pathPatternStr);
pathPatterns.add(pathPattern);
});
}

View File

@@ -65,7 +65,7 @@ public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {
}
protected Mono<String> randomId() {
return Mono.fromSupplier(idGenerator::generateId).map(UUID::toString).publishOn(Schedulers.boundedElastic());
return Mono.fromSupplier(idGenerator::generateId).publishOn(Schedulers.boundedElastic()).map(UUID::toString);
}
}

View File

@@ -201,14 +201,10 @@ public class RouteDefinitionRouteLocator implements RouteLocator {
// this is a very rare case, but possible, just match all
return AsyncPredicate.from(exchange -> true);
}
AsyncPredicate<ServerWebExchange> predicate = lookup(routeDefinition, predicates.get(0));
for (PredicateDefinition andPredicate : predicates.subList(1, predicates.size())) {
AsyncPredicate<ServerWebExchange> found = lookup(routeDefinition, andPredicate);
predicate = predicate.and(found);
}
return predicate;
return predicates.stream()
.map(nextPredicate -> lookup(routeDefinition, nextPredicate))
.reduce(AsyncPredicate.from(exchange -> true), AsyncPredicate::and);
}
@SuppressWarnings("unchecked")

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