Refine Forwarded headers configuration for Jetty servers

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

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

Closes gh-51148
This commit is contained in:
Brian Clozel
2026-07-30 15:12:48 +02:00
parent 3407166818
commit 941045a2f4
15 changed files with 289 additions and 7 deletions
@@ -517,8 +517,8 @@ If your web server of choice supports the set of HTTP headers you need, setting
| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.tomcat[]
| Jetty
| `"X-Forwarded-*"`
|
| `"X-Forwarded-*"`, `"Forwarded"`
| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.jetty[]
| Reactor Netty
| `"X-Forwarded-*"`
@@ -569,6 +569,24 @@ You can take complete control of the configuration of Tomcat's javadoc:org.apach
[[howto.webserver.use-behind-a-proxy-server.jetty]]
=== Customize Jetty's Proxy Configuration
If you use Jetty, you can additionally configure the type of the headers used to carry "`forwarded`" information.
By default, the `"X-Forwarded-*"` format is used, but you can opt into the standard RFC variant:
[configprops,yaml]
----
server:
jetty:
forwarded-headers:
header-format: "standard"
----
For more specific options, you can switch off the forwarded configuration (using `server.forward-headers-strategy=NONE`) and directly use Jetty's javadoc:org.eclipse.jetty.server.ForwardedRequestCustomizer[] to change the HTTP configuration.
[[howto.webserver.enable-multiple-connectors]]
== Enable Multiple Connectors
@@ -56,6 +56,15 @@ public interface ConfigurableJettyWebServerFactory extends ConfigurableWebServer
*/
void setUseForwardHeaders(boolean useForwardHeaders);
/**
* Set if the RFC forwarded header should be processed.
* <p>
* {@link #setUseForwardHeaders(boolean)} will take precedence if enabled.
* @param useRfcForwardHeader if forwarded header should be used
* @since 4.2.0
*/
void setUseRfcForwardHeader(boolean useRfcForwardHeader);
/**
* Add {@link JettyServerCustomizer}s that will be applied to the {@link Server}
* before it is started.
@@ -30,9 +30,36 @@ import org.eclipse.jetty.server.Server;
*/
public class ForwardHeadersCustomizer implements JettyServerCustomizer {
private final boolean useXForwarded;
/**
* Create a new customizer instance.
* @param useXForwarded "X-Forwarded-*" headers are used if {@code true}, otherwise
* standard "Forwarded" are used instead.
* @since 4.2.0
*/
public ForwardHeadersCustomizer(boolean useXForwarded) {
this.useXForwarded = useXForwarded;
}
/**
* Create a new customizer instance with "X-Forwarded-*" support.
*/
public ForwardHeadersCustomizer() {
this(true);
}
@Override
public void customize(Server server) {
ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer();
if (this.useXForwarded) {
// disable "Forwarded" support
customizer.setForwardedHeader(null);
}
else {
// disable "X-Forwarded-*" support
customizer.setForwardedOnly(true);
}
for (Connector connector : server.getConnectors()) {
for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
if (connectionFactory instanceof HttpConfiguration.ConnectionFactory jettyConnectionFactory) {
@@ -64,6 +64,8 @@ public class JettyWebServerFactory extends AbstractConfigurableWebServerFactory
private boolean useForwardHeaders;
private boolean useRfcForwardHeader;
private Set<JettyServerCustomizer> jettyServerCustomizers = new LinkedHashSet<>();
private int maxConnections = -1;
@@ -180,6 +182,15 @@ public class JettyWebServerFactory extends AbstractConfigurableWebServerFactory
this.useForwardHeaders = useForwardHeaders;
}
public boolean isUseRfcForwardHeader() {
return this.useRfcForwardHeader;
}
@Override
public void setUseRfcForwardHeader(boolean useRfcForwardHeader) {
this.useRfcForwardHeader = useRfcForwardHeader;
}
protected AbstractConnector createConnector(InetSocketAddress address, Server server) {
return this.createConnector(address, server, null, null, null);
}
@@ -86,6 +86,11 @@ public class JettyServerProperties {
*/
private final Accesslog accesslog = new Accesslog();
/**
* Forwarded headers configuration.
*/
private final Forwardedheaders forwardedHeaders = new Forwardedheaders();
/**
* Thread related configuration.
*/
@@ -95,6 +100,10 @@ public class JettyServerProperties {
return this.accesslog;
}
public Forwardedheaders getForwardedHeaders() {
return this.forwardedHeaders;
}
public Threads getThreads() {
return this.threads;
}
@@ -268,6 +277,43 @@ public class JettyServerProperties {
}
/**
* 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
}
}
/**
* Jetty thread properties.
*/
@@ -79,7 +79,9 @@ public class JettyWebServerFactoryCustomizer
@Override
public void customize(ConfigurableJettyWebServerFactory factory) {
factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders());
if (getOrDeduceUseForwardHeaders()) {
configureForwardedSupport(factory);
}
JettyServerProperties.Threads threadProperties = this.jettyProperties.getThreads();
factory.setThreadPool(JettyThreadPool.create(this.jettyProperties.getThreads()));
PropertyMapper map = PropertyMapper.get();
@@ -121,6 +123,13 @@ public class JettyWebServerFactoryCustomizer
return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
}
private void configureForwardedSupport(ConfigurableJettyWebServerFactory factory) {
switch (this.jettyProperties.getForwardedHeaders().getHeaderFormat()) {
case X_FORWARDED -> factory.setUseForwardHeaders(true);
case STANDARD -> factory.setUseRfcForwardHeader(true);
}
}
private <T> Consumer<T> customizeHttpConfigurations(ConfigurableJettyWebServerFactory factory,
BiConsumer<HttpConfiguration, T> action) {
return customizeConnectionFactories(factory, HttpConfiguration.ConnectionFactory.class,
@@ -118,7 +118,10 @@ public class JettyReactiveWebServerFactory extends JettyWebServerFactory
customizer.customize(server);
}
if (this.isUseForwardHeaders()) {
new ForwardHeadersCustomizer().customize(server);
new ForwardHeadersCustomizer(true).customize(server);
}
else if (this.isUseRfcForwardHeader()) {
new ForwardHeadersCustomizer(false).customize(server);
}
if (getShutdown() == Shutdown.GRACEFUL) {
GracefulHandler gracefulHandler = new GracefulHandler();
@@ -177,7 +177,10 @@ public class JettyServletWebServerFactory extends JettyWebServerFactory
customizer.customize(server);
}
if (this.isUseForwardHeaders()) {
new ForwardHeadersCustomizer().customize(server);
new ForwardHeadersCustomizer(true).customize(server);
}
else if (this.isUseRfcForwardHeader()) {
new ForwardHeadersCustomizer(false).customize(server);
}
if (getShutdown() == Shutdown.GRACEFUL) {
GracefulHandler gracefulHandler = new GracefulHandler();
@@ -0,0 +1,66 @@
/*
* Copyright 2012-present 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.boot.jetty;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Server;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ForwardHeadersCustomizer}.
*
* @author Brian Clozel
*/
class ForwardHeadersCustomizerTests {
@Test
void defaultConstructorUsesXForwardedHeaders() {
ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer());
assertThat(customizer.getForwardedHeader()).isNull();
assertThat(customizer.getForwardedForHeader()).isNotNull();
}
@Test
void useXForwardedTrueEnablesXForwardedAndDisablesForwarded() {
ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer(true));
assertThat(customizer.getForwardedHeader()).isNull();
assertThat(customizer.getForwardedForHeader()).isNotNull();
}
@Test
void useXForwardedFalseEnablesForwardedAndDisablesXForwarded() {
ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer(false));
assertThat(customizer.getForwardedHeader()).isNotNull();
assertThat(customizer.getForwardedForHeader()).isNull();
}
private ForwardedRequestCustomizer customize(ForwardHeadersCustomizer customizer) {
Server server = new Server(0);
customizer.customize(server);
HttpConfiguration httpConfiguration = ((HttpConfiguration.ConnectionFactory) server.getConnectors()[0]
.getConnectionFactories()
.stream()
.filter(HttpConfiguration.ConnectionFactory.class::isInstance)
.findFirst()
.orElseThrow()).getHttpConfiguration();
return httpConfiguration.getCustomizer(ForwardedRequestCustomizer.class);
}
}
@@ -100,6 +100,19 @@ class JettyServerPropertiesTests {
assertThat(this.properties.getAccesslog().getIgnorePaths()).containsExactly("/a/path", "/b/path");
}
@Test
void forwardedHeadersDefaultToXForwarded() {
assertThat(this.properties.getForwardedHeaders().getHeaderFormat())
.isEqualTo(JettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED);
}
@Test
void testCustomizeJettyForwardedHeadersFormat() {
bind("server.jetty.forwarded-headers.header-format", "standard");
assertThat(this.properties.getForwardedHeaders().getHeaderFormat())
.isEqualTo(JettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD);
}
@Test
void jettyThreadPoolPropertyDefaultsShouldMatchServerDefault() {
JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0);
@@ -54,8 +54,10 @@ import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link JettyWebServerFactoryCustomizer}.
@@ -94,7 +96,8 @@ class JettyWebServerFactoryCustomizerTests {
void defaultUseForwardHeaders() {
ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setUseForwardHeaders(false);
then(factory).should(never()).setUseForwardHeaders(anyBoolean());
then(factory).should(never()).setUseRfcForwardHeader(anyBoolean());
}
@Test
@@ -111,7 +114,28 @@ class JettyWebServerFactoryCustomizerTests {
this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE);
ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.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", "-");
bind("server.jetty.forwarded-headers.header-format=x_forwarded");
ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setUseForwardHeaders(true);
then(factory).should(never()).setUseRfcForwardHeader(anyBoolean());
}
@Test
void forwardedHeadersFormatStandardConfiguresUseRfcForwardHeader() {
this.environment.setProperty("DYNO", "-");
bind("server.jetty.forwarded-headers.header-format=standard");
ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setUseRfcForwardHeader(true);
then(factory).should(never()).setUseForwardHeaders(anyBoolean());
}
@Test
@@ -43,6 +43,7 @@ import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
@@ -121,6 +122,22 @@ class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
assertForwardHeaderIsUsed(factory);
}
@Test
void useRfcForwardHeader() {
JettyReactiveWebServerFactory factory = getFactory();
factory.setUseRfcForwardHeader(true);
assertRfcForwardHeaderIsUsed(factory);
}
@Test
void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() {
JettyReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
factory.setUseRfcForwardHeader(true);
assertForwardHeaderIsUsed(factory);
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory));
}
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() {
JettyReactiveWebServerFactory factory = getFactory();
@@ -442,6 +442,22 @@ class JettyServletWebServerFactoryTests extends AbstractServletWebServerFactoryT
assertForwardHeaderIsUsed(factory);
}
@Test
void useRfcForwardHeader() throws Exception {
JettyServletWebServerFactory factory = getFactory();
factory.setUseRfcForwardHeader(true);
assertRfcForwardHeaderIsUsed(factory);
}
@Test
void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() throws Exception {
JettyServletWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
factory.setUseRfcForwardHeader(true);
assertForwardHeaderIsUsed(factory);
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory));
}
@Test
void defaultThreadPool() {
JettyServletWebServerFactory factory = getFactory();
@@ -677,6 +677,18 @@ public abstract class AbstractReactiveWebServerFactoryTests {
assertThat(body).isEqualTo("https");
}
protected void assertRfcForwardHeaderIsUsed(ConfigurableReactiveWebServerFactory factory) {
this.webServer = factory.getWebServer(new XForwardedHandler());
this.webServer.start();
String body = getWebClient(this.webServer.getPort()).build()
.get()
.header("Forwarded", "proto=https")
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofSeconds(30));
assertThat(body).isEqualTo("https");
}
private <T> T doWithRetry(Callable<T> action) throws Exception {
Exception lastFailure = null;
for (int i = 0; i < 10; i++) {
@@ -1617,6 +1617,14 @@ public abstract class AbstractServletWebServerFactoryTests {
.contains("remoteaddr=140.211.11.130");
}
protected void assertRfcForwardHeaderIsUsed(ServletWebServerFactory factory)
throws IOException, URISyntaxException {
this.webServer = factory.getWebServer(new ServletRegistrationBean<>(new ExampleServlet(true, false), "/hello"));
this.webServer.start();
assertThat(getResponse(getLocalUrl("/hello"), "Forwarded:for=140.211.11.130"))
.contains("remoteaddr=140.211.11.130");
}
protected abstract ConfigurableServletWebServerFactory getFactory();
protected abstract org.apache.jasper.servlet.@Nullable JspServlet getJspServlet() throws Exception;