Default use-relative-redirects to true

Spring Boot unconditionally set useRelativeRedirects on the Tomcat
Context, defaulting it to false. That overrode Tomcat's own default and
forced absolute Location headers on every sendRedirect.

Keep server.tomcat.use-relative-redirects a simple boolean, but default
it to true so that relative Location headers are used out of the box.
The property can still be set to false to opt back into absolute
redirects.

Smoke tests that asserted a port-qualified absolute Location are updated
to the relative form, and the proxy tip in the reference documentation is
qualified since the context root redirect no longer carries a scheme.

Signed-off-by: Tiziano Basile <tiz.basile@gmail.com>

See gh-51173
This commit is contained in:
Tiziano Basile
2026-09-07 08:50:09 +01:00
committed by Andy Wilkinson
parent a07c43466b
commit f119e58767
14 changed files with 33 additions and 28 deletions
@@ -562,8 +562,9 @@ server:
NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but do not do so in production).
TIP: If you are using Tomcat and terminating SSL at the proxy, configprop:server.tomcat.redirect-context-root[] should be set to `false`.
TIP: If you are using Tomcat, terminating SSL at the proxy, and have set configprop:server.tomcat.use-relative-redirects[] to `false`, then configprop:server.tomcat.redirect-context-root[] should also be set to `false`.
This allows the `X-Forwarded-Proto` header to be honored before any redirects are performed.
When relative redirects are in use, which is Tomcat's default, the context root redirect carries no scheme so there is nothing for the header to correct.
You can take complete control of the configuration of Tomcat's javadoc:org.apache.catalina.valves.RemoteIpValve[] by switching the automatic one off (to do so, set `server.forward-headers-strategy=NONE`) and adding a new valve instance using a javadoc:org.springframework.boot.web.server.WebServerFactoryCustomizer[] bean.
@@ -56,6 +56,7 @@ import org.springframework.util.unit.DataSize;
* @author Florian Storz
* @author Michael Weidmann
* @author Lasse Wulff
* @author Tiziano Basile
* @since 4.0.0
*/
@ConfigurationProperties("server.tomcat")
@@ -103,9 +104,9 @@ public class TomcatServerProperties {
/**
* Whether HTTP 1.1 and later location headers generated by a call to sendRedirect
* will use relative or absolute redirects.
* will use relative or absolute redirects. Has no effect on a reactive web server.
*/
private boolean useRelativeRedirects;
private boolean useRelativeRedirects = true;
/**
* Character encoding to use to decode the URI.
@@ -29,6 +29,7 @@ import org.springframework.util.ObjectUtils;
*
* @author Brian Clozel
* @author Phillip Webb
* @author Tiziano Basile
*/
class TomcatServletWebServerFactoryCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>, Ordered {
@@ -45,6 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link TomcatServerProperties}.
*
* @author Andy Wilkinson
* @author Tiziano Basile
*/
class TomcatServerPropertiesTests {
@@ -235,8 +236,8 @@ class TomcatServerPropertiesTests {
}
@Test
void tomcatUseRelativeRedirectsDefaultsToFalse() {
assertThat(this.properties.isUseRelativeRedirects()).isFalse();
void tomcatUseRelativeRedirectsDefaultsToTrue() {
assertThat(this.properties.isUseRelativeRedirects()).isTrue();
}
@Test
@@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link TomcatServletWebServerFactoryCustomizer}.
*
* @author Phillip Webb
* @author Tiziano Basile
*/
class TomcatServletWebServerFactoryCustomizerTests {
@@ -80,14 +81,22 @@ class TomcatServletWebServerFactoryCustomizerTests {
}
@Test
void useRelativeRedirectsCanBeConfigured() {
bind("server.tomcat.use-relative-redirects=true");
void useRelativeRedirectsDefaultsToTrue() {
assertThat(this.tomcatProperties.isUseRelativeRedirects()).isTrue();
TomcatWebServer server = customizeAndGetServer();
Context context = (Context) server.getTomcat().getHost().findChildren()[0];
assertThat(context.getUseRelativeRedirects()).isTrue();
}
@Test
void useRelativeRedirectsCanBeDisabled() {
bind("server.tomcat.use-relative-redirects=false");
assertThat(this.tomcatProperties.isUseRelativeRedirects()).isFalse();
TomcatWebServer server = customizeAndGetServer();
Context context = (Context) server.getTomcat().getHost().findChildren()[0];
assertThat(context.getUseRelativeRedirects()).isFalse();
}
private void bind(String... inlinedProperties) {
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, inlinedProperties);
new Binder(ConfigurationPropertySources.get(this.environment)).bind("server.tomcat",
@@ -113,7 +113,7 @@ class SampleOAuth2AuthorizationServerApplicationTests {
void anonymousShouldRedirectToLogin() {
RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange();
response.expectStatus().isFound();
response.expectHeader().location("http://localhost:" + this.port + "/login");
response.expectHeader().location("/login");
}
@Test
@@ -182,7 +182,7 @@ class SampleOAuth2AuthorizationServerApplicationTests {
.body(body)
.exchange();
response.expectStatus().isFound();
response.expectHeader().location("http://localhost:" + this.port + "/login");
response.expectHeader().location("/login");
}
}
@@ -54,7 +54,7 @@ class SampleOAuth2ClientApplicationTests {
void everythingShouldRedirectToLogin() {
RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange();
response.expectStatus().isFound();
response.expectHeader().location("http://localhost:" + this.port + "/login");
response.expectHeader().location("/login");
}
@Test
@@ -52,7 +52,7 @@ class SampleSaml2RelyingPartyApplicationTests {
void everythingShouldRedirectToLogin() {
RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange();
response.expectStatus().isFound();
response.expectHeader().location("http://localhost:" + this.port + "/login");
response.expectHeader().location("/login");
}
@Test
@@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
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.test.web.servlet.client.RestTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -38,9 +37,6 @@ import static org.assertj.core.api.Assertions.assertThat;
@AutoConfigureRestTestClient
class SampleGroovyTemplateApplicationTests {
@LocalServerPort
private int port;
@Autowired
private RestTestClient restTestClient;
@@ -62,7 +58,7 @@ class SampleGroovyTemplateApplicationTests {
.body(map)
.exchange()
.expectHeader()
.value("Location", (location) -> assertThat(location).contains("localhost:" + this.port));
.value("Location", (location) -> assertThat(location).matches("/\\d+(;jsessionid=[\\w.]+)?"));
}
@Test
@@ -91,7 +91,7 @@ class SampleMethodSecurityApplicationTests {
assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND);
URI location = result.getResponseHeaders().getLocation();
assertThat(location).isNotNull();
assertThat(location.toString()).endsWith(this.port + "/");
assertThat(location.toString()).isEqualTo("/");
}
@Test
@@ -71,7 +71,7 @@ class SampleWebSecureCustomApplicationTests {
assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND);
URI location = result.getResponseHeaders().getLocation();
assertThat(location).isNotNull();
assertThat(location.toString()).endsWith(this.port + "/login");
assertThat(location.toString()).isEqualTo("/login");
}
@Test
@@ -99,7 +99,7 @@ class SampleWebSecureCustomApplicationTests {
assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND);
URI location = result.getResponseHeaders().getLocation();
assertThat(location).isNotNull();
assertThat(location.toString()).endsWith(this.port + "/");
assertThat(location.toString()).isEqualTo("/");
}
}
@@ -71,7 +71,7 @@ class SampleWebSecureJdbcApplicationTests {
assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND);
URI location = result.getResponseHeaders().getLocation();
assertThat(location).isNotNull();
assertThat(location.toString()).endsWith(this.port + "/login");
assertThat(location.toString()).isEqualTo("/login");
}
@Test
@@ -99,7 +99,7 @@ class SampleWebSecureJdbcApplicationTests {
assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND);
URI location = result.getResponseHeaders().getLocation();
assertThat(location).isNotNull();
assertThat(location.toString()).endsWith(this.port + "/");
assertThat(location.toString()).isEqualTo("/");
}
}
@@ -78,7 +78,7 @@ class SampleWebSecureApplicationTests {
assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND);
URI location = result.getResponseHeaders().getLocation();
assertThat(location).isNotNull();
assertThat(location.toString()).endsWith(this.port + "/login");
assertThat(location.toString()).isEqualTo("/login");
}
@Test
@@ -106,7 +106,7 @@ class SampleWebSecureApplicationTests {
assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND);
URI location = result.getResponseHeaders().getLocation();
assertThat(location).isNotNull();
assertThat(location.toString()).endsWith(this.port + "/");
assertThat(location.toString()).isEqualTo("/");
}
@org.springframework.boot.test.context.TestConfiguration(proxyBeanMethods = false)
@@ -24,7 +24,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
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.test.web.servlet.client.RestTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -43,9 +42,6 @@ class SampleWebUiApplicationTests {
@Autowired
private RestTestClient restTestClient;
@LocalServerPort
private int port;
@Test
void testHome() {
this.restTestClient.get().uri("/").exchangeSuccessfully().expectBody(String.class).value((body) -> {
@@ -67,7 +63,7 @@ class SampleWebUiApplicationTests {
.getResponseHeaders()
.getLocation();
assertThat(location).isNotNull();
assertThat(location.toString()).contains("localhost:" + this.port);
assertThat(location.toString()).matches("/\\d+(;jsessionid=[\\w.]+)?");
}
}