Skip to content

Conversation

@raccoonback
Copy link
Contributor

Motivation

The by parameter of the Forwarded header 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, ForwardedRequestHeadersFilter previously supported only the for, proto, and host parameters, while support for by was left as a TODO.

Key changed

  • Added optional support for the by parameter in the Forwarded header.

  • The by value is derived from the local server address and port.

    • IPv6 addresses are formatted according to RFC 7239 (wrapped in square brackets).
  • 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=true

When enabled, the by parameter is added to the generated Forwarded header.

Comment on lines +135 to +146
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)
Copy link
Contributor Author

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.

Comment on lines 194 to 218
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);
}
}
Copy link
Contributor Author

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]>
@raccoonback raccoonback force-pushed the support-forwarded-by-header branch from 8458c93 to 3872cf0 Compare December 13, 2025 02:53
Comment on lines +157 to +161
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());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enable ForwardedRequestHeadersFilterTests

@raccoonback
Copy link
Contributor Author

@ryanjbaxter @spencergibb
Hello.
I'd appreciate it if you could review this PR.

Copy link

Copilot AI left a 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 by parameter support in ForwardedRequestHeadersFilter, derived from the server's local address and port
  • Introduced spring.cloud.gateway.mvc.forwarded-by-enabled configuration property (defaults to false for 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.

Comment on lines +42 to +58
/**
* 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>
*/
Copy link

Copilot AI Dec 22, 2025

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.

Copilot uses AI. Check for mistakes.

[source]
----
Forwarded: for="192.0.2.60:47011";proto=https;by="198.51.100.17:8080"
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
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"

Copilot uses AI. Check for mistakes.
addForwardedByHeader(forwarded, request);
}

updated.add(FORWARDED_HEADER, forwarded.toHeaderValue());
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
updated.add(FORWARDED_HEADER, forwarded.toHeaderValue());
if (host != null) {
updated.add(FORWARDED_HEADER, forwarded.toHeaderValue());
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanjbaxter agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spencergibb Please check it 😀

Delegate to the new constructor with forwardedByEnabled parameter instead of duplicating initialization.

Signed-off-by: raccoonback <[email protected]>
@raccoonback
Copy link
Contributor Author

@ryanjbaxter
I’ve incorporated all the suggested review comments.
I’d appreciate it if you could take another look. 😀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants