Refine Forwarded headers configuration for Reactor servers

This commit introduces new properties for configuring the HTTP headers
that should be used for forward support.
The new `server.netty.forwarded-headers.header-format` property allows
"standard" or "x_forwarded" to select "Forwarded" or "X-Forwarded-*"
headers.

This aligns the behavior of Netty server with the "FRAMEWORK" strategy.

Closes gh-51031
This commit is contained in:
Brian Clozel
2026-07-31 14:10:21 +02:00
parent 607fc5cd54
commit 66a093dbfa
7 changed files with 156 additions and 6 deletions
@@ -59,6 +59,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
private boolean useForwardHeaders;
private boolean useRfcForwardHeader;
private @Nullable ReactorResourceFactory resourceFactory;
public NettyReactiveWebServerFactory() {
@@ -139,6 +141,17 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
this.useForwardHeaders = useForwardHeaders;
}
/**
* Set if the RFC "Forwarded" header should be processed.
* <p>
* {@link #setUseForwardHeaders(boolean)} will take precedence if enabled.
* @param useRfcForwardHeader if the "Forwarded" header should be used
* @since 4.2.0
*/
public void setUseRfcForwardHeader(boolean useRfcForwardHeader) {
this.useRfcForwardHeader = useRfcForwardHeader;
}
/**
* Set the {@link ReactorResourceFactory} to get the shared resources from.
* @param resourceFactory the server resources
@@ -157,10 +170,20 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(getCompression());
server = compressionCustomizer.apply(server);
}
server = server.protocol(listProtocols()).forwarded(this.useForwardHeaders);
server = configureForwardedHeaders(server.protocol(listProtocols()));
return applyCustomizers(server);
}
private HttpServer configureForwardedHeaders(HttpServer server) {
if (this.useForwardHeaders) {
return server.forwarded(false, false);
}
if (this.useRfcForwardHeader) {
return server.forwarded(true, false);
}
return server.noForwarded();
}
private HttpServer customizeSslConfiguration(HttpServer httpServer, Ssl ssl) {
SslServerCustomizer customizer = new SslServerCustomizer(getHttp2(), ssl.getClientAuth(), getSslBundle(),
getServerNameSslBundles());
@@ -59,7 +59,9 @@ public class NettyReactiveWebServerFactoryCustomizer
@Override
public void customize(NettyReactiveWebServerFactory factory) {
factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders());
if (getOrDeduceUseForwardHeaders()) {
configureForwardedSupport(factory);
}
PropertyMapper map = PropertyMapper.get();
map.from(this.nettyProperties::getConnectionTimeout)
.to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout));
@@ -81,6 +83,13 @@ public class NettyReactiveWebServerFactoryCustomizer
return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
}
private void configureForwardedSupport(NettyReactiveWebServerFactory factory) {
switch (this.nettyProperties.getForwardedHeaders().getHeaderFormat()) {
case X_FORWARDED -> factory.setUseForwardHeaders(true);
case STANDARD -> factory.setUseRfcForwardHeader(true);
}
}
private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) {
factory.addServerCustomizers((httpServer) -> httpServer.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
(int) connectionTimeout.toMillis()));
@@ -90,6 +90,11 @@ public class NettyServerProperties {
*/
private @Nullable Duration idleTimeout;
/**
* Forwarded headers configuration.
*/
private final Forwardedheaders forwardedHeaders = new Forwardedheaders();
public @Nullable Duration getConnectionTimeout() {
return this.connectionTimeout;
}
@@ -146,4 +151,45 @@ public class NettyServerProperties {
this.idleTimeout = idleTimeout;
}
public Forwardedheaders getForwardedHeaders() {
return this.forwardedHeaders;
}
/**
* Forwarded headers.
*/
public static class Forwardedheaders {
/**
* Format of the forwarded headers to support.
*/
private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED;
public HeaderFormat getHeaderFormat() {
return this.headerFormat;
}
public void setHeaderFormat(HeaderFormat headerFormat) {
this.headerFormat = headerFormat;
}
/**
* Formats of forwarded headers supported by {@link Forwardedheaders}.
*/
public enum HeaderFormat {
/**
* Use the standard "Forwarded" header, as defined by RFC 7239.
*/
STANDARD,
/**
* Use the non-standard "X-Forwarded-*" headers.
*/
X_FORWARDED
}
}
}
@@ -146,6 +146,22 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
assertForwardHeaderIsUsed(factory);
}
@Test
void useRfcForwardHeader() {
NettyReactiveWebServerFactory factory = getFactory();
factory.setUseRfcForwardHeader(true);
assertRfcForwardHeaderIsUsed(factory);
}
@Test
void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() {
NettyReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
factory.setUseRfcForwardHeader(true);
assertForwardHeaderIsUsed(factory);
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory));
}
@Test
@WithPackageResources("test.jks")
void whenSslIsConfiguredWithAValidAliasARequestSucceeds() {
@@ -39,6 +39,7 @@ import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -85,7 +86,8 @@ class NettyReactiveWebServerFactoryCustomizerTests {
void defaultUseForwardHeaders() {
NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setUseForwardHeaders(false);
then(factory).should(never()).setUseForwardHeaders(anyBoolean());
then(factory).should(never()).setUseRfcForwardHeader(anyBoolean());
}
@Test
@@ -102,7 +104,30 @@ class NettyReactiveWebServerFactoryCustomizerTests {
this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE);
NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setUseForwardHeaders(false);
then(factory).should(never()).setUseForwardHeaders(anyBoolean());
then(factory).should(never()).setUseRfcForwardHeader(anyBoolean());
}
@Test
void forwardedHeadersFormatXForwardedConfiguresUseForwardHeaders() {
this.environment.setProperty("DYNO", "-");
this.nettyProperties.getForwardedHeaders()
.setHeaderFormat(NettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED);
NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setUseForwardHeaders(true);
then(factory).should(never()).setUseRfcForwardHeader(anyBoolean());
}
@Test
void forwardedHeadersFormatStandardConfiguresUseRfcForwardHeader() {
this.environment.setProperty("DYNO", "-");
this.nettyProperties.getForwardedHeaders()
.setHeaderFormat(NettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD);
NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setUseRfcForwardHeader(true);
then(factory).should(never()).setUseForwardHeaders(anyBoolean());
}
@Test
@@ -73,6 +73,19 @@ class NettyServerPropertiesTests {
.isEqualTo(HttpDecoderSpec.DEFAULT_INITIAL_BUFFER_SIZE);
}
@Test
void forwardedHeadersDefaultToXForwarded() {
assertThat(this.properties.getForwardedHeaders().getHeaderFormat())
.isEqualTo(NettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED);
}
@Test
void testCustomizeNettyForwardedHeadersFormat() {
bind("server.netty.forwarded-headers.header-format", "standard");
assertThat(this.properties.getForwardedHeaders().getHeaderFormat())
.isEqualTo(NettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD);
}
private void bind(String name, String value) {
bind(Collections.singletonMap(name, value));
}