Defer to Tomcat's default for use-relative-redirects - #51173
Open
basteez wants to merge 1 commit into
Open
Conversation
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 spring-projectsgh-50900 Signed-off-by: Tiziano Basile <tiz.basile@gmail.com>
basteez
force-pushed
the
gh-50900-tomcat-relative-redirects
branch
from
August 1, 2026 14:45
fbd6ef6 to
88d6b89
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Defer to Tomcat's default for use-relative-redirects
Closes gh-50900
Spring Boot unconditionally calls
setUseRelativeRedirectson the TomcatContext, with theproperty defaulting to
false. So a plainsendRedirect("/login")comes back asLocation: http://host:port/login, and it does so because Boot went out of its way to make ithappen rather than because Tomcat wanted it.
The obvious fix is to flip the default to
true. I started there and it is wrong, so I want toexplain why before describing what I did instead.
Tomcat's default is not a constant:
Probing it directly on tomcat-embed-core 11.0.24:
So hardcoding
truedoes not stop Boot overriding Tomcat. It keeps overriding it, just in theopposite direction, and specifically for the people who deliberately turned on strict servlet
compliance. That is the same point Andy made when he first declined this issue, citing
StandardContextusing absolute redirects in strict compliance mode. I did not appreciate what itimplied until I had the code in front of me.
What this does instead is make
server.tomcat.use-relative-redirectsa nullableBooleanthat isonly applied when the user has actually set it, mirroring the sibling
redirect-context-rootproperty that sits three lines above it in the same customizer. Unset now means "Boot does not
touch this", so Tomcat's own default wins in every mode. Setting the property explicitly still
works in both directions.
The cost, and I want to be upfront about it: this changes the accessors from
isUseRelativeRedirects()/setUseRelativeRedirects(boolean)togetUseRelativeRedirects()/setUseRelativeRedirects(Boolean). It is also a reversal of f29bce6 (gh-20796), whichdeliberately went from
Booleantobooleanto harmonise the default. I think the strictcompliance case is the argument for going back, but that is your call and I am happy to be told
otherwise. If you would prefer to keep the primitive and just accept overriding strict compliance
mode, the change is much smaller and I can redo it that way.
Two other things I would rather flag than have you find:
The published configuration metadata loses its
defaultValue. It used to emitfalse, and nowemits nothing, because there genuinely is no Boot default any more. The practical effect is a blank
default column in the properties appendix and no default in IDE completion. That seemed correct
rather than unfortunate, but it is visible.
server.forward-headers-strategy=frameworknullifies this change entirely.ForwardedHeaderFilterrebuilds redirects into absolute forwarded URLs and Boot never calls its
setRelativeRedirects,so
frameworkusers see no change whilenativeusers do. That inconsistency already exists todayand fixing it would take this PR well past the scope of gh-50900, so I have left it alone. Happy to
open a separate issue if it is worth tracking.
What is in the change:
TomcatServerPropertiesgets a nullableBooleanfield and updated Javadoc. Since that Javadocbecomes the published metadata description, it now says that leaving the property unset defers to
Tomcat, and that the property has no effect on a reactive web server, which was already true but
undocumented.
TomcatServletWebServerFactoryCustomizerguards the call with a null check, exactly as it alreadydoes for
redirectContextRoot.TomcatServletWebServerFactoryCustomizerTests. The important one isuseRelativeRedirectsWhenNotSetDoesNotCustomizeContext, which asserts via a mockContextthatnothing calls
setUseRelativeRedirectswhen the property is unset. I wrote a version thatcompared against
new StandardContext().getUseRelativeRedirects()first and then found it passeseven with the buggy hardcoded implementation, because CI runs without strict compliance and both
sides are
true. The mock based one fails properly. I verified that by temporarily putting thehardcoded version back and watching it go red.
log on the earlier attempt only surfaced four of them. Where they previously did
endsWith(this.port + "/login")they now doisEqualTo("/login"), which is a tighter assertionthan what was there before. Two cases redirect to a generated id and match on the relative pattern
instead. Two now unused
@LocalServerPortfields are removed as a consequence.redirect-context-root=falseso
X-Forwarded-Protois honoured before redirects, which only makes sense when the context rootredirect is absolute and carries a scheme. Following it unchanged after this would produce a 404
for no benefit.
Verification, all on JDK 25 with
SPRING_PROFILES_ACTIVEunset:./gradlew :module:spring-boot-tomcat:checkpasses./gradlew :documentation:spring-boot-docs:checkpasses--rerun-tasksto confirm the green was real rather than a cache hit./gradlew formatleaves the tree cleanI also built a small application outside the repo to watch the headers over a real connection. With
no configuration the Location headers are relative, with
=falsethey are absolute, and with-Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=trueand no Boot property set at all they comeback absolute because Tomcat asked for it. That last case is the one that convinced me the nullable
approach was necessary. Redirects to fully qualified external URLs are untouched in every mode,
which matters because otherwise every cross origin handoff would break.
No release note here since those live in the wiki, but this is worth calling out in the 4.2 notes.
The symptom for anyone affected is a changed Location header rather than an error, so infrastructure
that matches or rewrites absolute Location values will fail quietly.