Merge branch '4.1.x' into fix-rewritepath-double-encoding
Signed-off-by: Jens Mallien <108389225+jensmatw@users.noreply.github.com>
This commit is contained in:
2
.github/dco.yml
vendored
Normal file
2
.github/dco.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
require:
|
||||
members: false
|
||||
4
.github/workflows/maven.yml
vendored
4
.github/workflows/maven.yml
vendored
@@ -5,9 +5,9 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, 3.1.x ]
|
||||
branches: [ main, 4.1.x, 3.1.x ]
|
||||
pull_request:
|
||||
branches: [ main, 3.1.x ]
|
||||
branches: [ main, 4.1.x, 3.1.x ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
259
README.adoc
259
README.adoc
@@ -27,246 +27,7 @@ image::https://codecov.io/gh/spring-cloud/spring-cloud-gateway/branch/main/graph
|
||||
[[building]]
|
||||
= Building
|
||||
|
||||
:spring-cloud-build-branch: main
|
||||
|
||||
Spring Cloud is released under the non-restrictive Apache 2.0 license,
|
||||
and follows a very standard Github development process, using Github
|
||||
tracker for issues and merging pull requests into main. If you want
|
||||
to contribute even something trivial please do not hesitate, but
|
||||
follow the guidelines below.
|
||||
|
||||
[[sign-the-contributor-license-agreement]]
|
||||
== Sign the Contributor License Agreement
|
||||
|
||||
Before we accept a non-trivial patch or pull request we will need you to sign the
|
||||
https://cla.pivotal.io/sign/spring[Contributor License Agreement].
|
||||
Signing the contributor's agreement does not grant anyone commit rights to the main
|
||||
repository, but it does mean that we can accept your contributions, and you will get an
|
||||
author credit if we do. Active contributors might be asked to join the core team, and
|
||||
given the ability to merge pull requests.
|
||||
|
||||
[[code-of-conduct]]
|
||||
== Code of Conduct
|
||||
This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/src/main/asciidoc/code-of-conduct.adoc[code of
|
||||
conduct]. By participating, you are expected to uphold this code. Please report
|
||||
unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
|
||||
[[code-conventions-and-housekeeping]]
|
||||
== Code Conventions and Housekeeping
|
||||
None of these is essential for a pull request, but they will all help. They can also be
|
||||
added after the original pull request but before a merge.
|
||||
|
||||
* Use the Spring Framework code format conventions. If you use Eclipse
|
||||
you can import formatter settings using the
|
||||
`eclipse-code-formatter.xml` file from the
|
||||
https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring
|
||||
Cloud Build] project. If using IntelliJ, you can use the
|
||||
https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter
|
||||
Plugin] to import the same file.
|
||||
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an
|
||||
`@author` tag identifying you, and preferably at least a paragraph on what the class is
|
||||
for.
|
||||
* Add the ASF license header comment to all new `.java` files (copy from existing files
|
||||
in the project)
|
||||
* Add yourself as an `@author` to the .java files that you modify substantially (more
|
||||
than cosmetic changes).
|
||||
* Add some Javadocs and, if you change the namespace, some XSD doc elements.
|
||||
* A few unit tests would help a lot as well -- someone has to do it.
|
||||
* If no-one else is using your branch, please rebase it against the current main (or
|
||||
other target branch in the main project).
|
||||
* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions],
|
||||
if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit
|
||||
message (where XXXX is the issue number).
|
||||
|
||||
[[checkstyle]]
|
||||
== Checkstyle
|
||||
|
||||
Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are:
|
||||
|
||||
.spring-cloud-build-tools/
|
||||
----
|
||||
└── src
|
||||
├── checkstyle
|
||||
│ └── checkstyle-suppressions.xml <3>
|
||||
└── main
|
||||
└── resources
|
||||
├── checkstyle-header.txt <2>
|
||||
└── checkstyle.xml <1>
|
||||
----
|
||||
<1> Default Checkstyle rules
|
||||
<2> File header setup
|
||||
<3> Default suppression rules
|
||||
|
||||
[[checkstyle-configuration]]
|
||||
=== Checkstyle configuration
|
||||
|
||||
Checkstyle rules are *disabled by default*. To add checkstyle to your project just define the following properties and plugins.
|
||||
|
||||
.pom.xml
|
||||
----
|
||||
<properties>
|
||||
<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError> <1>
|
||||
<maven-checkstyle-plugin.failsOnViolation>true
|
||||
</maven-checkstyle-plugin.failsOnViolation> <2>
|
||||
<maven-checkstyle-plugin.includeTestSourceDirectory>true
|
||||
</maven-checkstyle-plugin.includeTestSourceDirectory> <3>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin> <4>
|
||||
<groupId>io.spring.javaformat</groupId>
|
||||
<artifactId>spring-javaformat-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin> <5>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin> <5>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
</build>
|
||||
----
|
||||
<1> Fails the build upon Checkstyle errors
|
||||
<2> Fails the build upon Checkstyle violations
|
||||
<3> Checkstyle analyzes also the test sources
|
||||
<4> Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules
|
||||
<5> Add checkstyle plugin to your build and reporting phases
|
||||
|
||||
If you need to suppress some rules (e.g. line length needs to be longer), then it's enough for you to define a file under `${project.root}/src/checkstyle/checkstyle-suppressions.xml` with your suppressions. Example:
|
||||
|
||||
.projectRoot/src/checkstyle/checkstyle-suppresions.xml
|
||||
----
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE suppressions PUBLIC
|
||||
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
|
||||
"https://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
|
||||
<suppressions>
|
||||
<suppress files=".*ConfigServerApplication\.java" checks="HideUtilityClassConstructor"/>
|
||||
<suppress files=".*ConfigClientWatch\.java" checks="LineLengthCheck"/>
|
||||
</suppressions>
|
||||
----
|
||||
|
||||
It's advisable to copy the `${spring-cloud-build.rootFolder}/.editorconfig` and `${spring-cloud-build.rootFolder}/.springformat` to your project. That way, some default formatting rules will be applied. You can do so by running this script:
|
||||
|
||||
```bash
|
||||
$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/.editorconfig -o .editorconfig
|
||||
$ touch .springformat
|
||||
```
|
||||
|
||||
[[ide-setup]]
|
||||
== IDE setup
|
||||
|
||||
[[intellij-idea]]
|
||||
=== Intellij IDEA
|
||||
|
||||
In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin.
|
||||
The following files can be found in the https://github.com/spring-cloud/spring-cloud-build/tree/main/spring-cloud-build-tools[Spring Cloud Build] project.
|
||||
|
||||
.spring-cloud-build-tools/
|
||||
----
|
||||
└── src
|
||||
├── checkstyle
|
||||
│ └── checkstyle-suppressions.xml <3>
|
||||
└── main
|
||||
└── resources
|
||||
├── checkstyle-header.txt <2>
|
||||
├── checkstyle.xml <1>
|
||||
└── intellij
|
||||
├── Intellij_Project_Defaults.xml <4>
|
||||
└── Intellij_Spring_Boot_Java_Conventions.xml <5>
|
||||
----
|
||||
<1> Default Checkstyle rules
|
||||
<2> File header setup
|
||||
<3> Default suppression rules
|
||||
<4> Project defaults for Intellij that apply most of Checkstyle rules
|
||||
<5> Project style conventions for Intellij that apply most of Checkstyle rules
|
||||
|
||||
.Code style
|
||||
|
||||
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/assets/images/intellij-code-style.png[Code style]
|
||||
|
||||
Go to `File` -> `Settings` -> `Editor` -> `Code style`. There click on the icon next to the `Scheme` section. There, click on the `Import Scheme` value and pick the `Intellij IDEA code style XML` option. Import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml` file.
|
||||
|
||||
.Inspection profiles
|
||||
|
||||
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/assets/images/intellij-inspections.png[Code style]
|
||||
|
||||
Go to `File` -> `Settings` -> `Editor` -> `Inspections`. There click on the icon next to the `Profile` section. There, click on the `Import Profile` and import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml` file.
|
||||
|
||||
.Checkstyle
|
||||
|
||||
To have Intellij work with Checkstyle, you have to install the `Checkstyle` plugin. It's advisable to also install the `Assertions2Assertj` to automatically convert the JUnit assertions
|
||||
|
||||
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/assets/images/intellij-checkstyle.png[Checkstyle]
|
||||
|
||||
Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on the `+` icon in the `Configuration file` section. There, you'll have to define where the checkstyle rules should be picked from. In the image above, we've picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build's GitHub repository (e.g. for the `checkstyle.xml` : `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/spring-cloud-build-tools/src/main/resources/checkstyle.xml`). We need to provide the following variables:
|
||||
|
||||
- `checkstyle.header.file` - please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` URL.
|
||||
- `checkstyle.suppressions.file` - default suppressions. Please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` URL.
|
||||
- `checkstyle.additional.suppressions.file` - this variable corresponds to suppressions in your local project. E.g. you're working on `spring-cloud-contract`. Then point to the `project-root/src/checkstyle/checkstyle-suppressions.xml` folder. Example for `spring-cloud-contract` would be: `/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`.
|
||||
|
||||
IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.
|
||||
|
||||
[[duplicate-finder]]
|
||||
== Duplicate Finder
|
||||
|
||||
Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath.
|
||||
|
||||
[[duplicate-finder-configuration]]
|
||||
=== Duplicate Finder configuration
|
||||
|
||||
Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`.
|
||||
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
----
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.basepom.maven</groupId>
|
||||
<artifactId>duplicate-finder-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
----
|
||||
|
||||
For other properties, we have set defaults as listed in the https://github.com/basepom/duplicate-finder-maven-plugin/wiki[plugin documentation].
|
||||
|
||||
You can easily override them but setting the value of the selected property prefixed with `duplicate-finder-maven-plugin`. For example, set `duplicate-finder-maven-plugin.skip` to `true` in order to skip duplicates check in your build.
|
||||
|
||||
If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your setup, make sure to add them in the plugin configuration section of your project:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.basepom.maven</groupId>
|
||||
<artifactId>duplicate-finder-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<ignoredClassPatterns>
|
||||
<ignoredClassPattern>org.joda.time.base.BaseDateTime</ignoredClassPattern>
|
||||
<ignoredClassPattern>.*module-info</ignoredClassPattern>
|
||||
</ignoredClassPatterns>
|
||||
<ignoredResourcePatterns>
|
||||
<ignoredResourcePattern>changelog.txt</ignoredResourcePattern>
|
||||
</ignoredResourcePatterns>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
----
|
||||
|
||||
Unresolved directive in <stdin> - include::https:///raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/partials/building.adoc[]
|
||||
|
||||
[[contributing]]
|
||||
= Contributing
|
||||
@@ -279,21 +40,17 @@ tracker for issues and merging pull requests into main. If you want
|
||||
to contribute even something trivial please do not hesitate, but
|
||||
follow the guidelines below.
|
||||
|
||||
[[sign-the-contributor-license-agreement]]
|
||||
== Sign the Contributor License Agreement
|
||||
[[developer-certificate-of-origin]]
|
||||
== Developer Certificate of Origin (DCO)
|
||||
|
||||
Before we accept a non-trivial patch or pull request we will need you to sign the
|
||||
https://cla.pivotal.io/sign/spring[Contributor License Agreement].
|
||||
Signing the contributor's agreement does not grant anyone commit rights to the main
|
||||
repository, but it does mean that we can accept your contributions, and you will get an
|
||||
author credit if we do. Active contributors might be asked to join the core team, and
|
||||
given the ability to merge pull requests.
|
||||
All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
|
||||
For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring].
|
||||
|
||||
[[code-of-conduct]]
|
||||
== Code of Conduct
|
||||
This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/src/main/asciidoc/code-of-conduct.adoc[code of
|
||||
This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/modules/ROOT/partials/code-of-conduct.adoc[code of
|
||||
conduct]. By participating, you are expected to uphold this code. Please report
|
||||
unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
unacceptable behavior to code-of-conduct@spring.io.
|
||||
|
||||
[[code-conventions-and-housekeeping]]
|
||||
== Code Conventions and Housekeeping
|
||||
@@ -467,7 +224,7 @@ Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, th
|
||||
[[duplicate-finder-configuration]]
|
||||
=== Duplicate Finder configuration
|
||||
|
||||
Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`.
|
||||
Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the project's `pom.xml`.
|
||||
|
||||
.pom.xml
|
||||
[source,xml]
|
||||
|
||||
@@ -6,7 +6,7 @@ nav:
|
||||
ext:
|
||||
collector:
|
||||
run:
|
||||
command: ./mvnw --no-transfer-progress -B process-resources -Pdocs -pl docs -Dantora-maven-plugin.phase=none -Dgenerate-docs.phase=none -Dgenerate-readme.phase=none -Dgenerate-cloud-resources.phase=none -Dmaven-dependency-plugin-for-docs.phase=none -Dmaven-dependency-plugin-for-docs-classes.phase=none -DskipTests
|
||||
command: ./mvnw --no-transfer-progress -B process-resources -Pdocs -pl docs -Dantora-maven-plugin.phase=none -Dgenerate-docs.phase=none -Dgenerate-readme.phase=none -Dgenerate-cloud-resources.phase=none -Dmaven-dependency-plugin-for-docs.phase=none -Dmaven-dependency-plugin-for-docs-classes.phase=none -DskipTests -DdisableConfigurationProperties
|
||||
local: true
|
||||
scan:
|
||||
dir: ./target/classes/antora-resources/
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
** xref:spring-cloud-gateway/global-filters.adoc[]
|
||||
** xref:spring-cloud-gateway/httpheadersfilters.adoc[]
|
||||
** xref:spring-cloud-gateway/tls-and-ssl.adoc[]
|
||||
** xref:spring-cloud-gateway/http-client.adoc[]
|
||||
** xref:spring-cloud-gateway/configuration.adoc[]
|
||||
** xref:spring-cloud-gateway/route-metadata-configuration.adoc[]
|
||||
** xref:spring-cloud-gateway/http-timeouts-configuration.adoc[]
|
||||
|
||||
@@ -43,6 +43,8 @@ spring:
|
||||
- Path=/api/**
|
||||
----
|
||||
|
||||
WARNING: If using the `lb()` filter, it needs to be after any filter that manipulates the path such as `setPath()` or `stripPrefix()`, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.
|
||||
|
||||
NOTE: By default, when a service instance cannot be found by the `ReactorLoadBalancer`, a `503` is returned.
|
||||
// TODO: implement use404
|
||||
// You can configure the gateway to return a `404` by setting `spring.cloud.gateway.loadbalancer.use404=true`.
|
||||
|
||||
@@ -33,7 +33,7 @@ class RouteConfiguration {
|
||||
public RouterFunction<ServerResponse> gatewayRouterFunctionsPrefixPath() {
|
||||
return route("prefixpath_route")
|
||||
.GET("/**", http("https://example.org"))
|
||||
.before("/mypath")
|
||||
.before(prefixPath("/mypath"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -42,3 +42,4 @@ class RouteConfiguration {
|
||||
This prefixes `/mypath` to the path of all matching requests.
|
||||
So a request to `/hello` is sent to `/mypath/hello`.
|
||||
|
||||
WARNING: If using the `lb()` filter, it needs to be after the `prefixPath()` filter, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.
|
||||
|
||||
@@ -9,6 +9,7 @@ The `Retry` filter supports the following parameters:
|
||||
* `methods`: The HTTP methods that should be retried, represented by using `org.springframework.http.HttpMethod`.
|
||||
* `series`: The series of status codes to be retried, represented by using `org.springframework.http.HttpStatus.Series`.
|
||||
* `exceptions`: A list of thrown exceptions that should be retried.
|
||||
* `cacheBody`: A flag to signal if the request body should be cached. If set to `true`, the `adaptCacheBody` filter must be used to send the cached body downstream.
|
||||
//* `backoff`: The configured exponential backoff for the retries.
|
||||
//Retries are performed after a backoff interval of `firstBackoff * (factor ^ n)`, where `n` is the iteration.
|
||||
//If `maxBackoff` is configured, the maximum backoff applied is limited to `maxBackoff`.
|
||||
@@ -20,8 +21,11 @@ The following defaults are configured for `Retry` filter, if enabled:
|
||||
* `series`: 5XX series
|
||||
* `methods`: GET method
|
||||
* `exceptions`: `IOException`, `TimeoutException` and `RetryException`
|
||||
* `cacheBody`: `false`
|
||||
//* `backoff`: disabled
|
||||
|
||||
WARNING: Setting `cacheBody` to `true` causes the gateway to read the whole body into memory. This should be used with caution.
|
||||
|
||||
The following listing configures a Retry filter:
|
||||
|
||||
.application.yml
|
||||
@@ -42,11 +46,14 @@ spring:
|
||||
retries: 3
|
||||
series: SERVER_ERROR
|
||||
methods: GET,POST
|
||||
cacheBody: true
|
||||
- name: AdaptCachedBody
|
||||
----
|
||||
|
||||
.GatewaySampleApplication.java
|
||||
[source,java]
|
||||
----
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.adaptCachedBody;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
|
||||
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
|
||||
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
|
||||
@@ -59,7 +66,8 @@ class RouteConfiguration {
|
||||
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
|
||||
return route("add_request_parameter_route")
|
||||
.route(host("*.retry.com"), http("https://example.org"))
|
||||
.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST))))
|
||||
.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST)).setCacheBody(true)))
|
||||
.filter(adaptCachedBody())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,3 +43,4 @@ class RouteConfiguration {
|
||||
|
||||
For a request path of `/red/blue`, this sets the path to `/blue` before making the downstream request. Note that in `application.yml` the `$` should be replaced with `$\` because of the YAML specification.
|
||||
|
||||
WARNING: If using the `lb()` filter, it needs to be after the `rewritePath()` filter, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.
|
||||
@@ -45,3 +45,4 @@ class RouteConfiguration {
|
||||
|
||||
For a request path of `/red/blue`, this sets the path to `/blue` before making the downstream request.
|
||||
|
||||
WARNING: If using the `lb()` filter, it needs to be after the `setPath()` filter, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.
|
||||
@@ -43,3 +43,4 @@ class RouteConfiguration {
|
||||
|
||||
When a request is made through the gateway to `/name/blue/red`, the request made to `nameservice` looks like `https://nameservice/red`.
|
||||
|
||||
WARNING: If using the `lb()` filter, it needs to be after the `stripPrefix()` filter, otherwise the resulting url could be incorrect. The `lb:` scheme handler in configuration, automatically puts the filter in the highest precedence order.
|
||||
@@ -94,7 +94,7 @@ import org.springframework.web.servlet.function.ServerResponse;
|
||||
class SampleHandlerFilterFunctions {
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> instrument(String requestHeader, String responseHeader) {
|
||||
return (request, next) -> {
|
||||
ServerRequest modified = ServerRequest.from(request).header(requestHeader, generateId());
|
||||
ServerRequest modified = ServerRequest.from(request).header(requestHeader, generateId()).build();
|
||||
ServerResponse response = next.handle(modified);
|
||||
response.headers().add(responseHeader, generateId());
|
||||
return response;
|
||||
@@ -147,7 +147,7 @@ import org.springframework.web.servlet.function.ServerRequest;
|
||||
|
||||
class SampleBeforeFilterFunctions {
|
||||
public static Function<ServerRequest, ServerRequest> instrument(String header) {
|
||||
return request -> ServerRequest.from(request).header(header, generateId());;
|
||||
return request -> ServerRequest.from(request).header(header, generateId()).build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
= Configuration properties
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
To see the list of all Spring Cloud Gateway related configuration properties, see link:appendix.html[the appendix].
|
||||
To see the list of all Spring Cloud Gateway related configuration properties, see link:../appendix.html[the appendix].
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
[[http-client]]
|
||||
= The HttpClientCustomizer
|
||||
|
||||
The HttpClientCustomizer interface in spring-cloud-gateway allows for the customization of the HTTP client used by the gateway. It provides a single method, customize, which takes an HttpClient as an argument and returns a customized version of it.
|
||||
|
||||
This interface is useful for scenarios where you need to configure specific settings or behaviors for the HTTP client, such as setting timeouts, adding custom headers, or enabling specific features. By implementing this interface, you can provide a custom implementation that meets your specific requirements.
|
||||
|
||||
Here's an example of how you might use the HttpClientCustomizer interface:
|
||||
|
||||
.MyHttpClientCustomizer.java
|
||||
[source,java]
|
||||
----
|
||||
import org.springframework.cloud.gateway.config.HttpClientCustomizer;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
public class MyHttpClientCustomizer implements HttpClientCustomizer {
|
||||
|
||||
@Override
|
||||
public HttpClient customize(HttpClient httpClient) {
|
||||
// Customize the HTTP client here
|
||||
return httpClient.tcpConfiguration(tcpClient -> {
|
||||
// Set the connect timeout to 5 seconds
|
||||
tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
|
||||
// Set the read timeout to 10 seconds
|
||||
tcpClient.option(ChannelOption.SO_TIMEOUT, 10000);
|
||||
return tcpClient;
|
||||
});
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In this example, the MyHttpClientCustomizer class implements the HttpClientCustomizer interface and overrides the customize method. Within this method, the HTTP client is customized by setting the connect timeout to 5 seconds and the read timeout to 10 seconds.
|
||||
|
||||
To use this customizer, you would need to register it with the Spring Cloud Gateway configuration:
|
||||
|
||||
.GatewayConfiguration.java
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class GatewayConfiguration {
|
||||
|
||||
@Bean
|
||||
public HttpClientCustomizer myHttpClientCustomizer() {
|
||||
return new MyHttpClientCustomizer();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
By registering the customizer as a bean, it will be automatically applied to the HTTP client used by the gateway.
|
||||
@@ -68,4 +68,3 @@ spring:
|
||||
close-notify-flush-timeout-millis: 3000
|
||||
close-notify-read-timeout-millis: 0
|
||||
----
|
||||
|
||||
|
||||
@@ -89,12 +89,12 @@
|
||||
|spring.cloud.gateway.httpclient.pool.max-life-time | | Duration after which the channel will be closed. If NULL, there is no max life time.
|
||||
|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 | | Type of pool for HttpClient to use, defaults to ELASTIC.
|
||||
|spring.cloud.gateway.httpclient.pool.type | | Type of pool for HttpClient to use (elastic, fixed or disabled).
|
||||
|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.
|
||||
|spring.cloud.gateway.httpclient.proxy.port | | Port for proxy configuration of Netty HttpClient.
|
||||
|spring.cloud.gateway.httpclient.proxy.type | | proxyType for proxy configuration of Netty HttpClient.
|
||||
|spring.cloud.gateway.httpclient.proxy.type | | 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.close-notify-flush-timeout | `+++3000ms+++` | SSL close_notify flush timeout. Default to 3000 ms.
|
||||
@@ -126,6 +126,8 @@
|
||||
|spring.cloud.gateway.mvc.remove-hop-by-hop-response-headers-filter.enabled | `+++true+++` | Enables the forwarded-request-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.
|
||||
|spring.cloud.gateway.mvc.streaming-media-types | | Mime-types that are streaming.
|
||||
|spring.cloud.gateway.mvc.transfer-encoding-normalization-request-headers-filter.enabled | `+++true+++` | Enables the transfer-encoding-normalization-request-headers-filter.
|
||||
|spring.cloud.gateway.mvc.weight-calculator-filter.enabled | `+++true+++` | Enables the weight-calculator-filter.
|
||||
|spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.4",
|
||||
"antora": "3.2.0-alpha.8",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"@antora/collector-extension": "1.0.1",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.11.1",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
"@springio/antora-extensions": "1.14.4",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.16"
|
||||
}
|
||||
}
|
||||
|
||||
30
docs/pom.xml
30
docs/pom.xml
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-gateway-docs</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
@@ -25,20 +25,28 @@
|
||||
<micrometer-docs-generator.inclusionPattern>.*</micrometer-docs-generator.inclusionPattern>
|
||||
<micrometer-docs-generator.outputPath>${maven.multiModuleProjectDirectory}/docs/modules/ROOT/partials/</micrometer-docs-generator.outputPath>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<sourceDirectory>src/main/asciidoc</sourceDirectory>
|
||||
</build>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>enable-configuration-properties</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>!disableConfigurationProperties</name>
|
||||
</property>
|
||||
</activation>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>docs</id>
|
||||
<build>
|
||||
|
||||
@@ -20,7 +20,7 @@ image::https://codecov.io/gh/spring-cloud/spring-cloud-gateway/branch/main/graph
|
||||
[[building]]
|
||||
= Building
|
||||
|
||||
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/partials/contributing.adoc[]
|
||||
include::https:///raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/partials/building.adoc[]
|
||||
|
||||
[[contributing]]
|
||||
= Contributing
|
||||
|
||||
8
pom.xml
8
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Spring Cloud Gateway</name>
|
||||
@@ -15,7 +15,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-build</artifactId>
|
||||
<version>4.1.4-SNAPSHOT</version>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<scm>
|
||||
@@ -55,8 +55,8 @@
|
||||
<blockhound.version>1.0.8.RELEASE</blockhound.version>
|
||||
<java.version>17</java.version>
|
||||
<junit-pioneer.version>1.9.1</junit-pioneer.version>
|
||||
<spring-cloud-circuitbreaker.version>3.1.3-SNAPSHOT</spring-cloud-circuitbreaker.version>
|
||||
<spring-cloud-commons.version>4.1.5-SNAPSHOT</spring-cloud-commons.version>
|
||||
<spring-cloud-circuitbreaker.version>3.1.5-SNAPSHOT</spring-cloud-circuitbreaker.version>
|
||||
<spring-cloud-commons.version>4.1.6-SNAPSHOT</spring-cloud-commons.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<parent>
|
||||
<artifactId>spring-cloud-dependencies-parent</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<version>4.1.4-SNAPSHOT</version>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-cloud-gateway-dependencies</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>spring-cloud-gateway-dependencies</name>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.Vector;
|
||||
import java.util.function.Function;
|
||||
@@ -234,7 +235,7 @@ public class ProxyExchange<T> {
|
||||
|
||||
this.excluded.clear();
|
||||
for (String name : names) {
|
||||
this.excluded.add(name.toLowerCase());
|
||||
this.excluded.add(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -384,7 +385,7 @@ public class ProxyExchange<T> {
|
||||
private Set<String> filterHeaderKeys(Collection<String> headerNames) {
|
||||
final Set<String> excludedHeaders = this.excluded != null ? this.excluded : Collections.emptySet();
|
||||
return headerNames.stream()
|
||||
.filter(header -> !excludedHeaders.contains(header.toLowerCase()))
|
||||
.filter(header -> !excludedHeaders.contains(header.toLowerCase(Locale.ROOT)))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -101,7 +102,7 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String header = headerNames.nextElement();
|
||||
if (this.autoForwardedHeaders.contains(header.toLowerCase())) {
|
||||
if (this.autoForwardedHeaders.contains(header.toLowerCase(Locale.ROOT))) {
|
||||
headers.addAll(header, Collections.list(nativeRequest.getHeaders(header)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.gateway.sample;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -77,7 +78,7 @@ public class GatewaySampleApplication {
|
||||
.addResponseHeader("X-TestHeader", "rewrite_request")
|
||||
.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
|
||||
(exchange, s) -> {
|
||||
return Mono.just(new Hello(s.toUpperCase()));
|
||||
return Mono.just(new Hello(s.toUpperCase(Locale.ROOT)));
|
||||
})
|
||||
).uri(uri)
|
||||
)
|
||||
@@ -86,7 +87,7 @@ public class GatewaySampleApplication {
|
||||
.addResponseHeader("X-TestHeader", "rewrite_request_upper")
|
||||
.modifyRequestBody(String.class, String.class,
|
||||
(exchange, s) -> {
|
||||
return Mono.just(s.toUpperCase() + s.toUpperCase());
|
||||
return Mono.just(s.toUpperCase(Locale.ROOT) + s.toUpperCase(Locale.ROOT));
|
||||
})
|
||||
).uri(uri)
|
||||
)
|
||||
@@ -95,7 +96,7 @@ public class GatewaySampleApplication {
|
||||
.addResponseHeader("X-TestHeader", "rewrite_response_upper")
|
||||
.modifyResponseBody(String.class, String.class,
|
||||
(exchange, s) -> {
|
||||
return Mono.just(s.toUpperCase());
|
||||
return Mono.just(s.toUpperCase(Locale.ROOT));
|
||||
})
|
||||
).uri(uri)
|
||||
)
|
||||
@@ -107,7 +108,7 @@ public class GatewaySampleApplication {
|
||||
if (s == null) {
|
||||
return Mono.just("emptybody");
|
||||
}
|
||||
return Mono.just(s.toUpperCase());
|
||||
return Mono.just(s.toUpperCase(Locale.ROOT));
|
||||
})
|
||||
|
||||
).uri(uri)
|
||||
@@ -120,7 +121,7 @@ public class GatewaySampleApplication {
|
||||
if (s == null) {
|
||||
return Mono.error(new IllegalArgumentException("this should not happen"));
|
||||
}
|
||||
return Mono.just(s.toUpperCase());
|
||||
return Mono.just(s.toUpperCase(Locale.ROOT));
|
||||
})
|
||||
).uri(uri)
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<artifactId>spring-cloud-gateway-server-mvc</artifactId>
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.Res
|
||||
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;
|
||||
@@ -87,8 +88,9 @@ public class GatewayServerMvcAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ProxyExchange.class)
|
||||
public RestClientProxyExchange restClientProxyExchange(RestClient.Builder restClientBuilder) {
|
||||
return new RestClientProxyExchange(restClientBuilder.build());
|
||||
public RestClientProxyExchange restClientProxyExchange(RestClient.Builder restClientBuilder,
|
||||
GatewayMvcProperties properties) {
|
||||
return new RestClientProxyExchange(restClientBuilder.build(), properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -163,6 +165,14 @@ public class GatewayServerMvcAutoConfiguration {
|
||||
return new RemoveContentLengthRequestHeadersFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(prefix = GatewayMvcProperties.PREFIX,
|
||||
name = "remove-http2-status-response-headers-filter.enabled", matchIfMissing = true)
|
||||
public RemoveHttp2StatusResponseHeadersFilter removeHttp2StatusResponseHeadersFilter() {
|
||||
return new RemoveHttp2StatusResponseHeadersFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(prefix = GatewayMvcProperties.PREFIX,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
|
||||
import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchange;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
public abstract class AbstractProxyExchange implements ProxyExchange {
|
||||
|
||||
private final GatewayMvcProperties properties;
|
||||
|
||||
protected AbstractProxyExchange(GatewayMvcProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
protected int copyResponseBody(ClientHttpResponse clientResponse, InputStream inputStream,
|
||||
OutputStream outputStream) throws IOException {
|
||||
Assert.notNull(clientResponse, "No ClientResponse specified");
|
||||
Assert.notNull(inputStream, "No InputStream specified");
|
||||
Assert.notNull(outputStream, "No OutputStream specified");
|
||||
|
||||
int transferredBytes;
|
||||
|
||||
if (properties.getStreamingMediaTypes().contains(clientResponse.getHeaders().getContentType())) {
|
||||
transferredBytes = copyResponseBodyWithFlushing(inputStream, outputStream);
|
||||
}
|
||||
else {
|
||||
transferredBytes = StreamUtils.copy(inputStream, outputStream);
|
||||
}
|
||||
|
||||
return transferredBytes;
|
||||
}
|
||||
|
||||
private int copyResponseBodyWithFlushing(InputStream inputStream, OutputStream outputStream) throws IOException {
|
||||
int readBytes;
|
||||
var totalReadBytes = 0;
|
||||
var buffer = new byte[properties.getStreamingBufferSize()];
|
||||
|
||||
while ((readBytes = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, readBytes);
|
||||
outputStream.flush();
|
||||
if (totalReadBytes < Integer.MAX_VALUE) {
|
||||
try {
|
||||
totalReadBytes = Math.addExact(totalReadBytes, readBytes);
|
||||
}
|
||||
catch (ArithmeticException e) {
|
||||
totalReadBytes = Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
|
||||
return totalReadBytes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc.common;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
@@ -36,7 +38,7 @@ public class HttpStatusHolder {
|
||||
public static HttpStatusHolder valueOf(String status) {
|
||||
HttpStatusCode httpStatus;
|
||||
try {
|
||||
httpStatus = HttpStatus.valueOf(status.toUpperCase());
|
||||
httpStatus = HttpStatus.valueOf(status.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
httpStatus = null;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2012-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.Map;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public class MultipartEnvironmentPostProcessor implements EnvironmentPostProcessor {
|
||||
|
||||
/* for testing */ static final String MULTIPART_ENABLED_PROPERTY = "spring.servlet.multipart.enabled";
|
||||
|
||||
/* for testing */ static final String MULTIPART_PROPERTY_SOURCE_NAME = "gatewayServerWebmvcMultipartPropertySource";
|
||||
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
||||
String multipartEnabled = environment.getProperty(MULTIPART_ENABLED_PROPERTY);
|
||||
if (!StringUtils.hasText(multipartEnabled)) {
|
||||
// no user set property, set it to false.
|
||||
MapPropertySource propertySource = new MapPropertySource(MULTIPART_PROPERTY_SOURCE_NAME,
|
||||
Map.of(MULTIPART_ENABLED_PROPERTY, Boolean.FALSE));
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -72,6 +73,11 @@ public abstract class MvcUtils {
|
||||
*/
|
||||
public static final String GATEWAY_ATTRIBUTES_ATTR = qualify("gatewayAttributes");
|
||||
|
||||
/**
|
||||
* Gateway original request URL attribute name.
|
||||
*/
|
||||
public static final String GATEWAY_ORIGINAL_REQUEST_URL_ATTR = qualify("gatewayOriginalRequestUrl");
|
||||
|
||||
/**
|
||||
* Gateway request URL attribute name.
|
||||
*/
|
||||
@@ -116,6 +122,14 @@ public abstract class MvcUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static ByteArrayInputStream getOrCacheBody(ServerRequest request) {
|
||||
ByteArrayInputStream body = getAttribute(request, MvcUtils.CACHED_REQUEST_BODY_ATTR);
|
||||
if (body != null) {
|
||||
return body;
|
||||
}
|
||||
return cacheBody(request);
|
||||
}
|
||||
|
||||
public static String expand(ServerRequest request, String template) {
|
||||
Assert.notNull(request, "request may not be null");
|
||||
Assert.notNull(template, "template may not be null");
|
||||
@@ -242,6 +256,13 @@ public abstract class MvcUtils {
|
||||
request.servletRequest().setAttribute(GATEWAY_REQUEST_URL_ATTR, url);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void addOriginalRequestUrl(ServerRequest request, URI url) {
|
||||
LinkedHashSet<URI> urls = (LinkedHashSet<URI>) request.attributes()
|
||||
.computeIfAbsent(GATEWAY_ORIGINAL_REQUEST_URL_ATTR, s -> new LinkedHashSet<>());
|
||||
urls.add(url);
|
||||
}
|
||||
|
||||
private record ByteArrayInputMessage(ServerRequest request, ByteArrayInputStream body) implements HttpInputMessage {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.cloud.gateway.server.mvc.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
@@ -26,6 +27,7 @@ import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
@ConfigurationProperties(GatewayMvcProperties.PREFIX)
|
||||
public class GatewayMvcProperties {
|
||||
@@ -51,6 +53,18 @@ public class GatewayMvcProperties {
|
||||
|
||||
private HttpClient httpClient = new HttpClient();
|
||||
|
||||
/**
|
||||
* Mime-types that are streaming.
|
||||
*/
|
||||
private List<MediaType> streamingMediaTypes = Arrays.asList(MediaType.TEXT_EVENT_STREAM,
|
||||
new MediaType("application", "stream+json"), new MediaType("application", "grpc"),
|
||||
new MediaType("application", "grpc+protobuf"), new MediaType("application", "grpc+json"));
|
||||
|
||||
/**
|
||||
* Buffer size for streaming media mime-types.
|
||||
*/
|
||||
private int streamingBufferSize = 16384;
|
||||
|
||||
public List<RouteProperties> getRoutes() {
|
||||
return routes;
|
||||
}
|
||||
@@ -71,11 +85,29 @@ public class GatewayMvcProperties {
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public List<MediaType> getStreamingMediaTypes() {
|
||||
return streamingMediaTypes;
|
||||
}
|
||||
|
||||
public void setStreamingMediaTypes(List<MediaType> streamingMediaTypes) {
|
||||
this.streamingMediaTypes = streamingMediaTypes;
|
||||
}
|
||||
|
||||
public int getStreamingBufferSize() {
|
||||
return streamingBufferSize;
|
||||
}
|
||||
|
||||
public void setStreamingBufferSize(int streamingBufferSize) {
|
||||
this.streamingBufferSize = streamingBufferSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this).append("httpClient", httpClient)
|
||||
.append("routes", routes)
|
||||
.append("routesMap", routesMap)
|
||||
.append("streamingMediaTypes", streamingMediaTypes)
|
||||
.append("streamingBufferSize", streamingBufferSize)
|
||||
.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import org.springframework.cloud.gateway.server.mvc.invoke.OperationParameter;
|
||||
import org.springframework.cloud.gateway.server.mvc.invoke.OperationParameters;
|
||||
import org.springframework.cloud.gateway.server.mvc.invoke.reflect.DefaultOperationMethod;
|
||||
import org.springframework.cloud.gateway.server.mvc.invoke.reflect.OperationMethod;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -52,8 +54,9 @@ public class NormalizedOperationMethod implements OperationMethod {
|
||||
}
|
||||
|
||||
public boolean isConfigurable() {
|
||||
Configurable annotation = delegate.getMethod().getAnnotation(Configurable.class);
|
||||
return annotation != null && delegate.getParameters().getParameterCount() == 1;
|
||||
MergedAnnotation<Configurable> configurable = MergedAnnotations.from(delegate.getMethod())
|
||||
.get(Configurable.class);
|
||||
return configurable.isPresent() && delegate.getParameters().getParameterCount() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,8 +75,10 @@ public class NormalizedOperationMethod implements OperationMethod {
|
||||
|
||||
private Map<String, Object> normalizeArgs(Map<String, Object> operationArgs) {
|
||||
if (hasGeneratedKey(operationArgs)) {
|
||||
Shortcut shortcut = getMethod().getAnnotation(Shortcut.class);
|
||||
if (shortcut != null) {
|
||||
MergedAnnotation<Shortcut> shortcutMergedAnnotation = MergedAnnotations.from(delegate.getMethod())
|
||||
.get(Shortcut.class);
|
||||
if (shortcutMergedAnnotation.isPresent()) {
|
||||
Shortcut shortcut = shortcutMergedAnnotation.synthesize();
|
||||
String[] fieldOrder = getFieldOrder(shortcut);
|
||||
return switch (shortcut.type()) {
|
||||
case DEFAULT -> {
|
||||
|
||||
@@ -16,10 +16,14 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -161,13 +165,13 @@ public class RouterFunctionHolderFactory {
|
||||
String scheme = routeProperties.getUri().getScheme();
|
||||
Map<String, Object> handlerArgs = new HashMap<>();
|
||||
Optional<NormalizedOperationMethod> handlerOperationMethod = findOperation(handlerOperations,
|
||||
scheme.toLowerCase(), handlerArgs);
|
||||
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(), handlerArgs);
|
||||
handlerOperationMethod = findOperation(handlerOperations, scheme.toLowerCase(Locale.ROOT), handlerArgs);
|
||||
if (handlerOperationMethod.isEmpty()) {
|
||||
throw new IllegalStateException("Unable to find HandlerFunction for scheme: " + scheme);
|
||||
}
|
||||
@@ -175,12 +179,15 @@ public class RouterFunctionHolderFactory {
|
||||
NormalizedOperationMethod normalizedOpMethod = handlerOperationMethod.get();
|
||||
Object response = invokeOperation(normalizedOpMethod, normalizedOpMethod.getNormalizedArgs());
|
||||
HandlerFunction<ServerResponse> handlerFunction = null;
|
||||
|
||||
// filters added by HandlerDiscoverer need to go last, so save them
|
||||
List<HandlerFilterFunction<ServerResponse, ServerResponse>> handlerFilterFunctionFilters = new ArrayList<>();
|
||||
if (response instanceof HandlerFunction<?>) {
|
||||
handlerFunction = (HandlerFunction<ServerResponse>) response;
|
||||
}
|
||||
else if (response instanceof HandlerDiscoverer.Result result) {
|
||||
handlerFunction = result.getHandlerFunction();
|
||||
result.getFilters().forEach(builder::filter);
|
||||
handlerFilterFunctionFilters.addAll(result.getFilters());
|
||||
}
|
||||
if (handlerFunction == null) {
|
||||
throw new IllegalStateException(
|
||||
@@ -218,6 +225,9 @@ public class RouterFunctionHolderFactory {
|
||||
translate(filterOperations, filterProperties.getName(), args, HandlerFilterFunction.class, builder::filter);
|
||||
});
|
||||
|
||||
// HandlerDiscoverer filters need higher priority, so put them last
|
||||
handlerFilterFunctionFilters.forEach(builder::filter);
|
||||
|
||||
builder.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, routeId);
|
||||
|
||||
return builder.build();
|
||||
@@ -233,6 +243,11 @@ public class RouterFunctionHolderFactory {
|
||||
if (handlerFilterFunction != null) {
|
||||
operationHandler.accept(handlerFilterFunction);
|
||||
}
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(LogMessage.format("Yaml Properties matched Operations name: %s, args: %s, params: %s",
|
||||
normalizedName, opMethod.getNormalizedArgs().toString(),
|
||||
Arrays.toString(opMethod.getParameters().stream().toArray())));
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException(String.format("Unable to find operation %s for %s with args %s",
|
||||
@@ -244,6 +259,7 @@ public class RouterFunctionHolderFactory {
|
||||
String operationName, Map<String, Object> operationArgs) {
|
||||
return operations.getOrDefault(operationName, Collections.emptyList())
|
||||
.stream()
|
||||
.sorted(Comparator.comparing(OperationMethod::isConfigurable))
|
||||
.map(operationMethod -> new NormalizedOperationMethod(operationMethod, operationArgs))
|
||||
.filter(opeMethod -> matchOperation(opeMethod, operationArgs))
|
||||
.findFirst();
|
||||
@@ -272,7 +288,7 @@ public class RouterFunctionHolderFactory {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
if (operationMethod.isConfigurable()) {
|
||||
OperationParameter operationParameter = operationMethod.getParameters().get(0);
|
||||
Object config = bindConfigurable(operationMethod, args, operationParameter);
|
||||
Object config = bindConfigurable(operationMethod, operationArgs, operationParameter);
|
||||
args.put(operationParameter.getName(), config);
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriTemplate;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import static org.springframework.cloud.gateway.server.mvc.common.MvcUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
|
||||
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
|
||||
@@ -188,12 +189,14 @@ public abstract class BeforeFilterFunctions {
|
||||
final UriTemplate uriTemplate = new UriTemplate(prefix);
|
||||
|
||||
return request -> {
|
||||
MvcUtils.addOriginalRequestUrl(request, request.uri());
|
||||
Map<String, Object> uriVariables = MvcUtils.getUriTemplateVariables(request);
|
||||
URI uri = uriTemplate.expand(uriVariables);
|
||||
|
||||
String newPath = uri.getRawPath() + request.uri().getRawPath();
|
||||
|
||||
URI prefixedUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(newPath).build().toUri();
|
||||
MvcUtils.setRequestUrl(request, prefixedUri);
|
||||
return ServerRequest.from(request).uri(prefixedUri).build();
|
||||
};
|
||||
}
|
||||
@@ -214,10 +217,12 @@ public abstract class BeforeFilterFunctions {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(request.params());
|
||||
queryParams.remove(name);
|
||||
|
||||
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
|
||||
|
||||
// remove from uri
|
||||
URI newUri = UriComponentsBuilder.fromUri(request.uri())
|
||||
.replaceQueryParams(unmodifiableMultiValueMap(queryParams))
|
||||
.build()
|
||||
.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams))
|
||||
.build(true)
|
||||
.toUri();
|
||||
|
||||
// remove resolved params from request
|
||||
@@ -323,7 +328,7 @@ public abstract class BeforeFilterFunctions {
|
||||
String normalizedReplacement = replacement.replace("$\\", "$");
|
||||
Pattern pattern = Pattern.compile(regexp);
|
||||
return request -> {
|
||||
// TODO: original request url
|
||||
MvcUtils.addOriginalRequestUrl(request, request.uri());
|
||||
String path = request.uri().getPath();
|
||||
String newPath = pattern.matcher(path).replaceAll(normalizedReplacement);
|
||||
|
||||
@@ -331,8 +336,7 @@ public abstract class BeforeFilterFunctions {
|
||||
|
||||
ServerRequest modified = ServerRequest.from(request).uri(rewrittenUri).build();
|
||||
|
||||
// TODO: can this be restored at some point?
|
||||
// MvcUtils.setRequestUrl(modified, modified.uri());
|
||||
MvcUtils.setRequestUrl(request, rewrittenUri);
|
||||
return modified;
|
||||
};
|
||||
}
|
||||
@@ -348,12 +352,13 @@ public abstract class BeforeFilterFunctions {
|
||||
UriTemplate uriTemplate = new UriTemplate(path);
|
||||
|
||||
return request -> {
|
||||
MvcUtils.addOriginalRequestUrl(request, request.uri());
|
||||
Map<String, Object> uriVariables = MvcUtils.getUriTemplateVariables(request);
|
||||
URI uri = uriTemplate.expand(uriVariables);
|
||||
String newPath = uri.getRawPath();
|
||||
|
||||
URI prefixedUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(newPath).build().toUri();
|
||||
return ServerRequest.from(request).uri(prefixedUri).build();
|
||||
URI newUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(uri.getRawPath()).build(true).toUri();
|
||||
MvcUtils.setRequestUrl(request, newUri);
|
||||
return ServerRequest.from(request).uri(newUri).build();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -384,6 +389,7 @@ public abstract class BeforeFilterFunctions {
|
||||
|
||||
public static Function<ServerRequest, ServerRequest> stripPrefix(int parts) {
|
||||
return request -> {
|
||||
MvcUtils.addOriginalRequestUrl(request, request.uri());
|
||||
// TODO: gateway url attributes
|
||||
String path = request.uri().getRawPath();
|
||||
// TODO: begin duplicate code from StripPrefixGatewayFilterFactory
|
||||
@@ -407,8 +413,10 @@ public abstract class BeforeFilterFunctions {
|
||||
|
||||
URI prefixedUri = UriComponentsBuilder.fromUri(request.uri())
|
||||
.replacePath(newPath.toString())
|
||||
.build()
|
||||
.build(true)
|
||||
.toUri();
|
||||
MvcUtils.setRequestUrl(request, prefixedUri);
|
||||
|
||||
return ServerRequest.from(request).uri(prefixedUri).build();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public abstract class CircuitBreakerFilterFunctions {
|
||||
return circuitBreaker(config);
|
||||
}
|
||||
|
||||
@Shortcut
|
||||
@Shortcut("id")
|
||||
@Configurable
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> circuitBreaker(CircuitBreakerConfig config) {
|
||||
Set<HttpStatusCode> failureStatuses = config.getStatusCodes()
|
||||
|
||||
@@ -66,6 +66,7 @@ public interface FilterFunctions {
|
||||
return ofResponseProcessor(AfterFilterFunctions.addResponseHeader(name, values));
|
||||
}
|
||||
|
||||
@Shortcut
|
||||
static HandlerFilterFunction<ServerResponse, ServerResponse> dedupeResponseHeader(String name) {
|
||||
return ofResponseProcessor(AfterFilterFunctions.dedupeResponseHeader(name));
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
|
||||
HttpHeaders updated = new HttpHeaders();
|
||||
|
||||
// copy all headers except Forwarded
|
||||
for (Map.Entry<String, List<String>> entry : original.entrySet()) {
|
||||
for (Map.Entry<String, List<String>> entry : original.headerSet()) {
|
||||
if (!entry.getKey().equalsIgnoreCase(FORWARDED_HEADER)) {
|
||||
updated.addAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ public abstract class LoadBalancerFilterFunctions {
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> lb(String serviceId,
|
||||
BiFunction<ServiceInstance, URI, URI> reconstructUriFunction) {
|
||||
return (request, next) -> {
|
||||
MvcUtils.addOriginalRequestUrl(request, request.uri());
|
||||
|
||||
LoadBalancerClientFactory clientFactory = getApplicationContext(request)
|
||||
.getBean(LoadBalancerClientFactory.class);
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.server.mvc.filter;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -54,8 +55,8 @@ public class RemoveHopByHopRequestHeadersFilter implements RequestHttpHeadersFil
|
||||
static HttpHeaders filter(HttpHeaders input, Set<String> headersToRemove) {
|
||||
HttpHeaders filtered = new HttpHeaders();
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : input.entrySet()) {
|
||||
if (!headersToRemove.contains(entry.getKey().toLowerCase())) {
|
||||
for (Map.Entry<String, List<String>> entry : input.headerSet()) {
|
||||
if (!headersToRemove.contains(entry.getKey().toLowerCase(Locale.ROOT))) {
|
||||
filtered.addAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
// jdk http client add the :status pseudo-header to the response.
|
||||
// Copying this header to an upstream HTTP/2 connection will cause a protocol error.
|
||||
public class RemoveHttp2StatusResponseHeadersFilter implements HttpHeadersFilter.ResponseHttpHeadersFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders apply(HttpHeaders input, ServerResponse serverResponse) {
|
||||
if (input.containsKey(":status")) {
|
||||
|
||||
HttpHeaders filtered = new HttpHeaders();
|
||||
filtered.addAll(input);
|
||||
filtered.remove(":status");
|
||||
return filtered;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
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.common.Shortcut;
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -48,6 +49,7 @@ public abstract class RetryFilterFunctions {
|
||||
private RetryFilterFunctions() {
|
||||
}
|
||||
|
||||
@Shortcut
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> retry(int retries) {
|
||||
return retry(config -> config.setRetries(retries));
|
||||
}
|
||||
@@ -58,7 +60,7 @@ public abstract class RetryFilterFunctions {
|
||||
return retry(config);
|
||||
}
|
||||
|
||||
@Shortcut
|
||||
@Shortcut({ "retries", "series", "methods" })
|
||||
@Configurable
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> retry(RetryConfig config) {
|
||||
RetryTemplateBuilder retryTemplateBuilder = RetryTemplate.builder();
|
||||
@@ -70,6 +72,9 @@ public abstract class RetryFilterFunctions {
|
||||
.setPolicies(Arrays.asList(simpleRetryPolicy, new HttpRetryPolicy(config)).toArray(new RetryPolicy[0]));
|
||||
RetryTemplate retryTemplate = retryTemplateBuilder.customPolicy(compositeRetryPolicy).build();
|
||||
return (request, next) -> retryTemplate.execute(context -> {
|
||||
if (config.isCacheBody()) {
|
||||
MvcUtils.getOrCacheBody(request);
|
||||
}
|
||||
ServerResponse serverResponse = next.handle(request);
|
||||
|
||||
if (isRetryableStatusCode(serverResponse.statusCode(), config)
|
||||
@@ -120,6 +125,8 @@ public abstract class RetryFilterFunctions {
|
||||
|
||||
private Set<HttpMethod> methods = new HashSet<>(List.of(HttpMethod.GET));
|
||||
|
||||
private boolean cacheBody = false;
|
||||
|
||||
// TODO: individual statuses
|
||||
// TODO: backoff
|
||||
// TODO: support more Spring Retry policies
|
||||
@@ -175,6 +182,15 @@ public abstract class RetryFilterFunctions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isCacheBody() {
|
||||
return cacheBody;
|
||||
}
|
||||
|
||||
public RetryConfig setCacheBody(boolean cacheBody) {
|
||||
this.cacheBody = cacheBody;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class RetryException extends NestedRuntimeException {
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Map;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -374,7 +375,7 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
|
||||
HttpHeaders original = input;
|
||||
HttpHeaders updated = new HttpHeaders();
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : original.entrySet()) {
|
||||
for (Map.Entry<String, List<String>> entry : original.headerSet()) {
|
||||
updated.addAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
@@ -397,18 +398,15 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
|
||||
// - see XForwardedHeadersFilterTests, so first get uris, then extract paths
|
||||
// and remove one from another if it's the ending part.
|
||||
|
||||
LinkedHashSet<URI> originalUris = null; // TODO:
|
||||
// exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
|
||||
URI requestUri = null; // TODO:
|
||||
// exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
LinkedHashSet<URI> originalUris = MvcUtils.getAttribute(request,
|
||||
MvcUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
|
||||
URI requestUri = MvcUtils.getAttribute(request, MvcUtils.GATEWAY_REQUEST_URL_ATTR);
|
||||
|
||||
if (originalUris != null && requestUri != null) {
|
||||
|
||||
originalUris.forEach(originalUri -> {
|
||||
|
||||
if (originalUri != null && originalUri.getPath() != null) {
|
||||
String prefix = originalUri.getPath();
|
||||
|
||||
// strip trailing slashes before checking if request path is end
|
||||
// of original path
|
||||
String originalUriPath = stripTrailingSlash(originalUri);
|
||||
|
||||
@@ -20,18 +20,28 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
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.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
public class ClientHttpRequestFactoryProxyExchange implements ProxyExchange {
|
||||
public class ClientHttpRequestFactoryProxyExchange extends AbstractProxyExchange {
|
||||
|
||||
private final ClientHttpRequestFactory requestFactory;
|
||||
|
||||
@Deprecated
|
||||
public ClientHttpRequestFactoryProxyExchange(ClientHttpRequestFactory requestFactory) {
|
||||
super(new GatewayMvcProperties());
|
||||
this.requestFactory = requestFactory;
|
||||
}
|
||||
|
||||
public ClientHttpRequestFactoryProxyExchange(ClientHttpRequestFactory requestFactory,
|
||||
GatewayMvcProperties properties) {
|
||||
super(properties);
|
||||
this.requestFactory = requestFactory;
|
||||
}
|
||||
|
||||
@@ -54,7 +64,8 @@ public class ClientHttpRequestFactoryProxyExchange implements ProxyExchange {
|
||||
InputStream inputStream = MvcUtils.getAttribute(request.getServerRequest(),
|
||||
MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR);
|
||||
// copy body from request to clientHttpRequest
|
||||
StreamUtils.copy(inputStream, httpServletResponse.getOutputStream());
|
||||
ClientHttpRequestFactoryProxyExchange.this.copyResponseBody(clientHttpResponse, inputStream,
|
||||
httpServletResponse.getOutputStream());
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -123,9 +123,9 @@ public class ProxyExchangeHandlerFunction
|
||||
private <REQUEST_OR_RESPONSE> HttpHeaders filterHeaders(List<?> filters, HttpHeaders original,
|
||||
REQUEST_OR_RESPONSE requestOrResponse) {
|
||||
HttpHeaders filtered = original;
|
||||
for (var filter : filters) {
|
||||
for (Object filter : filters) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var typed = ((HttpHeadersFilter<REQUEST_OR_RESPONSE>) filter);
|
||||
HttpHeadersFilter<REQUEST_OR_RESPONSE> typed = ((HttpHeadersFilter<REQUEST_OR_RESPONSE>) filter);
|
||||
filtered = typed.apply(filtered, requestOrResponse);
|
||||
}
|
||||
return filtered;
|
||||
|
||||
@@ -19,35 +19,56 @@ package org.springframework.cloud.gateway.server.mvc.handler;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
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.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
public class RestClientProxyExchange implements ProxyExchange {
|
||||
public class RestClientProxyExchange extends AbstractProxyExchange {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
@Deprecated
|
||||
public RestClientProxyExchange(RestClient restClient) {
|
||||
super(new GatewayMvcProperties());
|
||||
this.restClient = restClient;
|
||||
}
|
||||
|
||||
public RestClientProxyExchange(RestClient restClient, GatewayMvcProperties properties) {
|
||||
super(properties);
|
||||
this.restClient = restClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse exchange(Request request) {
|
||||
return restClient.method(request.getMethod())
|
||||
RestClient.RequestBodySpec requestSpec = restClient.method(request.getMethod())
|
||||
.uri(request.getUri())
|
||||
.headers(httpHeaders -> httpHeaders.putAll(request.getHeaders()))
|
||||
.body(outputStream -> copyBody(request, outputStream))
|
||||
.exchange((clientRequest, clientResponse) -> doExchange(request, clientResponse), false);
|
||||
.headers(httpHeaders -> httpHeaders.putAll(request.getHeaders()));
|
||||
if (isBodyPresent(request)) {
|
||||
requestSpec.body(outputStream -> copyBody(request, outputStream));
|
||||
}
|
||||
return requestSpec.exchange((clientRequest, clientResponse) -> doExchange(request, clientResponse), false);
|
||||
}
|
||||
|
||||
private static boolean isBodyPresent(Request request) {
|
||||
try {
|
||||
return !request.getServerRequest().servletRequest().getInputStream().isFinished();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static int copyBody(Request request, OutputStream outputStream) throws IOException {
|
||||
return StreamUtils.copy(request.getServerRequest().servletRequest().getInputStream(), outputStream);
|
||||
}
|
||||
|
||||
private static ServerResponse doExchange(Request request, ClientHttpResponse clientResponse) throws IOException {
|
||||
private ServerResponse doExchange(Request request, ClientHttpResponse clientResponse) throws IOException {
|
||||
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);
|
||||
@@ -59,7 +80,8 @@ public class RestClientProxyExchange implements ProxyExchange {
|
||||
InputStream inputStream = MvcUtils.getAttribute(request.getServerRequest(),
|
||||
MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR);
|
||||
// copy body from request to clientHttpRequest
|
||||
StreamUtils.copy(inputStream, httpServletResponse.getOutputStream());
|
||||
RestClientProxyExchange.this.copyResponseBody(clientResponse, inputStream,
|
||||
httpServletResponse.getOutputStream());
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -374,6 +374,9 @@ public abstract class GatewayRequestPredicates {
|
||||
@Override
|
||||
public boolean test(ServerRequest request) {
|
||||
String host = request.headers().firstHeader(HttpHeaders.HOST);
|
||||
if (host == null) {
|
||||
host = "";
|
||||
}
|
||||
PathContainer pathContainer = PathContainer.parsePath(host, PathContainer.Options.MESSAGE_ROUTE);
|
||||
PathPattern.PathMatchInfo info = this.pattern.matchAndExtract(pathContainer);
|
||||
traceMatch("Pattern", this.pattern.getPatternString(), host, info != null);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
#
|
||||
# Copyright 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.
|
||||
#
|
||||
#
|
||||
|
||||
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,\
|
||||
@@ -12,3 +29,6 @@ org.springframework.cloud.gateway.server.mvc.handler.HandlerSupplier=\
|
||||
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.common.MultipartEnvironmentPostProcessor
|
||||
@@ -28,6 +28,7 @@ import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeade
|
||||
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;
|
||||
@@ -46,6 +47,7 @@ public class GatewayServerMvcAutoConfigurationTests {
|
||||
"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")
|
||||
@@ -55,6 +57,7 @@ public class GatewayServerMvcAutoConfigurationTests {
|
||||
assertThat(context).doesNotHaveBean(RemoveContentLengthRequestHeadersFilter.class);
|
||||
assertThat(context).doesNotHaveBean(RemoveHopByHopRequestHeadersFilter.class);
|
||||
assertThat(context).doesNotHaveBean(RemoveHopByHopResponseHeadersFilter.class);
|
||||
assertThat(context).doesNotHaveBean(RemoveHttp2StatusResponseHeadersFilter.class);
|
||||
assertThat(context).doesNotHaveBean(TransferEncodingNormalizationRequestHeadersFilter.class);
|
||||
assertThat(context).doesNotHaveBean(WeightCalculatorFilter.class);
|
||||
assertThat(context).doesNotHaveBean(XForwardedRequestHeadersFilter.class);
|
||||
|
||||
@@ -22,11 +22,11 @@ import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
@@ -39,8 +39,6 @@ import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -78,10 +76,8 @@ import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.function.HandlerFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
@@ -116,7 +112,6 @@ import static org.springframework.cloud.gateway.server.mvc.filter.CircuitBreaker
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeader;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeadersIfNotPresent;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestParameter;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.redirectTo;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.removeRequestHeader;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.rewritePath;
|
||||
@@ -125,7 +120,6 @@ import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunction
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.setRequestHostHeader;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.stripPrefix;
|
||||
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.forward;
|
||||
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
|
||||
@@ -241,6 +235,7 @@ public class ServerMvcIntegrationTests {
|
||||
public void stripPrefixWorks() {
|
||||
restClient.get()
|
||||
.uri("/long/path/to/get")
|
||||
.header("Host", "www.stripprefix.org")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
@@ -248,6 +243,13 @@ public class ServerMvcIntegrationTests {
|
||||
.consumeWith(res -> {
|
||||
Map<String, Object> map = res.getResponseBody();
|
||||
Map<String, Object> headers = getMap(map, "headers");
|
||||
assertThat(headers).containsKeys(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER);
|
||||
assertThat(headers).containsEntry(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
|
||||
"/long/path/to");
|
||||
assertThat(headers).containsEntry("X-Test", "stripPrefix");
|
||||
});
|
||||
}
|
||||
@@ -266,10 +268,40 @@ public class ServerMvcIntegrationTests {
|
||||
Map<String, Object> map = res.getResponseBody();
|
||||
assertThat(map).containsEntry("data", "hello");
|
||||
Map<String, Object> headers = getMap(map, "headers");
|
||||
assertThat(headers).containsKeys(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER);
|
||||
assertThat(headers).containsEntry(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
|
||||
"/long/path/to");
|
||||
assertThat(headers).containsEntry("X-Test", "stripPrefixPost");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stripPrefixLbWorks() {
|
||||
restClient.get()
|
||||
.uri("/long/path/to/get")
|
||||
.header("Host", "www.stripprefixlb.org")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(Map.class)
|
||||
.consumeWith(res -> {
|
||||
Map<String, Object> map = res.getResponseBody();
|
||||
Map<String, Object> headers = getMap(map, "headers");
|
||||
assertThat(headers).containsKeys(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER,
|
||||
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER);
|
||||
assertThat(headers).containsEntry(XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER,
|
||||
"/long/path/to");
|
||||
assertThat(headers).containsEntry("X-Test", "stripPrefix");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setStatusGatewayRouterFunctionWorks() {
|
||||
restClient.get()
|
||||
@@ -392,20 +424,6 @@ public class ServerMvcIntegrationTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryWorks() {
|
||||
restClient.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3");
|
||||
// test for: java.lang.IllegalArgumentException: You have already selected another
|
||||
// retry policy
|
||||
restClient.get()
|
||||
.uri("/retry?key=get2")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rateLimitWorks() {
|
||||
restClient.get().uri("/anything/ratelimit").exchange().expectStatus().isOk();
|
||||
@@ -483,7 +501,7 @@ public class ServerMvcIntegrationTests {
|
||||
@Test
|
||||
public void rewritePathPostLocalWorks() {
|
||||
restClient.post()
|
||||
.uri("/baz/post")
|
||||
.uri("/baz/localpost")
|
||||
.bodyValue("hello")
|
||||
.header("Host", "www.rewritepathpostlocal.org")
|
||||
.exchange()
|
||||
@@ -635,8 +653,21 @@ public class ServerMvcIntegrationTests {
|
||||
private void assertMultipartData(Map responseBody) {
|
||||
Map<String, Object> files = (Map<String, Object>) responseBody.get("files");
|
||||
assertThat(files).containsKey("imgpart");
|
||||
String file = (String) files.get("imgpart");
|
||||
assertThat(file).startsWith("data:").contains(";base64,");
|
||||
Object imgpart = files.get("imgpart");
|
||||
if (imgpart instanceof List l) {
|
||||
String file = (String) l.get(0);
|
||||
assertThat(isPNG(file.getBytes()));
|
||||
}
|
||||
else {
|
||||
String file = (String) imgpart;
|
||||
assertThat(file).startsWith("data:").contains(";base64,");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPNG(byte[] bytes) {
|
||||
byte[] pngSignature = { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
byte[] header = Arrays.copyOf(bytes, pngSignature.length);
|
||||
return Arrays.equals(pngSignature, header);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -990,11 +1021,6 @@ public class ServerMvcIntegrationTests {
|
||||
return new TestHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RetryController retryController() {
|
||||
return new RetryController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
EventController eventController() {
|
||||
return new EventController();
|
||||
@@ -1070,8 +1096,8 @@ public class ServerMvcIntegrationTests {
|
||||
// @formatter:off
|
||||
return route("testsetpath")
|
||||
.route(POST("/mycustompath{extra}").and(host("**.setpathpost.org")), http())
|
||||
.filter(new HttpbinUriResolver())
|
||||
.filter(setPath("/{extra}"))
|
||||
.filter(new HttpbinUriResolver())
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -1079,11 +1105,12 @@ public class ServerMvcIntegrationTests {
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> gatewayRouterFunctionsStripPrefix() {
|
||||
// @formatter:off
|
||||
return route(GET("/long/path/to/get"), http())
|
||||
.filter(new HttpbinUriResolver())
|
||||
return route("teststripprefix")
|
||||
.route(GET("/long/path/to/get").and(host("**.stripprefix.org")), http())
|
||||
.filter(stripPrefix(3))
|
||||
.filter(addRequestHeader("X-Test", "stripPrefix"))
|
||||
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "teststripprefix");
|
||||
.filter(new HttpbinUriResolver())
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@@ -1092,9 +1119,21 @@ public class ServerMvcIntegrationTests {
|
||||
// @formatter:off
|
||||
return route("teststripprefixpost")
|
||||
.route(POST("/long/path/to/post").and(host("**.stripprefixpost.org")), http())
|
||||
.filter(new HttpbinUriResolver())
|
||||
.filter(stripPrefix(3))
|
||||
.filter(addRequestHeader("X-Test", "stripPrefixPost"))
|
||||
.filter(new HttpbinUriResolver())
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> gatewayRouterFunctionsStripPrefixLb() {
|
||||
// @formatter:off
|
||||
return route("teststripprefix")
|
||||
.route(GET("/long/path/to/get").and(host("**.stripprefixlb.org")), http())
|
||||
.filter(stripPrefix(3))
|
||||
.filter(addRequestHeader("X-Test", "stripPrefix"))
|
||||
.filter(lb("httpbin"))
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -1179,19 +1218,6 @@ public class ServerMvcIntegrationTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> gatewayRouterFunctionsRetry() {
|
||||
// @formatter:off
|
||||
return route("testretry")
|
||||
.route(path("/retry"), http())
|
||||
.before(new LocalServerPortUriResolver())
|
||||
.filter(retry(3))
|
||||
//.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST))))
|
||||
.filter(prefixPath("/do"))
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> gatewayRouterFunctionsRateLimit() {
|
||||
// @formatter:off
|
||||
@@ -1278,8 +1304,7 @@ public class ServerMvcIntegrationTests {
|
||||
// @formatter:off
|
||||
return route("testform")
|
||||
.POST("/post", host("**.testform.org"), http())
|
||||
.before(new LocalServerPortUriResolver())
|
||||
.filter(prefixPath("/test"))
|
||||
.filter(new HttpbinUriResolver())
|
||||
.filter(addRequestHeader("X-Test", "form"))
|
||||
.build();
|
||||
// @formatter:on
|
||||
@@ -1443,8 +1468,8 @@ public class ServerMvcIntegrationTests {
|
||||
return route("requestheadertorequesturi")
|
||||
.route(cloudFoundryRouteService().and(host("**.requestheadertorequesturi.org")), http())
|
||||
//.before(new HttpbinUriResolver()) NO URI RESOLVER!
|
||||
.before(requestHeaderToRequestUri("X-CF-Forwarded-Url"))
|
||||
.filter(setPath("/hello"))
|
||||
.before(requestHeaderToRequestUri("X-CF-Forwarded-Url"))
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -1533,12 +1558,12 @@ public class ServerMvcIntegrationTests {
|
||||
return route("testmodifyrequestbodystring")
|
||||
.POST("/post", host("**.modifyrequestbodystring.org"), http())
|
||||
.before(new HttpbinUriResolver())
|
||||
.before(modifyRequestBody(String.class, String.class, null, (request, s) -> s.toUpperCase() + s.toUpperCase()))
|
||||
.before(modifyRequestBody(String.class, String.class, null, (request, s) -> s.toUpperCase(Locale.ROOT) + s.toUpperCase(Locale.ROOT)))
|
||||
.build().and(
|
||||
route("testmodifyrequestbodyobject")
|
||||
.POST("/post", host("**.modifyrequestbodyobject.org"), http())
|
||||
.before(new HttpbinUriResolver())
|
||||
.before(modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, (request, s) -> new Hello(s.toUpperCase())))
|
||||
.before(modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, (request, s) -> new Hello(s.toUpperCase(Locale.ROOT))))
|
||||
.build());
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -1676,37 +1701,6 @@ public class ServerMvcIntegrationTests {
|
||||
|
||||
}
|
||||
|
||||
@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 = getCount(key);
|
||||
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);
|
||||
}
|
||||
|
||||
AtomicInteger getCount(String key) {
|
||||
return map.computeIfAbsent(key, s -> new AtomicInteger());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class TestHandler implements HandlerFunction<ServerResponse> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -79,7 +80,7 @@ public class VanillaRouterFunctionTests {
|
||||
// @formatter:off
|
||||
return RouterFunctions.route()
|
||||
.POST("/anything/routerfunctionsroute", host("**.routerfunctionsroute.org"), http())
|
||||
.before(modifyRequestBody(String.class, String.class, null, (request, s) -> s.toUpperCase()))
|
||||
.before(modifyRequestBody(String.class, String.class, null, (request, s) -> s.toUpperCase(Locale.ROOT)))
|
||||
.before(new HttpbinUriResolver())
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Jens Mallien
|
||||
*/
|
||||
public class AbstractProxyExchangeTests {
|
||||
|
||||
@Test
|
||||
public void copyResponseBodyForJson() throws IOException {
|
||||
MockClientHttpResponse mockResponse = new MockClientHttpResponse(new byte[0], 200);
|
||||
mockResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
InputStream inputStream = mock(InputStream.class);
|
||||
when(inputStream.transferTo(any())).thenReturn(3L);
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
|
||||
int result = new TestProxyExchange().copyResponseBody(mockResponse, inputStream, outputStream);
|
||||
|
||||
assertThat(result).isEqualTo(3);
|
||||
verify(outputStream, times(1)).flush();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copyResponseBodyForTextEventStream() throws IOException {
|
||||
MockClientHttpResponse mockResponse = new MockClientHttpResponse(new byte[0], 200);
|
||||
mockResponse.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM);
|
||||
|
||||
InputStream inputStream = mock(InputStream.class);
|
||||
when(inputStream.read(any())).thenReturn(1).thenReturn(1).thenReturn(1).thenReturn(-1);
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
|
||||
int result = new TestProxyExchange().copyResponseBody(mockResponse, inputStream, outputStream);
|
||||
|
||||
assertThat(result).isEqualTo(3);
|
||||
verify(outputStream, times(4)).flush();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copyResponseBodyWithoutContentType() throws IOException {
|
||||
MockClientHttpResponse mockResponse = new MockClientHttpResponse(new byte[0], 200);
|
||||
|
||||
InputStream inputStream = mock(InputStream.class);
|
||||
when(inputStream.transferTo(any())).thenReturn(3L);
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
|
||||
int result = new TestProxyExchange().copyResponseBody(mockResponse, inputStream, outputStream);
|
||||
|
||||
assertThat(result).isEqualTo(3);
|
||||
verify(outputStream, times(1)).flush();
|
||||
}
|
||||
|
||||
class TestProxyExchange extends AbstractProxyExchange {
|
||||
|
||||
protected TestProxyExchange() {
|
||||
super(new GatewayMvcProperties());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse exchange(Request request) {
|
||||
return ServerResponse.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.gateway.server.mvc.common.MultipartEnvironmentPostProcessor.MULTIPART_ENABLED_PROPERTY;
|
||||
import static org.springframework.cloud.gateway.server.mvc.common.MultipartEnvironmentPostProcessor.MULTIPART_PROPERTY_SOURCE_NAME;
|
||||
|
||||
public class MultipartEnvironmentPostProcessorTests {
|
||||
|
||||
@Test
|
||||
void multipartDisabledByDefault() {
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
MultipartEnvironmentPostProcessor processor = new MultipartEnvironmentPostProcessor();
|
||||
processor.postProcessEnvironment(environment, null);
|
||||
|
||||
assertThat(environment.getPropertySources().contains(MULTIPART_PROPERTY_SOURCE_NAME)).isTrue();
|
||||
|
||||
Boolean multipartEnabled = environment.getProperty(MULTIPART_ENABLED_PROPERTY, Boolean.class);
|
||||
assertThat(multipartEnabled).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipartEnabledByUser() {
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
environment.setProperty(MULTIPART_ENABLED_PROPERTY, "true");
|
||||
MultipartEnvironmentPostProcessor processor = new MultipartEnvironmentPostProcessor();
|
||||
processor.postProcessEnvironment(environment, null);
|
||||
|
||||
assertThat(environment.getPropertySources().contains(MULTIPART_PROPERTY_SOURCE_NAME)).isFalse();
|
||||
|
||||
Boolean multipartEnabled = environment.getProperty(MULTIPART_ENABLED_PROPERTY, Boolean.class);
|
||||
assertThat(multipartEnabled).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
|
||||
predicate.accept(new AbstractRequestPredicatesVisitor() {
|
||||
@Override
|
||||
public void path(String pattern) {
|
||||
assertThat(pattern).isEqualTo("/anything/listRoute3");
|
||||
assertThat(pattern).isEqualTo("/extra/anything/listRoute3");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -181,7 +181,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void lbRouteWorks() {
|
||||
restClient.get()
|
||||
.uri("/anything/listRoute3")
|
||||
.uri("/extra/anything/listRoute3")
|
||||
.header("MyHeaderName", "MyHeaderVal")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
|
||||
@@ -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.
|
||||
@@ -27,12 +27,163 @@ import org.springframework.web.servlet.function.ServerRequest;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author raccoonback
|
||||
* @author Jens Mallien
|
||||
*/
|
||||
public class BeforeFilterFunctionsTests {
|
||||
class BeforeFilterFunctionsTests {
|
||||
|
||||
@Test
|
||||
public void rewritePath() {
|
||||
void setPath() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/legacy/path")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.setPath("/new/path").apply(request);
|
||||
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/new/path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setEncodedPath() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/legacy/path")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.setPath("/new/é").apply(request);
|
||||
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/new/%C3%A9");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setPathWithParameters() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/legacy/path")
|
||||
.queryParam("foo", "bar")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.setPath("/new/path").apply(request);
|
||||
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/new/path?foo=bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setPathWithEncodedParameters() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/legacy/path")
|
||||
.queryParam("foo[]", "bar[]")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.setPath("/new/path").apply(request);
|
||||
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/new/path?foo%5B%5D=bar%5B%5D");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeRequestParameter() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path")
|
||||
.queryParam("foo", "bar")
|
||||
.queryParam("baz", "qux")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.removeRequestParameter("foo").apply(request);
|
||||
|
||||
assertThat(result.param("foo")).isEmpty();
|
||||
assertThat(result.param("baz")).isPresent().hasValue("qux");
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/path?baz=qux");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeEncodedRequestParameter() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path")
|
||||
.queryParam("foo[]", "bar")
|
||||
.queryParam("baz", "qux")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.removeRequestParameter("foo[]").apply(request);
|
||||
|
||||
assertThat(result.param("foo[]")).isEmpty();
|
||||
assertThat(result.param("baz")).isPresent().hasValue("qux");
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/path?baz=qux");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeRequestParameterWithEncodedRemainParameters() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path")
|
||||
.queryParam("foo", "bar")
|
||||
.queryParam("baz[]", "qux[]")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.removeRequestParameter("foo").apply(request);
|
||||
|
||||
assertThat(result.param("foo")).isEmpty();
|
||||
assertThat(result.param("baz[]")).isPresent().hasValue("qux[]");
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/path?baz%5B%5D=qux%5B%5D");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeRequestParameterWithEncodedPath() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/é")
|
||||
.queryParam("foo", "bar")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.removeRequestParameter("foo").apply(request);
|
||||
|
||||
assertThat(result.param("foo")).isEmpty();
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/%C3%A9");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripPrefix() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/depth1/depth2/depth3")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.stripPrefix(2).apply(request);
|
||||
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/depth3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripPrefixWithEncodedPath() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/depth1/depth2/depth3/é")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.stripPrefix(2).apply(request);
|
||||
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/depth3/%C3%A9");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripPrefixWithEncodedParameters() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/depth1/depth2/depth3")
|
||||
.queryParam("baz[]", "qux[]")
|
||||
.buildRequest(null);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
|
||||
ServerRequest result = BeforeFilterFunctions.stripPrefix(2).apply(request);
|
||||
|
||||
assertThat(result.param("baz[]")).isPresent().hasValue("qux[]");
|
||||
assertThat(result.uri().toString()).hasToString("http://localhost/depth3?baz%5B%5D=qux%5B%5D");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rewritePath() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
|
||||
.buildRequest(null);
|
||||
|
||||
@@ -44,7 +195,7 @@ public class BeforeFilterFunctionsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rewritePathWithSpace() {
|
||||
void rewritePathWithSpace() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get/path/with spaces")
|
||||
.buildRequest(null);
|
||||
|
||||
@@ -56,7 +207,7 @@ public class BeforeFilterFunctionsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rewritePathWithEnDash() {
|
||||
void rewritePathWithEnDash() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get/path/with–en–dashes")
|
||||
.buildRequest(null);
|
||||
|
||||
@@ -68,7 +219,7 @@ public class BeforeFilterFunctionsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rewritePathWithEnDashAndSpace() {
|
||||
void rewritePathWithEnDashAndSpace() {
|
||||
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get/path/with–en–dashes and spaces")
|
||||
.buildRequest(null);
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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.Set;
|
||||
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.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.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
|
||||
import org.springframework.cloud.gateway.server.mvc.test.LocalServerPortUriResolver;
|
||||
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
|
||||
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.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.adaptCachedBody;
|
||||
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath;
|
||||
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;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@SpringBootTest(properties = {}, webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
|
||||
public class RetryFilterFunctionTests {
|
||||
|
||||
@LocalServerPort
|
||||
int port;
|
||||
|
||||
@Autowired
|
||||
TestRestClient restClient;
|
||||
|
||||
@Test
|
||||
public void retryWorks() {
|
||||
restClient.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3");
|
||||
// test for: java.lang.IllegalArgumentException: You have already selected another
|
||||
// retry policy
|
||||
restClient.get()
|
||||
.uri("/retry?key=get2")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryBodyWorks() {
|
||||
restClient.post()
|
||||
.uri("/retrybody?key=post")
|
||||
.bodyValue("thebody")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("3");
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
|
||||
protected static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> gatewayRouterFunctionsRetry() {
|
||||
// @formatter:off
|
||||
return route("testretry")
|
||||
.GET("/retry", http())
|
||||
.before(new LocalServerPortUriResolver())
|
||||
.filter(retry(3))
|
||||
.filter(prefixPath("/do"))
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> gatewayRouterFunctionsRetryBody() {
|
||||
// @formatter:off
|
||||
return route("testretrybody")
|
||||
.POST("/retrybody", http())
|
||||
.before(new LocalServerPortUriResolver())
|
||||
.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR))
|
||||
.setMethods(Set.of(HttpMethod.GET, HttpMethod.POST)).setCacheBody(true)))
|
||||
.filter(adaptCachedBody())
|
||||
.filter(prefixPath("/do"))
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@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 = getCount(key);
|
||||
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);
|
||||
}
|
||||
|
||||
@PostMapping("/do/retrybody")
|
||||
public ResponseEntity<String> retryBody(@RequestParam("key") String key,
|
||||
@RequestParam(name = "count", defaultValue = "3") int count, @RequestBody String requestBody) {
|
||||
AtomicInteger num = getCount(key);
|
||||
int i = num.incrementAndGet();
|
||||
log.warn(LogMessage.format("Retry count: %s, body: %s", i, requestBody));
|
||||
String body = String.valueOf(i);
|
||||
if (!StringUtils.hasText(requestBody)) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.header("X-Retry-Count", body)
|
||||
.body("missing body");
|
||||
}
|
||||
if (i < count) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.header("X-Retry-Count", body)
|
||||
.body("temporarily broken");
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body);
|
||||
}
|
||||
|
||||
AtomicInteger getCount(String key) {
|
||||
return map.computeIfAbsent(key, s -> new AtomicInteger());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 java.util.Collections;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
|
||||
public class GatewayRequestPredicatesTests {
|
||||
|
||||
@Test
|
||||
void nullHostPassedToHostPredicate() {
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
|
||||
ServerRequest serverRequest = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
boolean result = GatewayRequestPredicates.host("*.myhost.org").test(serverRequest);
|
||||
Assertions.assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc.test;
|
||||
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
|
||||
@@ -36,7 +38,15 @@ public class HttpbinUriResolver
|
||||
String host = context.getEnvironment().getProperty("httpbin.host");
|
||||
Assert.hasText(host, "httpbin.host is not set, did you initialize HttpbinTestcontainers?");
|
||||
Assert.notNull(port, "httpbin.port is not set, did you initialize HttpbinTestcontainers?");
|
||||
return URI.create(String.format("http://%s:%d", host, port));
|
||||
URI original = request.uri();
|
||||
try {
|
||||
return new URI("http", original.getUserInfo(), host, port, original.getPath(), original.getQuery(),
|
||||
original.getFragment());
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
throw new UndeclaredThrowableException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,27 +16,21 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc.test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
@RestController
|
||||
@@ -58,35 +52,7 @@ public class TestController {
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> postFormData(HttpServletRequest request,
|
||||
@RequestParam MultiValueMap<String, MultipartFile> parts) throws ServletException, IOException {
|
||||
HashMap<String, Object> ret = new HashMap<>();
|
||||
ret.put("headers", getHeaders(request));
|
||||
HashMap<String, Object> files = new HashMap<>();
|
||||
ret.put("files", files);
|
||||
|
||||
parts.values().stream().flatMap(List::stream).forEach(part -> {
|
||||
String contentType = part.getContentType();
|
||||
long contentLength = part.getSize();
|
||||
// TODO: get part data
|
||||
files.put(part.getName(), "data:" + contentType + ";base64," + contentLength);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
@PostMapping(path = "/post", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> postUrlEncoded(HttpServletRequest request,
|
||||
@RequestBody(required = false) MultiValueMap form) throws IOException {
|
||||
HashMap<String, Object> ret = new HashMap<>();
|
||||
ret.put("headers", getHeaders(request));
|
||||
ret.put("form", form);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@PostMapping(path = "/post", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@PostMapping(path = "/localpost", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> post(HttpServletRequest request, @RequestBody(required = false) String body) {
|
||||
HashMap<String, Object> ret = new HashMap<>();
|
||||
ret.put("headers", getHeaders(request));
|
||||
|
||||
@@ -264,7 +264,7 @@ public class ExchangeResult {
|
||||
}
|
||||
|
||||
private String formatHeaders(HttpHeaders headers, String delimiter) {
|
||||
return headers.entrySet()
|
||||
return headers.headerSet()
|
||||
.stream()
|
||||
.map(entry -> entry.getKey() + ": " + entry.getValue())
|
||||
.collect(Collectors.joining(delimiter));
|
||||
|
||||
@@ -34,12 +34,16 @@ spring.cloud.gateway.mvc:
|
||||
- HttpbinUriResolver=
|
||||
- TokenRelay
|
||||
- AddRequestHeader=X-Test,listRoute2
|
||||
- Retry=3,SERVER_ERROR
|
||||
- id: listRoute3
|
||||
uri: lb://httpbin
|
||||
predicates:
|
||||
- Path=/anything/listRoute3
|
||||
- Path=/extra/anything/listRoute3
|
||||
- Header=MyHeaderName,MyHeader.*
|
||||
filters:
|
||||
- name: StripPrefix
|
||||
args:
|
||||
parts: 1
|
||||
- name: AddRequestHeader
|
||||
args:
|
||||
name: X-Test
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>4.1.6-SNAPSHOT</version>
|
||||
<version>4.1.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<artifactId>spring-cloud-gateway-server</artifactId>
|
||||
@@ -235,7 +235,7 @@
|
||||
<!-- Based on instructions here - https://kotlinlang.org/docs/reference/using-maven.html -->
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<version>1.9.23</version>
|
||||
<version>1.9.25</version>
|
||||
<configuration>
|
||||
<jvmTarget>17</jvmTarget>
|
||||
</configuration>
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
package org.springframework.cloud.gateway.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
@@ -354,10 +356,12 @@ public class GatewayAutoConfiguration {
|
||||
@ConditionalOnMissingBean(GrpcSslConfigurer.class)
|
||||
@ConditionalOnClass(name = "io.grpc.Channel")
|
||||
public GrpcSslConfigurer grpcSslConfigurer(HttpClientProperties properties)
|
||||
throws KeyStoreException, NoSuchAlgorithmException {
|
||||
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory
|
||||
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init(KeyStore.getInstance(KeyStore.getDefaultType()));
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keyStore.load(null);
|
||||
trustManagerFactory.init(keyStore);
|
||||
|
||||
return new GrpcSslConfigurer(properties.getSsl());
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ public class HttpClientProperties {
|
||||
|
||||
public static class Pool {
|
||||
|
||||
/** Type of pool for HttpClient to use, defaults to ELASTIC. */
|
||||
/** Type of pool for HttpClient to use (elastic, fixed or disabled). */
|
||||
private PoolType type = PoolType.ELASTIC;
|
||||
|
||||
/** The channel pool map name, defaults to proxy. */
|
||||
@@ -302,7 +302,9 @@ public class HttpClientProperties {
|
||||
|
||||
public static class Proxy {
|
||||
|
||||
/** proxyType for proxy configuration of Netty HttpClient. */
|
||||
/**
|
||||
* proxyType for proxy configuration of Netty HttpClient (http, socks4 or socks5).
|
||||
*/
|
||||
private ProxyProvider.Proxy type = ProxyProvider.Proxy.HTTP;
|
||||
|
||||
/** Hostname for proxy configuration of Netty HttpClient. */
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.discovery;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -170,7 +171,7 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc
|
||||
@Override
|
||||
public String getServiceId() {
|
||||
if (properties.isLowerCaseServiceId()) {
|
||||
return delegate.getServiceId().toLowerCase();
|
||||
return delegate.getServiceId().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return delegate.getServiceId();
|
||||
}
|
||||
|
||||
@@ -191,7 +191,8 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
|
||||
if (responseTimeout != null) {
|
||||
responseFlux = responseFlux
|
||||
.timeout(responseTimeout,
|
||||
Mono.error(new TimeoutException("Response took longer than timeout: " + responseTimeout)))
|
||||
Mono.defer(() -> Mono
|
||||
.error(new TimeoutException("Response took longer than timeout: " + responseTimeout))))
|
||||
.onErrorMap(TimeoutException.class,
|
||||
th -> new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, th.getMessage(), th));
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SignalType;
|
||||
import reactor.netty.Connection;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
@@ -98,8 +99,12 @@ public class NettyWriteResponseFilter implements GlobalFilter, Ordered {
|
||||
return (isStreamingMediaType(contentType)
|
||||
? response.writeAndFlushWith(body.map(Flux::just))
|
||||
: response.writeWith(body));
|
||||
})).doOnCancel(() -> cleanup(exchange))
|
||||
.doOnError(throwable -> cleanup(exchange));
|
||||
}))
|
||||
.doFinally(signalType -> {
|
||||
if (signalType == SignalType.CANCEL || signalType == SignalType.ON_ERROR) {
|
||||
cleanup(exchange);
|
||||
}
|
||||
});
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@@ -116,12 +121,12 @@ public class NettyWriteResponseFilter implements GlobalFilter, Ordered {
|
||||
byteBuf.release();
|
||||
return buffer;
|
||||
}
|
||||
throw new IllegalArgumentException("Unkown DataBufferFactory type " + bufferFactory.getClass());
|
||||
throw new IllegalArgumentException("Unknown DataBufferFactory type " + bufferFactory.getClass());
|
||||
}
|
||||
|
||||
private void cleanup(ServerWebExchange exchange) {
|
||||
Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);
|
||||
if (connection != null && connection.channel().isActive()) {
|
||||
if (connection != null) {
|
||||
connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,37 +16,17 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.buffer.PooledDataBuffer;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR;
|
||||
|
||||
public class RemoveCachedBodyFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private static final Log log = LogFactory.getLog(RemoveCachedBodyFilter.class);
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
return chain.filter(exchange).doFinally(s -> {
|
||||
Object attribute = exchange.getAttributes().remove(CACHED_REQUEST_BODY_ATTR);
|
||||
if (attribute != null && attribute instanceof PooledDataBuffer) {
|
||||
PooledDataBuffer dataBuffer = (PooledDataBuffer) attribute;
|
||||
if (dataBuffer.isAllocated()) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("releasing cached body in exchange attribute");
|
||||
}
|
||||
// ensure proper release
|
||||
while (!dataBuffer.release()) {
|
||||
// release() counts down until zero, will never be infinite loop
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return chain.filter(exchange).doFinally(s -> ServerWebExchangeUtils.clearCachedRequestBody(exchange));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -79,7 +80,7 @@ public class WebsocketRoutingFilter implements GlobalFilter, Ordered {
|
||||
|
||||
/* for testing */
|
||||
static String convertHttpToWs(String scheme) {
|
||||
scheme = scheme.toLowerCase();
|
||||
scheme = scheme.toLowerCase(Locale.ROOT);
|
||||
return "http".equals(scheme) ? "ws" : "https".equals(scheme) ? "wss" : scheme;
|
||||
}
|
||||
|
||||
@@ -142,8 +143,8 @@ public class WebsocketRoutingFilter implements GlobalFilter, Ordered {
|
||||
|
||||
headersFilters.add((headers, exchange) -> {
|
||||
HttpHeaders filtered = new HttpHeaders();
|
||||
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
|
||||
if (!entry.getKey().toLowerCase().startsWith("sec-websocket")) {
|
||||
for (Map.Entry<String, List<String>> entry : headers.headerSet()) {
|
||||
if (!entry.getKey().toLowerCase(Locale.ROOT).startsWith("sec-websocket")) {
|
||||
filtered.addAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
@@ -157,7 +158,7 @@ public class WebsocketRoutingFilter implements GlobalFilter, Ordered {
|
||||
static void changeSchemeIfIsWebSocketUpgrade(ServerWebExchange exchange) {
|
||||
// Check the Upgrade
|
||||
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
String scheme = requestUrl.getScheme().toLowerCase();
|
||||
String scheme = requestUrl.getScheme().toLowerCase(Locale.ROOT);
|
||||
String upgrade = exchange.getRequest().getHeaders().getUpgrade();
|
||||
// change the scheme if the socket client send a "http" or "https"
|
||||
if ("WebSocket".equalsIgnoreCase(upgrade) && ("http".equals(scheme) || "https".equals(scheme))) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import org.springframework.cloud.gateway.event.EnableBodyCachingEvent;
|
||||
import org.springframework.cloud.gateway.support.AbstractConfigurable;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
@@ -43,6 +44,13 @@ public abstract class AbstractGatewayFilterFactory<C> extends AbstractConfigurab
|
||||
return this.publisher;
|
||||
}
|
||||
|
||||
protected void enableBodyCaching(String routeId) {
|
||||
if (routeId != null && getPublisher() != null) {
|
||||
// send an event to enable caching
|
||||
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
|
||||
this.publisher = publisher;
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
|
||||
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
|
||||
@@ -57,14 +58,19 @@ public class RemoveRequestParameterGatewayFilterFactory
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(request.getQueryParams());
|
||||
queryParams.remove(config.getName());
|
||||
|
||||
URI newUri = UriComponentsBuilder.fromUri(request.getURI())
|
||||
.replaceQueryParams(unmodifiableMultiValueMap(queryParams))
|
||||
.build()
|
||||
.toUri();
|
||||
try {
|
||||
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
|
||||
URI newUri = UriComponentsBuilder.fromUri(request.getURI())
|
||||
.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams))
|
||||
.build(true)
|
||||
.toUri();
|
||||
|
||||
ServerHttpRequest updatedRequest = exchange.getRequest().mutate().uri(newUri).build();
|
||||
|
||||
return chain.filter(exchange.mutate().request(updatedRequest).build());
|
||||
ServerHttpRequest updatedRequest = exchange.getRequest().mutate().uri(newUri).build();
|
||||
return chain.filter(exchange.mutate().request(updatedRequest).build());
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("Invalid URI query: \"" + queryParams + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -69,7 +69,7 @@ public class RequestHeaderSizeGatewayFilterFactory
|
||||
HttpHeaders headers = request.getHeaders();
|
||||
HashMap<String, Long> longHeaders = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, List<String>> headerEntry : headers.entrySet()) {
|
||||
for (Map.Entry<String, List<String>> headerEntry : headers.headerSet()) {
|
||||
long headerSizeInBytes = 0L;
|
||||
headerSizeInBytes += headerEntry.getKey().getBytes().length;
|
||||
List<String> values = headerEntry.getValue();
|
||||
|
||||
@@ -35,7 +35,6 @@ import reactor.retry.RepeatContext;
|
||||
import reactor.retry.Retry;
|
||||
import reactor.retry.RetryContext;
|
||||
|
||||
import org.springframework.cloud.gateway.event.EnableBodyCachingEvent;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.support.HasRouteId;
|
||||
@@ -229,10 +228,7 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
|
||||
}
|
||||
|
||||
public GatewayFilter apply(String routeId, Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) {
|
||||
if (routeId != null && getPublisher() != null) {
|
||||
// send an event to enable caching
|
||||
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
|
||||
}
|
||||
enableBodyCaching(routeId);
|
||||
return (exchange, chain) -> {
|
||||
trace("Entering retry-filter");
|
||||
|
||||
|
||||
@@ -26,10 +26,14 @@ import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
|
||||
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
|
||||
|
||||
/**
|
||||
* @author Fredrich Ombico
|
||||
@@ -59,14 +63,25 @@ public class RewriteRequestParameterGatewayFilterFactory
|
||||
ServerHttpRequest req = exchange.getRequest();
|
||||
|
||||
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(req.getURI());
|
||||
if (req.getQueryParams().containsKey(config.getName())) {
|
||||
uriComponentsBuilder.replaceQueryParam(config.getName(), config.getReplacement());
|
||||
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(req.getQueryParams());
|
||||
if (queryParams.containsKey(config.getName())) {
|
||||
queryParams.remove(config.getName());
|
||||
queryParams.add(config.getName(), config.getReplacement());
|
||||
}
|
||||
|
||||
URI uri = uriComponentsBuilder.build().toUri();
|
||||
ServerHttpRequest request = req.mutate().uri(uri).build();
|
||||
try {
|
||||
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
|
||||
URI uri = uriComponentsBuilder.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams))
|
||||
.build(true)
|
||||
.toUri();
|
||||
|
||||
return chain.filter(exchange.mutate().request(request).build());
|
||||
ServerHttpRequest request = req.mutate().uri(uri).build();
|
||||
return chain.filter(exchange.mutate().request(request).build());
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("Invalid URI query: \"" + queryParams + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -136,7 +137,7 @@ public class SecureHeadersGatewayFilterFactory
|
||||
}
|
||||
|
||||
private boolean isEnabled(List<String> disabledHeaders, String header) {
|
||||
return !disabledHeaders.contains(header.toLowerCase());
|
||||
return !disabledHeaders.contains(header.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
|
||||
@@ -89,6 +89,9 @@ public abstract class SpringCloudCircuitBreakerFilterFactory
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(Config config) {
|
||||
if (config.getFallbackUri() != null) {
|
||||
enableBodyCaching(config.getRouteId());
|
||||
}
|
||||
ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(config.getId());
|
||||
Set<HttpStatus> statuses = config.getStatusCodes()
|
||||
.stream()
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCache
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties.RequestOptions;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.AfterCacheExchangeMutator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.RemoveHeadersAfterCacheExchangeMutator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetMaxAgeHeaderAfterCacheExchangeMutator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetResponseHeadersAfterCacheExchangeMutator;
|
||||
@@ -80,6 +81,7 @@ public class ResponseCacheManager {
|
||||
this.ignoreNoCacheUpdate = isSkipNoCacheUpdateActive(requestOptions);
|
||||
this.afterCacheExchangeMutators = List.of(new SetResponseHeadersAfterCacheExchangeMutator(),
|
||||
new SetStatusCodeAfterCacheExchangeMutator(),
|
||||
new RemoveHeadersAfterCacheExchangeMutator(HttpHeaders.PRAGMA, HttpHeaders.EXPIRES),
|
||||
new SetMaxAgeHeaderAfterCacheExchangeMutator(configuredTimeToLive, Clock.systemDefaultZone(),
|
||||
ignoreNoCacheUpdate),
|
||||
new SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator());
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.filter.factory.cache.postprocessor;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.CachedResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Removes one or more HTTP headers from response. Assumes it is run after
|
||||
* {@link SetResponseHeadersAfterCacheExchangeMutator}.
|
||||
*
|
||||
* @author Abel Salgado Romero
|
||||
*/
|
||||
public class RemoveHeadersAfterCacheExchangeMutator implements AfterCacheExchangeMutator {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(RemoveHeadersAfterCacheExchangeMutator.class);
|
||||
|
||||
private final String[] httpHeader;
|
||||
|
||||
public RemoveHeadersAfterCacheExchangeMutator(String... httpHeaders) {
|
||||
this.httpHeader = httpHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(ServerWebExchange exchange, CachedResponse cachedResponse) {
|
||||
for (String header : httpHeader) {
|
||||
var previousValue = exchange.getResponse().getHeaders().remove(header);
|
||||
if (previousValue != null) {
|
||||
LOGGER.debug("HTTP Header value found in response, removing HTTP header " + header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -96,7 +96,7 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
|
||||
HttpHeaders updated = new HttpHeaders();
|
||||
|
||||
// copy all headers except Forwarded
|
||||
for (Map.Entry<String, List<String>> entry : original.entrySet()) {
|
||||
for (Map.Entry<String, List<String>> entry : original.headerSet()) {
|
||||
if (!entry.getKey().equalsIgnoreCase(FORWARDED_HEADER)) {
|
||||
updated.addAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public class GRPCRequestHeadersFilter implements HttpHeadersFilter, Ordered {
|
||||
public HttpHeaders filter(HttpHeaders headers, ServerWebExchange exchange) {
|
||||
HttpHeaders updated = new HttpHeaders();
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
|
||||
for (Map.Entry<String, List<String>> entry : headers.headerSet()) {
|
||||
updated.addAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.filter.headers;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -73,8 +74,8 @@ public class RemoveHopByHopHeadersFilter implements HttpHeadersFilter, Ordered {
|
||||
Set<String> headersToRemove = new HashSet<>(headers);
|
||||
headersToRemove.addAll(connectionOptions);
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : originalHeaders.entrySet()) {
|
||||
if (!headersToRemove.contains(entry.getKey().toLowerCase())) {
|
||||
for (Map.Entry<String, List<String>> entry : originalHeaders.headerSet()) {
|
||||
if (!headersToRemove.contains(entry.getKey().toLowerCase(Locale.ROOT))) {
|
||||
filtered.addAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
|
||||
HttpHeaders original = input;
|
||||
HttpHeaders updated = new HttpHeaders();
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : original.entrySet()) {
|
||||
for (Map.Entry<String, List<String>> entry : original.headerSet()) {
|
||||
updated.addAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.cloud.gateway.config.GatewayProperties;
|
||||
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
|
||||
import org.springframework.cloud.gateway.route.Route;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
|
||||
@@ -99,6 +100,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
|
||||
})
|
||||
.switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
|
||||
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
|
||||
ServerWebExchangeUtils.clearCachedRequestBody(exchange);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.cloud.gateway.handler.predicate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -59,9 +58,7 @@ public class HeaderRoutePredicateFactory extends AbstractRoutePredicateFactory<H
|
||||
return new GatewayPredicate() {
|
||||
@Override
|
||||
public boolean test(ServerWebExchange exchange) {
|
||||
List<String> values = exchange.getRequest()
|
||||
.getHeaders()
|
||||
.getOrDefault(config.header, Collections.emptyList());
|
||||
List<String> values = exchange.getRequest().getHeaders().getValuesAsList(config.header);
|
||||
if (values.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.gateway.support;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -79,7 +80,7 @@ public final class NameUtils {
|
||||
matcher.appendReplacement(stringBuffer, matcher.group(1));
|
||||
}
|
||||
}
|
||||
return stringBuffer.toString().toLowerCase();
|
||||
return stringBuffer.toString().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static String removeGarbage(String s) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
@@ -40,6 +41,7 @@ import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBuffer;
|
||||
import org.springframework.core.io.buffer.NettyDataBuffer;
|
||||
import org.springframework.core.io.buffer.PooledDataBuffer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.AbstractServerHttpResponse;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
@@ -267,7 +269,7 @@ public final class ServerWebExchangeUtils {
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
// try the enum string
|
||||
httpStatus = HttpStatus.valueOf(statusString.toUpperCase());
|
||||
httpStatus = HttpStatus.valueOf(statusString.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
return httpStatus;
|
||||
}
|
||||
@@ -379,6 +381,27 @@ public final class ServerWebExchangeUtils {
|
||||
.flatMap(function);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear the request body in a ServerWebExchange attribute. The attribute is
|
||||
* {@link #CACHED_REQUEST_BODY_ATTR}.
|
||||
* @param exchange the available ServerWebExchange.
|
||||
*/
|
||||
public static void clearCachedRequestBody(ServerWebExchange exchange) {
|
||||
Object attribute = exchange.getAttributes().remove(CACHED_REQUEST_BODY_ATTR);
|
||||
if (attribute != null && attribute instanceof PooledDataBuffer) {
|
||||
PooledDataBuffer dataBuffer = (PooledDataBuffer) attribute;
|
||||
if (dataBuffer.isAllocated()) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("releasing cached body in exchange attribute");
|
||||
}
|
||||
// ensure proper release
|
||||
while (!dataBuffer.release()) {
|
||||
// release() counts down until zero, will never be infinite loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ServerHttpRequest decorate(ServerWebExchange exchange, DataBuffer dataBuffer,
|
||||
boolean cacheDecoratedRequest) {
|
||||
if (dataBuffer.readableByteCount() > 0) {
|
||||
|
||||
@@ -48,9 +48,9 @@ import static org.springframework.cloud.gateway.handler.predicate.RoutePredicate
|
||||
"spring.cloud.gateway.discovery.locator.lower-case-service-id=true"
|
||||
/*
|
||||
* "spring.cloud.gateway.discovery.locator.predicates[0].name=Path",
|
||||
* "spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]='/'+serviceId.toLowerCase()+'/**'",
|
||||
* "spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]='/'+serviceId.toLowerCase(Locale.ROOT)+'/**'",
|
||||
* "spring.cloud.gateway.discovery.locator.filters[0].name=RewritePath",
|
||||
* "spring.cloud.gateway.discovery.locator.filters[0].args[regexp]='/' + serviceId.toLowerCase() + '/(?<remaining>.*)'"
|
||||
* "spring.cloud.gateway.discovery.locator.filters[0].args[regexp]='/' + serviceId.toLowerCase(Locale.ROOT) + '/(?<remaining>.*)'"
|
||||
* ,
|
||||
* "spring.cloud.gateway.discovery.locator.filters[0].args[replacement]='/$\\\\{remaining}'",
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@ import static org.mockito.Mockito.when;
|
||||
/**
|
||||
* @author Thirunavukkarasu Ravichandran
|
||||
*/
|
||||
public class RemoveRequestParameterGatewayFilterFactoryTests {
|
||||
class RemoveRequestParameterGatewayFilterFactoryTests {
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
@@ -46,7 +46,7 @@ public class RemoveRequestParameterGatewayFilterFactoryTests {
|
||||
private ArgumentCaptor<ServerWebExchange> captor;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
void setUp() {
|
||||
filterChain = mock(GatewayFilterChain.class);
|
||||
captor = ArgumentCaptor.forClass(ServerWebExchange.class);
|
||||
when(filterChain.filter(captor.capture())).thenReturn(Mono.empty());
|
||||
@@ -54,7 +54,7 @@ public class RemoveRequestParameterGatewayFilterFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeRequestParameterFilterWorks() {
|
||||
void removeRequestParameterFilterWorks() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost")
|
||||
.queryParam("foo", singletonList("bar"))
|
||||
.build();
|
||||
@@ -70,7 +70,7 @@ public class RemoveRequestParameterGatewayFilterFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeRequestParameterFilterWorksWhenParamIsNotPresentInRequest() {
|
||||
void removeRequestParameterFilterWorksWhenParamIsNotPresentInRequest() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
NameConfig config = new NameConfig();
|
||||
@@ -84,7 +84,7 @@ public class RemoveRequestParameterGatewayFilterFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeRequestParameterFilterShouldOnlyRemoveSpecifiedParam() {
|
||||
void removeRequestParameterFilterShouldOnlyRemoveSpecifiedParam() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost")
|
||||
.queryParam("foo", "bar")
|
||||
.queryParam("abc", "xyz")
|
||||
@@ -102,7 +102,7 @@ public class RemoveRequestParameterGatewayFilterFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeRequestParameterFilterShouldHandleRemainingParamsWhichRequiringEncoding() {
|
||||
void removeRequestParameterFilterShouldHandleRemainingParamsWhichRequiringEncoding() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost")
|
||||
.queryParam("foo", "bar")
|
||||
.queryParam("aaa", "abc xyz")
|
||||
@@ -123,4 +123,40 @@ public class RemoveRequestParameterGatewayFilterFactoryTests {
|
||||
assertThat(actualRequest.getQueryParams()).containsEntry("ccc", singletonList(",xyz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeRequestParameterFilterShouldHandleEncodedParameterName() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost")
|
||||
.queryParam("foo", "bar")
|
||||
.queryParam("baz[]", "qux")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
NameConfig config = new NameConfig();
|
||||
config.setName("baz[]");
|
||||
GatewayFilter filter = new RemoveRequestParameterGatewayFilterFactory().apply(config);
|
||||
|
||||
filter.filter(exchange, filterChain);
|
||||
|
||||
ServerHttpRequest actualRequest = captor.getValue().getRequest();
|
||||
assertThat(actualRequest.getQueryParams()).doesNotContainKey("baz[]");
|
||||
assertThat(actualRequest.getQueryParams()).containsEntry("foo", singletonList("bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeRequestParameterFilterShouldMaintainEncodedParameters() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost")
|
||||
.queryParam("foo", "bar")
|
||||
.queryParam("baz[]", "qux")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
NameConfig config = new NameConfig();
|
||||
config.setName("foo");
|
||||
GatewayFilter filter = new RemoveRequestParameterGatewayFilterFactory().apply(config);
|
||||
|
||||
filter.filter(exchange, filterChain);
|
||||
|
||||
ServerHttpRequest actualRequest = captor.getValue().getRequest();
|
||||
assertThat(actualRequest.getQueryParams()).doesNotContainKey("foo");
|
||||
assertThat(actualRequest.getQueryParams()).containsEntry("baz[]", singletonList("qux"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -71,11 +71,23 @@ class RewriteRequestParameterGatewayFilterFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void rewriteRequestParameterFilterWorksWithSpecialCharacters() {
|
||||
void rewriteRequestParameterFilterWithSpecialCharactersInParameterValue() {
|
||||
testRewriteRequestParameterFilter("campaign", "black friday~(1.A-B_C!)", "campaign=old&color=green",
|
||||
Map.of("campaign", List.of("black friday~(1.A-B_C!)"), "color", List.of("green")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rewriteRequestParameterFilterWithSpecialCharactersInParameterName() {
|
||||
testRewriteRequestParameterFilter("campaign[]", "red", "campaign%5B%5D=blue&color=green",
|
||||
Map.of("campaign[]", List.of("red"), "color", List.of("green")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rewriteRequestParameterFilterKeepsOtherParamsEncoded() {
|
||||
testRewriteRequestParameterFilter("color", "white", "campaign%5B%5D=blue&color=green",
|
||||
Map.of("campaign[]", List.of("blue"), "color", List.of("white")));
|
||||
}
|
||||
|
||||
private void testRewriteRequestParameterFilter(String name, String replacement, String query,
|
||||
Map<String, List<String>> expectedQueryParams) {
|
||||
GatewayFilter filter = new RewriteRequestParameterGatewayFilterFactory()
|
||||
|
||||
@@ -21,7 +21,9 @@ import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
|
||||
|
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
|
||||
/**
|
||||
@@ -154,11 +156,11 @@ public abstract class SpringCloudCircuitBreakerFilterFactoryTests extends BaseWe
|
||||
.is5xxServerError()
|
||||
.expectBody()
|
||||
.jsonPath("$.status")
|
||||
.isEqualTo(504)
|
||||
.value(status -> assertThat(HttpStatus.valueOf((Integer) status).is5xxServerError()).isTrue())
|
||||
.jsonPath("$.message")
|
||||
.isNotEmpty()
|
||||
.jsonPath("$.error")
|
||||
.isEqualTo("Gateway Timeout");
|
||||
.isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -242,4 +244,11 @@ public abstract class SpringCloudCircuitBreakerFilterFactoryTests extends BaseWe
|
||||
.valueEquals(ROUTE_ID_HEADER, "circuitbreaker_resume_without_error");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterPostFallback() {
|
||||
testClient.post().uri("/post").body(BodyInserters.fromValue("hello"))
|
||||
.header("Host", "www.circuitbreakerfallbackpost.org").exchange().expectStatus()
|
||||
.isOk().expectBody().json("{\"body\":\"hello\"}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -68,6 +70,11 @@ public class SpringCloudCircuitBreakerTestConfig {
|
||||
return Collections.singletonMap("from", "circuitbreakerfallbackcontroller");
|
||||
}
|
||||
|
||||
@PostMapping("/circuitbreakerPostFallbackController")
|
||||
public Map<String, String> postFallbackController(@RequestBody String body) {
|
||||
return Collections.singletonMap("body", body);
|
||||
}
|
||||
|
||||
@GetMapping("/circuitbreakerUriFallbackController/**")
|
||||
public Map<String, String> uriFallbackcontroller(ServerWebExchange exchange, @RequestParam("a") String a) {
|
||||
return Collections.singletonMap("uri", exchange.getRequest().getURI().toString());
|
||||
|
||||
@@ -351,6 +351,34 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
|
||||
.jsonPath("$.headers." + CUSTOM_HEADER, "2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnPragmaHeaderInNonCachedAndCachedResponses() {
|
||||
shouldNotReturnHeader(HttpHeaders.PRAGMA);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnExpiresHeaderInNonCachedAndCachedResponses() {
|
||||
shouldNotReturnHeader(HttpHeaders.EXPIRES);
|
||||
}
|
||||
|
||||
private void shouldNotReturnHeader(String header) {
|
||||
String uri = "/" + UUID.randomUUID() + "/cache/headers";
|
||||
|
||||
testClient.get()
|
||||
.uri(uri)
|
||||
.header("Host", "www.localresponsecache.org")
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.doesNotExist(header);
|
||||
|
||||
testClient.get()
|
||||
.uri(uri)
|
||||
.header("Host", "www.localresponsecache.org")
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.doesNotExist(header);
|
||||
}
|
||||
|
||||
void assertNonVaryHeaderInContent(String uri, String varyHeader, String varyHeaderValue, String nonVaryHeader,
|
||||
String nonVaryHeaderValue, String expectedNonVaryResponse) {
|
||||
testClient.get()
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
@@ -123,6 +124,34 @@ public class LocalResponseCacheGlobalFilterTests {
|
||||
.isEqualTo("1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnPragmaHeaderInNonCachedAndCachedResponses() {
|
||||
shouldNotReturnHeader(HttpHeaders.PRAGMA);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnExpiresHeaderInNonCachedAndCachedResponses() {
|
||||
shouldNotReturnHeader(HttpHeaders.EXPIRES);
|
||||
}
|
||||
|
||||
private void shouldNotReturnHeader(String header) {
|
||||
String uri = "/" + UUID.randomUUID() + "/global-cache/headers";
|
||||
|
||||
testClient.get()
|
||||
.uri(uri)
|
||||
.header("Host", "www.localresponsecache.org")
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.doesNotExist(header);
|
||||
|
||||
testClient.get()
|
||||
.uri(uri)
|
||||
.header("Host", "www.localresponsecache.org")
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.doesNotExist(header);
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(DefaultTestConfig.class)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.filter.factory.cache.postprocessor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.CachedResponse;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.http.HttpHeaders.CACHE_CONTROL;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
|
||||
import static org.springframework.http.HttpHeaders.EXPIRES;
|
||||
import static org.springframework.http.HttpHeaders.PRAGMA;
|
||||
|
||||
/**
|
||||
* @author Abel Salgado Romero
|
||||
*/
|
||||
class RemoveHeaderAfterCacheExchangeMutatorTest {
|
||||
|
||||
private static final String HTTP_HEADER_TO_REMOVE = "X-To-Remove";
|
||||
|
||||
@Test
|
||||
void onlyHeaderToRemoveFromResponseIsRemoved() {
|
||||
final ServerWebExchange inputExchange = setupExchange(Map.of(HTTP_HEADER_TO_REMOVE, "A-Value"));
|
||||
|
||||
final var mutator = new RemoveHeadersAfterCacheExchangeMutator(HTTP_HEADER_TO_REMOVE);
|
||||
CachedResponse cachedResponse = new CachedResponse.Builder(HttpStatus.OK).build();
|
||||
|
||||
mutator.accept(inputExchange, cachedResponse);
|
||||
|
||||
assertThat(inputExchange.getResponse().getHeaders()).doesNotContainKey(HTTP_HEADER_TO_REMOVE)
|
||||
.containsEntry(CACHE_CONTROL, List.of("max-age=60"))
|
||||
.containsEntry(CONTENT_TYPE, List.of("application/octet-stream"))
|
||||
.hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleHeadersToRemoveFromResponseAreRemoved() {
|
||||
final Map<String, String> headers = Map.of(HTTP_HEADER_TO_REMOVE, "A-Value", PRAGMA, "void", EXPIRES, "0");
|
||||
final ServerWebExchange inputExchange = setupExchange(headers);
|
||||
|
||||
final var mutator = new RemoveHeadersAfterCacheExchangeMutator(HTTP_HEADER_TO_REMOVE, PRAGMA, EXPIRES);
|
||||
CachedResponse cachedResponse = new CachedResponse.Builder(HttpStatus.OK).build();
|
||||
|
||||
mutator.accept(inputExchange, cachedResponse);
|
||||
|
||||
assertThat(inputExchange.getResponse().getHeaders()).doesNotContainKey(HTTP_HEADER_TO_REMOVE)
|
||||
.doesNotContainKey(PRAGMA)
|
||||
.doesNotContainKey(EXPIRES)
|
||||
.containsEntry(CACHE_CONTROL, List.of("max-age=60"))
|
||||
.containsEntry(CONTENT_TYPE, List.of("application/octet-stream"))
|
||||
.hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void headersAreNotModifiedIfHeaderToRemoveIsEmpty() {
|
||||
final ServerWebExchange inputExchange = setupExchange(Map.of());
|
||||
|
||||
final var mutator = new RemoveHeadersAfterCacheExchangeMutator(HTTP_HEADER_TO_REMOVE);
|
||||
CachedResponse cachedResponse = new CachedResponse.Builder(HttpStatus.OK).build();
|
||||
|
||||
mutator.accept(inputExchange, cachedResponse);
|
||||
|
||||
assertThat(inputExchange.getResponse().getHeaders()).containsEntry(CACHE_CONTROL, List.of("max-age=60"))
|
||||
.containsEntry(CONTENT_TYPE, List.of("application/octet-stream"))
|
||||
.hasSize(2);
|
||||
}
|
||||
|
||||
private ServerWebExchange setupExchange(Map<String, String> headersToAdd) {
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.setCacheControl(CacheControl.maxAge(Duration.ofSeconds(60)));
|
||||
responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||
headersToAdd.forEach((k, v) -> responseHeaders.set(k, v));
|
||||
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("https://this").build();
|
||||
MockServerWebExchange inputExchange = MockServerWebExchange.from(httpRequest);
|
||||
MockServerHttpResponse httpResponse = inputExchange.getResponse();
|
||||
httpResponse.setStatusCode(HttpStatus.OK);
|
||||
httpResponse.getHeaders().putAll(responseHeaders);
|
||||
|
||||
return inputExchange;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory.rewrite;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -134,7 +136,7 @@ public class ModifyRequestBodyGatewayFilterFactoryTests extends BaseWebClientTes
|
||||
if (body == null) {
|
||||
return Mono.just("modifyrequest");
|
||||
}
|
||||
return Mono.just(body.toUpperCase());
|
||||
return Mono.just(body.toUpperCase(Locale.ROOT));
|
||||
}))
|
||||
.uri(uri))
|
||||
.route("test_modify_request_body_to_large",
|
||||
@@ -152,7 +154,7 @@ public class ModifyRequestBodyGatewayFilterFactoryTests extends BaseWebClientTes
|
||||
.filters(f -> f.modifyRequestBody(new ParameterizedTypeReference<String>() {
|
||||
}, new ParameterizedTypeReference<String>() {
|
||||
}, (swe, body) -> {
|
||||
return Mono.just(body.replaceAll(" ", "_").toUpperCase());
|
||||
return Mono.just(body.replaceAll(" ", "_").toUpperCase(Locale.ROOT));
|
||||
}))
|
||||
.uri(uri))
|
||||
.build();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user