Skip to content

Defer to Tomcat's default for use-relative-redirects - #51173

Open
basteez wants to merge 1 commit into
spring-projects:mainfrom
basteez:gh-50900-tomcat-relative-redirects
Open

Defer to Tomcat's default for use-relative-redirects#51173
basteez wants to merge 1 commit into
spring-projects:mainfrom
basteez:gh-50900-tomcat-relative-redirects

Conversation

@basteez

@basteez basteez commented Aug 1, 2026

Copy link
Copy Markdown

Defer to Tomcat's default for use-relative-redirects

Closes gh-50900

Spring Boot unconditionally calls setUseRelativeRedirects on the Tomcat Context, with the
property defaulting to false. So a plain sendRedirect("/login") comes back as
Location: http://host:port/login, and it does so because Boot went out of its way to make it
happen 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 to
explain why before describing what I did instead.

Tomcat's default is not a constant:

// org.apache.catalina.core.StandardContext:783
private boolean useRelativeRedirects = !Globals.STRICT_SERVLET_COMPLIANCE;

Probing it directly on tomcat-embed-core 11.0.24:

no flag                                          -> true
-Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=true -> false

So hardcoding true does not stop Boot overriding Tomcat. It keeps overriding it, just in the
opposite 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
StandardContext using absolute redirects in strict compliance mode. I did not appreciate what it
implied until I had the code in front of me.

What this does instead is make server.tomcat.use-relative-redirects a nullable Boolean that is
only applied when the user has actually set it, mirroring the sibling redirect-context-root
property 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) to getUseRelativeRedirects() /
setUseRelativeRedirects(Boolean). It is also a reversal of f29bce6 (gh-20796), which
deliberately went from Boolean to boolean to harmonise the default. I think the strict
compliance 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 emit false, and now
emits 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=framework nullifies this change entirely. ForwardedHeaderFilter
rebuilds redirects into absolute forwarded URLs and Boot never calls its setRelativeRedirects,
so framework users see no change while native users do. That inconsistency already exists today
and 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:

  • TomcatServerProperties gets a nullable Boolean field and updated Javadoc. Since that Javadoc
    becomes 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.
  • TomcatServletWebServerFactoryCustomizer guards the call with a null check, exactly as it already
    does for redirectContextRoot.
  • Three tests in TomcatServletWebServerFactoryCustomizerTests. The important one is
    useRelativeRedirectsWhenNotSetDoesNotCustomizeContext, which asserts via a mock Context that
    nothing calls setUseRelativeRedirects when the property is unset. I wrote a version that
    compared against new StandardContext().getUseRelativeRedirects() first and then found it passes
    even 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 the
    hardcoded version back and watching it go red.
  • Thirteen smoke test assertions across eight projects that encoded the old absolute form. The CI
    log on the earlier attempt only surfaced four of them. Where they previously did
    endsWith(this.port + "/login") they now do isEqualTo("/login"), which is a tighter assertion
    than what was there before. Two cases redirect to a generated id and match on the relative pattern
    instead. Two now unused @LocalServerPort fields are removed as a consequence.
  • The proxy tip in the reference docs is qualified. It advised setting redirect-context-root=false
    so X-Forwarded-Proto is honoured before redirects, which only makes sense when the context root
    redirect 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_ACTIVE unset:

  • ./gradlew :module:spring-boot-tomcat:check passes
  • ./gradlew :documentation:spring-boot-docs:check passes
  • All 108 smoke test projects pass. I re-ran a sample of the untouched Tomcat based ones with
    --rerun-tasks to confirm the green was real rather than a cache hit
  • ./gradlew format leaves the tree clean

I 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 =false they are absolute, and with
-Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=true and no Boot property set at all they come
back 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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 1, 2026
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
basteez force-pushed the gh-50900-tomcat-relative-redirects branch from fbd6ef6 to 88d6b89 Compare August 1, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Align the default for server.tomcat.use-relative-redirects with Tomcat's default

2 participants