Describe the bug
Spring Security's default response headers (Cache-Control, X-Content-Type-Options, X-XSS-Protection, X-Frame-Options, …) are not written on responses produced through a servlet RequestDispatcher.forward() when running on Tomcat 11. Hitting the same servlet directly returns all headers; only forwarded responses lose them, silently.
The trigger is Tomcat 11's changed default for the Context attribute suspendWrappedResponseAfterForward (false in Tomcat 10.1.x → true in Tomcat 11): after a forward, Catalina suspends/unwraps the wrapped response instead of closing it, so HeaderWriterFilter's OnCommittedResponseWrapper never receives its on-commit callback. The behavior tracks the Tomcat major version and is independent of the Spring Boot and Spring Security versions.
Setting HeaderWriterFilter.setShouldWriteHeadersEagerly(true) (or restoring suspendWrappedResponseAfterForward=false) works around it. Raising it here since HeaderWriterFilter is the component that silently omits the headers: should its default be made robust to Tomcat 11's behavior, or at least documented?
To Reproduce
The sample is a Spring Boot app with Spring Security (permitAll) and a servlet (TargetServlet) mapped at /target/*. It exposes the same servlet three ways (Application.java): directly (/target/*), via a URL forward request.getRequestDispatcher(...).forward(...) (/forward-url), and via a named-dispatcher forward using ServletForwardingController (/forward-named).
mvn clean package
java -jar target/ss-forward-mre-1.0.jar
curl -sD - -o /dev/null http://localhost:8090/target/direct # headers PRESENT
curl -sD - -o /dev/null http://localhost:8090/forward-url # headers MISSING
curl -sD - -o /dev/null http://localhost:8090/forward-named # headers MISSING
Rebuilding against Tomcat 10.1.x (mvn clean package -Dtomcat.version=10.1.39) makes the forwarded responses keep the headers.
Likewise, on Tomcat 11 setting suspendWrappedResponseAfterForward=false restores them; run with --tomcat.suspend-fix=true (the sample wires this flag through a WebServerFactoryCustomizer).
Expected behavior
Responses produced via forward() carry the same Spring Security response headers as direct responses (the Tomcat 10.1 / Spring Boot 3.x behavior).
Sample
https://github.com/mcollovati/spring-security-forward-headers-tomcat11
Environment: Spring Boot 4.1.0, Spring Security 7.1.0, Tomcat 11.0.22, Java 21.
Describe the bug
Spring Security's default response headers (
Cache-Control,X-Content-Type-Options,X-XSS-Protection,X-Frame-Options, …) are not written on responses produced through a servletRequestDispatcher.forward()when running on Tomcat 11. Hitting the same servlet directly returns all headers; only forwarded responses lose them, silently.The trigger is Tomcat 11's changed default for the
ContextattributesuspendWrappedResponseAfterForward(falsein Tomcat 10.1.x →truein Tomcat 11): after a forward, Catalina suspends/unwraps the wrapped response instead of closing it, soHeaderWriterFilter'sOnCommittedResponseWrappernever receives its on-commit callback. The behavior tracks the Tomcat major version and is independent of the Spring Boot and Spring Security versions.Setting
HeaderWriterFilter.setShouldWriteHeadersEagerly(true)(or restoringsuspendWrappedResponseAfterForward=false) works around it. Raising it here sinceHeaderWriterFilteris the component that silently omits the headers: should its default be made robust to Tomcat 11's behavior, or at least documented?To Reproduce
The sample is a Spring Boot app with Spring Security (
permitAll) and a servlet (TargetServlet) mapped at/target/*. It exposes the same servlet three ways (Application.java): directly (/target/*), via a URL forwardrequest.getRequestDispatcher(...).forward(...)(/forward-url), and via a named-dispatcher forward usingServletForwardingController(/forward-named).Rebuilding against Tomcat 10.1.x (
mvn clean package -Dtomcat.version=10.1.39) makes the forwarded responses keep the headers.Likewise, on Tomcat 11 setting
suspendWrappedResponseAfterForward=falserestores them; run with--tomcat.suspend-fix=true(the sample wires this flag through aWebServerFactoryCustomizer).Expected behavior
Responses produced via
forward()carry the same Spring Security response headers as direct responses (the Tomcat 10.1 / Spring Boot 3.x behavior).Sample
https://github.com/mcollovati/spring-security-forward-headers-tomcat11
Environment: Spring Boot 4.1.0, Spring Security 7.1.0, Tomcat 11.0.22, Java 21.