-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Support 'by' parameter in Forwarded header (RFC 7239)
#4019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Support 'by' parameter in Forwarded header (RFC 7239)
#4019
Conversation
| public void setForwardedByEnabled(boolean forwardedByEnabled) { | ||
| this.forwardedByEnabled = forwardedByEnabled; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return new ToStringCreator(this).append("routes", routes) | ||
| .append("routesMap", routesMap) | ||
| .append("streamingMediaTypes", streamingMediaTypes) | ||
| .append("streamingBufferSize", streamingBufferSize) | ||
| .append("trustedProxies", trustedProxies) | ||
| .append("forwardedByEnabled", forwardedByEnabled) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added forwardedByEnabled to GatewayMvcProperties for yaml configuration.
| private void addForwardedByHeader(Forwarded forwarded, ServerRequest request) { | ||
| try { | ||
| int serverPort = request.servletRequest().getServerPort(); | ||
| addForwardedBy(forwarded, InetAddress.getLocalHost(), serverPort); | ||
| } | ||
| catch (UnknownHostException e) { | ||
| log.warn("Can not resolve host address, skipping Forwarded 'by' header", e); | ||
| } | ||
| } | ||
|
|
||
| private void addForwardedBy(Forwarded forwarded, InetAddress localAddress, int serverPort) { | ||
| if (localAddress != null) { | ||
| String byValue = localAddress.getHostAddress(); | ||
| if (localAddress instanceof Inet6Address) { | ||
| byValue = "[" + byValue + "]"; | ||
| } | ||
| if (serverPort > 0) { | ||
| byValue = byValue + ":" + serverPort; | ||
| } | ||
| forwarded.put("by", byValue); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conditionally adds the gateway’s current IP address to the Forwarded header.
The "by" parameter identifies the proxy receiving the request, as specified in RFC 7239. This is useful for debugging and tracing in proxy/gateway environments. Signed-off-by: raccoonback <[email protected]>
8458c93 to
3872cf0
Compare
| Forwarded forwarded = new Forwarded(); | ||
| if (host != null) { | ||
| Forwarded forwarded = new Forwarded().put("host", host).put("proto", uri.getScheme()); | ||
|
|
||
| request.remoteAddress().ifPresent(remoteAddress -> { | ||
| // If remoteAddress is unresolved, calling getHostAddress() would cause a | ||
| // NullPointerException. | ||
| String forValue; | ||
| if (remoteAddress.isUnresolved()) { | ||
| forValue = remoteAddress.getHostName(); | ||
| } | ||
| else { | ||
| InetAddress address = remoteAddress.getAddress(); | ||
| forValue = remoteAddress.getAddress().getHostAddress(); | ||
| if (address instanceof Inet6Address) { | ||
| forValue = "[" + forValue + "]"; | ||
| } | ||
| } | ||
| if (!trustedProxies.isTrusted(forValue)) { | ||
| // don't add for value | ||
| return; | ||
| } | ||
| int port = remoteAddress.getPort(); | ||
| if (port >= 0 && !forValue.endsWith(":" + port)) { | ||
| forValue = forValue + ":" + port; | ||
| forwarded.put("host", host); | ||
| } | ||
| forwarded.put("proto", uri.getScheme()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lines 175 to 178 in 9689187
| Forwarded forwarded = new Forwarded().put("proto", uri.getScheme()); | |
| if (host != null) { | |
| forwarded.put("host", host); | |
| } |
Change the behavior to add the Forwarded header only if the Host header exists, matching ForwardedHeadersFilter.
| * @author raccoonback | ||
| */ | ||
| @Disabled("FIXME: ") | ||
| public class ForwardedRequestHeadersFilterTests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enable ForwardedRequestHeadersFilterTests
Signed-off-by: raccoonback <[email protected]>
|
@ryanjbaxter @spencergibb |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for the by parameter in the RFC 7239 Forwarded header to Spring Cloud Gateway MVC, bringing it into feature parity with a previously unimplemented TODO. The by parameter identifies the proxy interface that received the request, which is useful for debugging and tracing in multi-hop proxy environments.
Key changes:
- Added optional
byparameter support inForwardedRequestHeadersFilter, derived from the server's local address and port - Introduced
spring.cloud.gateway.mvc.forwarded-by-enabledconfiguration property (defaults tofalsefor backward compatibility) - Refactored filter logic to construct Forwarded headers outside of the host null-check, aligning behavior with the WebFlux implementation
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
ForwardedRequestHeadersFilter.java |
Added forwardedByEnabled field, new constructor overload, and methods to populate the by parameter with server address/port; includes IPv6 bracket formatting and error handling |
GatewayMvcProperties.java |
Added forwardedByEnabled boolean property with getter/setter and updated toString() method |
GatewayServerMvcAutoConfiguration.java |
Updated filter bean creation to pass the forwardedByEnabled property value to the constructor |
ForwardedRequestHeadersFilterTests.java |
Removed @Disabled annotation, added tests for the by parameter in enabled/disabled states, including IPv6 and port handling scenarios |
httpheadersfilters.adoc |
Documented the new forwarded-by-enabled property with usage examples and RFC 7239 reference |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Filter that creates RFC 7239 compliant Forwarded headers to send to downstream | ||
| * services. | ||
| * | ||
| * <p> | ||
| * This filter adds Forwarded headers containing information about the original request, | ||
| * including the client address (for), request protocol (proto), host header (host), and | ||
| * optionally the proxy interface that received the request (by). | ||
| * | ||
| * <p> | ||
| * The filter validates that proxies are trusted using a regular expression pattern | ||
| * configured via {@code spring.cloud.gateway.server.webmvc.trusted-proxies}. | ||
| * | ||
| * @author raccoonback | ||
| * @see <a href="https://tools.ietf.org/html/rfc7239">RFC 7239: Forwarded HTTP | ||
| * Extension</a> | ||
| */ |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class-level documentation is missing the original author credit. The existing code already had an author (Spencer Gibb), and the new documentation should include both the original author and the new contributor. This is important for proper attribution and maintaining the project's history.
.../java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilter.java
Outdated
Show resolved
Hide resolved
|
|
||
| [source] | ||
| ---- | ||
| Forwarded: for="192.0.2.60:47011";proto=https;by="198.51.100.17:8080" |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example header in the documentation is incomplete and doesn't align with the RFC 7239 format. The example shows for="192.0.2.60:47011";proto=https;by="198.51.100.17:8080" but is missing the host parameter that the filter also includes. Consider updating the example to show a complete header that reflects the actual output of the filter, including the host parameter.
| Forwarded: for="192.0.2.60:47011";proto=https;by="198.51.100.17:8080" | |
| Forwarded: for="192.0.2.60:47011";proto=https;host="example.com:443";by="198.51.100.17:8080" |
| addForwardedByHeader(forwarded, request); | ||
| } | ||
|
|
||
| updated.add(FORWARDED_HEADER, forwarded.toHeaderValue()); |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refactored code introduces a behavioral change. Previously, the Forwarded header was only added when the host header was present (the entire Forwarded construction was inside if (host != null)). Now, the Forwarded header is always added at line 223, even when host is null, meaning a Forwarded header with only proto and potentially by parameters will be sent downstream. While this aligns with the WebFlux implementation's behavior, it's a breaking change for existing MVC users who may rely on the absence of the Forwarded header when no host is present. Consider documenting this behavioral change in the PR description or release notes.
| updated.add(FORWARDED_HEADER, forwarded.toHeaderValue()); | |
| if (host != null) { | |
| updated.add(FORWARDED_HEADER, forwarded.toHeaderValue()); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ryanjbaxter
According to RFC 7239, all parameters (for, by, host, proto) in the Forwarded header are optional.
The specification does not mandate that the host parameter must be present. Therefore, sending a Forwarded header with only proto, for, and by parameters (without host) is valid per
the RFC standard.
However, I understand the concern about backward compatibility. If maintaining the previous behavior (not sending the Forwarded header when host is absent) is important for existing users, I can wrap the header addition in a host null check.
How should we decide whether to prioritize RFC 7239 compliance or backward compatibility in this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this aligns the MVC behavior with the WebFlux behavior I am inclined to change the behavior so we are both consistent and align with the RFC.
@spencergibb thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ryanjbaxter agree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@spencergibb Please check it 😀
.../java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilter.java
Show resolved
Hide resolved
.../java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilter.java
Outdated
Show resolved
Hide resolved
Signed-off-by: raccoonback <[email protected]>
Delegate to the new constructor with forwardedByEnabled parameter instead of duplicating initialization. Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
|
@ryanjbaxter |
Motivation
The
byparameter of theForwardedheader identifies the proxy that received the request, as defined in RFC 7239.This information is useful for debugging, tracing proxy chains, and improving observability in gateway environments.
In Spring Cloud Gateway MVC,
ForwardedRequestHeadersFilterpreviously supported only thefor,proto, andhostparameters, while support forbywas left as a TODO.Key changed
Added optional support for the
byparameter in theForwardedheader.The
byvalue is derived from the local server address and port.The feature is disabled by default to avoid any behavioral changes for existing users.
Configuration
A new configuration property has been introduced:
spring.cloud.gateway.mvc.forwarded-by-enabled=trueWhen enabled, the
byparameter is added to the generatedForwardedheader.