Skip to content

Commit fbd6ef6

Browse files
committed
Defer to Tomcat's default for use-relative-redirects
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. Tomcat's default is not a constant. StandardContext declares it as !Globals.STRICT_SERVLET_COMPLIANCE, so it is true normally and false under strict servlet compliance. Simply flipping Boot's default to true would still override Tomcat, just in the other direction, and precisely for users who opted into strict compliance. Make server.tomcat.use-relative-redirects a nullable Boolean that is only applied when set, mirroring the sibling redirect-context-root property. When it is left unset Boot no longer touches the setting and Tomcat's own default wins in every mode. Setting the property explicitly continues to work in both directions. This changes the accessors from isUseRelativeRedirects()/ setUseRelativeRedirects(boolean) to getUseRelativeRedirects()/ setUseRelativeRedirects(Boolean). 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. See gh-50900 Signed-off-by: Tiziano Basile <tiziano.basile@nearform.com>
1 parent db19648 commit fbd6ef6

14 files changed

Lines changed: 63 additions & 32 deletions

File tree

documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,9 @@ server:
562562

563563
NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but do not do so in production).
564564

565-
TIP: If you are using Tomcat and terminating SSL at the proxy, configprop:server.tomcat.redirect-context-root[] should be set to `false`.
565+
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`.
566566
This allows the `X-Forwarded-Proto` header to be honored before any redirects are performed.
567+
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.
567568

568569
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.
569570

module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerProperties.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
* @author Florian Storz
5757
* @author Michael Weidmann
5858
* @author Lasse Wulff
59+
* @author Tiziano Basile
5960
* @since 4.0.0
6061
*/
6162
@ConfigurationProperties("server.tomcat")
@@ -103,9 +104,11 @@ public class TomcatServerProperties {
103104

104105
/**
105106
* Whether HTTP 1.1 and later location headers generated by a call to sendRedirect
106-
* will use relative or absolute redirects.
107+
* will use relative or absolute redirects. When not set, Tomcat's own default is
108+
* used, which is relative unless strict servlet compliance is enabled. Has no effect
109+
* on a reactive web server.
107110
*/
108-
private boolean useRelativeRedirects;
111+
private @Nullable Boolean useRelativeRedirects;
109112

110113
/**
111114
* Character encoding to use to decode the URI.
@@ -235,11 +238,11 @@ public void setRedirectContextRoot(Boolean redirectContextRoot) {
235238
this.redirectContextRoot = redirectContextRoot;
236239
}
237240

238-
public boolean isUseRelativeRedirects() {
241+
public @Nullable Boolean getUseRelativeRedirects() {
239242
return this.useRelativeRedirects;
240243
}
241244

242-
public void setUseRelativeRedirects(boolean useRelativeRedirects) {
245+
public void setUseRelativeRedirects(@Nullable Boolean useRelativeRedirects) {
243246
this.useRelativeRedirects = useRelativeRedirects;
244247
}
245248

module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*
3030
* @author Brian Clozel
3131
* @author Phillip Webb
32+
* @author Tiziano Basile
3233
*/
3334
class TomcatServletWebServerFactoryCustomizer
3435
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>, Ordered {
@@ -52,7 +53,9 @@ public void customize(TomcatServletWebServerFactory factory) {
5253
if (this.tomcatProperties.getRedirectContextRoot() != null) {
5354
customizeRedirectContextRoot(factory, this.tomcatProperties.getRedirectContextRoot());
5455
}
55-
customizeUseRelativeRedirects(factory, this.tomcatProperties.isUseRelativeRedirects());
56+
if (this.tomcatProperties.getUseRelativeRedirects() != null) {
57+
customizeUseRelativeRedirects(factory, this.tomcatProperties.getUseRelativeRedirects());
58+
}
5659
}
5760

5861
private void customizeRedirectContextRoot(ConfigurableTomcatWebServerFactory factory, boolean redirectContextRoot) {

module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerPropertiesTests.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
* Tests for {@link TomcatServerProperties}.
4646
*
4747
* @author Andy Wilkinson
48+
* @author Tiziano Basile
4849
*/
4950
class TomcatServerPropertiesTests {
5051

@@ -95,7 +96,7 @@ void testTomcatBinding() {
9596
assertThat(this.properties.getBackgroundProcessorDelay()).hasSeconds(10);
9697
assertThat(this.properties.getRelaxedPathChars()).containsExactly('|', '<');
9798
assertThat(this.properties.getRelaxedQueryChars()).containsExactly('^', '|');
98-
assertThat(this.properties.isUseRelativeRedirects()).isTrue();
99+
assertThat(this.properties.getUseRelativeRedirects()).isTrue();
99100
}
100101

101102
@Test
@@ -235,8 +236,8 @@ void tomcatInternalProxiesMatchesDefault() {
235236
}
236237

237238
@Test
238-
void tomcatUseRelativeRedirectsDefaultsToFalse() {
239-
assertThat(this.properties.isUseRelativeRedirects()).isFalse();
239+
void tomcatUseRelativeRedirectsIsNotSetByDefault() {
240+
assertThat(this.properties.getUseRelativeRedirects()).isNull();
240241
}
241242

242243
@Test

module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizerTests.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.tomcat.autoconfigure.servlet;
1818

1919
import org.apache.catalina.Context;
20+
import org.apache.catalina.core.StandardContext;
2021
import org.junit.jupiter.api.BeforeEach;
2122
import org.junit.jupiter.api.Test;
2223

@@ -30,11 +31,16 @@
3031
import org.springframework.test.context.support.TestPropertySourceUtils;
3132

3233
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.mockito.ArgumentMatchers.anyBoolean;
35+
import static org.mockito.BDDMockito.then;
36+
import static org.mockito.Mockito.mock;
37+
import static org.mockito.Mockito.never;
3338

3439
/**
3540
* Tests for {@link TomcatServletWebServerFactoryCustomizer}.
3641
*
3742
* @author Phillip Webb
43+
* @author Tiziano Basile
3844
*/
3945
class TomcatServletWebServerFactoryCustomizerTests {
4046

@@ -80,14 +86,39 @@ void redirectContextRootCanBeConfigured() {
8086
}
8187

8288
@Test
83-
void useRelativeRedirectsCanBeConfigured() {
89+
void useRelativeRedirectsWhenNotSetDoesNotCustomizeContext() {
90+
TomcatServletWebServerFactory factory = customizeAndGetFactory();
91+
Context context = mock(Context.class);
92+
factory.getContextCustomizers().forEach((customizer) -> customizer.customize(context));
93+
then(context).should(never()).setUseRelativeRedirects(anyBoolean());
94+
}
95+
96+
@Test
97+
void useRelativeRedirectsWhenNotSetUsesTomcatsDefault() {
98+
assertThat(this.tomcatProperties.getUseRelativeRedirects()).isNull();
99+
TomcatWebServer server = customizeAndGetServer();
100+
Context context = (Context) server.getTomcat().getHost().findChildren()[0];
101+
assertThat(context.getUseRelativeRedirects()).isEqualTo(new StandardContext().getUseRelativeRedirects());
102+
}
103+
104+
@Test
105+
void useRelativeRedirectsCanBeEnabled() {
84106
bind("server.tomcat.use-relative-redirects=true");
85-
assertThat(this.tomcatProperties.isUseRelativeRedirects()).isTrue();
107+
assertThat(this.tomcatProperties.getUseRelativeRedirects()).isTrue();
86108
TomcatWebServer server = customizeAndGetServer();
87109
Context context = (Context) server.getTomcat().getHost().findChildren()[0];
88110
assertThat(context.getUseRelativeRedirects()).isTrue();
89111
}
90112

113+
@Test
114+
void useRelativeRedirectsCanBeDisabled() {
115+
bind("server.tomcat.use-relative-redirects=false");
116+
assertThat(this.tomcatProperties.getUseRelativeRedirects()).isFalse();
117+
TomcatWebServer server = customizeAndGetServer();
118+
Context context = (Context) server.getTomcat().getHost().findChildren()[0];
119+
assertThat(context.getUseRelativeRedirects()).isFalse();
120+
}
121+
91122
private void bind(String... inlinedProperties) {
92123
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, inlinedProperties);
93124
new Binder(ConfigurationPropertySources.get(this.environment)).bind("server.tomcat",

smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ void authServerMetadataShouldAllowAccess() {
112112
void anonymousShouldRedirectToLogin() {
113113
RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange();
114114
response.expectStatus().isFound();
115-
response.expectHeader().location("http://localhost:" + this.port + "/login");
115+
response.expectHeader().location("/login");
116116
}
117117

118118
@Test
@@ -181,7 +181,7 @@ void anonymousTokenRequestWithAcceptHeaderTextHtmlShouldRedirectToLogin() {
181181
.body(body)
182182
.exchange();
183183
response.expectStatus().isFound();
184-
response.expectHeader().location("http://localhost:" + this.port + "/login");
184+
response.expectHeader().location("/login");
185185
}
186186

187187
}

smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private RestTestClient nonFollowingRedirect() {
5353
void everythingShouldRedirectToLogin() {
5454
RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange();
5555
response.expectStatus().isFound();
56-
response.expectHeader().location("http://localhost:" + this.port + "/login");
56+
response.expectHeader().location("/login");
5757
}
5858

5959
@Test

smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ private RestTestClient nonFollowingRedirect() {
5151
void everythingShouldRedirectToLogin() {
5252
RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange();
5353
response.expectStatus().isFound();
54-
response.expectHeader().location("http://localhost:" + this.port + "/login");
54+
response.expectHeader().location("/login");
5555
}
5656

5757
@Test

smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
2323
import org.springframework.boot.test.context.SpringBootTest;
2424
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
25-
import org.springframework.boot.test.web.server.LocalServerPort;
2625
import org.springframework.test.web.servlet.client.RestTestClient;
2726
import org.springframework.util.LinkedMultiValueMap;
2827
import org.springframework.util.MultiValueMap;
@@ -38,9 +37,6 @@
3837
@AutoConfigureRestTestClient
3938
class SampleGroovyTemplateApplicationTests {
4039

41-
@LocalServerPort
42-
private int port;
43-
4440
@Autowired
4541
private RestTestClient restTestClient;
4642

@@ -62,7 +58,7 @@ void testCreate() {
6258
.body(map)
6359
.exchange()
6460
.expectHeader()
65-
.value("Location", (location) -> assertThat(location).contains("localhost:" + this.port));
61+
.value("Location", (location) -> assertThat(location).matches("/\\d+(;jsessionid=[\\w.]+)?"));
6662
}
6763

6864
@Test

smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ void testLogin() {
9191
assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND);
9292
URI location = result.getResponseHeaders().getLocation();
9393
assertThat(location).isNotNull();
94-
assertThat(location.toString()).endsWith(this.port + "/");
94+
assertThat(location.toString()).isEqualTo("/");
9595
}
9696

9797
@Test

0 commit comments

Comments
 (0)