From ce0e10503bee7b44b3e868e3253ad123510a24d2 Mon Sep 17 00:00:00 2001 From: sanjay44ns Date: Thu, 21 May 2026 15:19:48 +0530 Subject: [PATCH 1/2] Fix session commits for explicit zero content length Commit the session once an explicit Content-Length of 0 is followed by a body write so String responses still write the session cookie, including empty-body cases. Add regressions for the wrapper and filter paths that previously missed the early commit. Signed-off-by: sanjay44ns --- .../web/http/OnCommittedResponseWrapper.java | 12 +++++-- .../http/OnCommittedResponseWrapperTests.java | 27 ++++++++++++++++ .../http/SessionRepositoryFilterTests.java | 31 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/spring-session-core/src/main/java/org/springframework/session/web/http/OnCommittedResponseWrapper.java b/spring-session-core/src/main/java/org/springframework/session/web/http/OnCommittedResponseWrapper.java index 39c04ac64..b40d259b5 100644 --- a/spring-session-core/src/main/java/org/springframework/session/web/http/OnCommittedResponseWrapper.java +++ b/spring-session-core/src/main/java/org/springframework/session/web/http/OnCommittedResponseWrapper.java @@ -43,6 +43,8 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper { */ private long contentLength; + private boolean contentLengthSet; + /** * The size of data written to the response body. The field will only be updated when * {@link #disableOnCommitted} is false. @@ -107,7 +109,8 @@ public void setContentLength(int len) { private void setContentLength(long len) { this.contentLength = len; - checkContentLength(0); + this.contentLengthSet = true; + checkContentLength(0, false); } /** @@ -259,8 +262,13 @@ private void trackContentLength(String content) { * @param contentLengthToWrite the size of the content that is about to be written. */ private void checkContentLength(long contentLengthToWrite) { + checkContentLength(contentLengthToWrite, true); + } + + private void checkContentLength(long contentLengthToWrite, boolean bodyWriteOccurred) { this.contentWritten += contentLengthToWrite; - boolean isBodyFullyWritten = this.contentLength > 0 && this.contentWritten >= this.contentLength; + boolean isBodyFullyWritten = this.contentLengthSet && this.contentWritten >= this.contentLength + && (this.contentLength > 0 || bodyWriteOccurred); int bufferSize = getBufferSize(); boolean requiresFlush = bufferSize > 0 && this.contentWritten >= bufferSize; if (isBodyFullyWritten || requiresFlush) { diff --git a/spring-session-core/src/test/java/org/springframework/session/web/http/OnCommittedResponseWrapperTests.java b/spring-session-core/src/test/java/org/springframework/session/web/http/OnCommittedResponseWrapperTests.java index d551a19f4..64c740077 100644 --- a/spring-session-core/src/test/java/org/springframework/session/web/http/OnCommittedResponseWrapperTests.java +++ b/spring-session-core/src/test/java/org/springframework/session/web/http/OnCommittedResponseWrapperTests.java @@ -631,6 +631,33 @@ void printWriterWriteStringContentLengthCommits() throws IOException { assertThat(this.committed).isTrue(); } + @Test + void explicitZeroContentLengthPrintWriterWriteStringCommits() throws IOException { + this.response.setContentLength(0); + + this.response.getWriter().write("ok"); + + assertThat(this.committed).isTrue(); + } + + @Test + void explicitZeroContentLengthPrintWriterWriteEmptyStringCommits() throws IOException { + this.response.setContentLength(0); + + this.response.getWriter().write(""); + + assertThat(this.committed).isTrue(); + } + + @Test + void explicitZeroContentLengthOutputStreamWriteEmptyByteArrayCommits() throws IOException { + this.response.setContentLength(0); + + this.response.getOutputStream().write(new byte[0]); + + assertThat(this.committed).isTrue(); + } + @Test void printWriterWriteStringDoesNotCommit() throws IOException { String body = "something"; diff --git a/spring-session-core/src/test/java/org/springframework/session/web/http/SessionRepositoryFilterTests.java b/spring-session-core/src/test/java/org/springframework/session/web/http/SessionRepositoryFilterTests.java index 150b3e341..dc86ed281 100644 --- a/spring-session-core/src/test/java/org/springframework/session/web/http/SessionRepositoryFilterTests.java +++ b/spring-session-core/src/test/java/org/springframework/session/web/http/SessionRepositoryFilterTests.java @@ -17,6 +17,7 @@ package org.springframework.session.web.http; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -973,6 +974,36 @@ public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrap }); } + @Test + void doFilterOutputWriteWithExplicitZeroContentLengthCommitsSession() throws Exception { + doFilter(new DoInFilter() { + @Override + public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse) + throws IOException { + String id = wrappedRequest.getSession().getId(); + wrappedResponse.setContentLength(0); + wrappedResponse.getOutputStream().write("ok".getBytes(StandardCharsets.UTF_8)); + assertThat(SessionRepositoryFilterTests.this.sessionRepository.findById(id)).isNotNull(); + assertThat(SessionRepositoryFilterTests.this.response.getCookie("SESSION")).isNotNull(); + } + }); + } + + @Test + void doFilterOutputWriteEmptyBodyWithExplicitZeroContentLengthCommitsSession() throws Exception { + doFilter(new DoInFilter() { + @Override + public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse) + throws IOException { + String id = wrappedRequest.getSession().getId(); + wrappedResponse.setContentLength(0); + wrappedResponse.getOutputStream().write(new byte[0]); + assertThat(SessionRepositoryFilterTests.this.sessionRepository.findById(id)).isNotNull(); + assertThat(SessionRepositoryFilterTests.this.response.getCookie("SESSION")).isNotNull(); + } + }); + } + @Test // gh-1243 void doFilterInclude() throws Exception { doFilter(new DoInFilter() { From b29b9dc7539b60ba5bf55b3a7f85c11b8f1de9a4 Mon Sep 17 00:00:00 2001 From: sanjay44ns Date: Thu, 21 May 2026 16:01:54 +0530 Subject: [PATCH 2/2] Add sample checks for string response session cookies Add plain String endpoints to the JDBC and Redis Boot samples and verify with MockMvc that the response sets the Spring Session cookie without falling back to JSESSIONID. Signed-off-by: sanjay44ns --- .../integration-test/java/sample/BootTests.java | 14 ++++++++++++++ .../src/main/java/sample/IndexController.java | 10 ++++++++++ .../main/java/sample/config/SecurityConfig.java | 1 + .../integration-test/java/sample/BootTests.java | 14 ++++++++++++++ .../main/java/sample/config/IndexController.java | 9 +++++++++ .../main/java/sample/config/SecurityConfig.java | 1 + 6 files changed, 49 insertions(+) diff --git a/spring-session-samples/spring-session-sample-boot-jdbc/src/integration-test/java/sample/BootTests.java b/spring-session-samples/spring-session-sample-boot-jdbc/src/integration-test/java/sample/BootTests.java index f6345fb06..314449963 100644 --- a/spring-session-samples/spring-session-sample-boot-jdbc/src/integration-test/java/sample/BootTests.java +++ b/spring-session-samples/spring-session-sample-boot-jdbc/src/integration-test/java/sample/BootTests.java @@ -32,6 +32,11 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + /** * @author Eddú Meléndez * @author Vedran Pavic @@ -82,4 +87,13 @@ void logout() { login.assertAt(); } + @Test + void stringResponseSetsSessionCookie() throws Exception { + this.mockMvc.perform(get("/string")) + .andExpect(status().isOk()) + .andExpect(content().string("ok")) + .andExpect(cookie().exists("SESSION")) + .andExpect(cookie().doesNotExist("JSESSIONID")); + } + } diff --git a/spring-session-samples/spring-session-sample-boot-jdbc/src/main/java/sample/IndexController.java b/spring-session-samples/spring-session-sample-boot-jdbc/src/main/java/sample/IndexController.java index ee6588b88..7966a8d62 100644 --- a/spring-session-samples/spring-session-sample-boot-jdbc/src/main/java/sample/IndexController.java +++ b/spring-session-samples/spring-session-sample-boot-jdbc/src/main/java/sample/IndexController.java @@ -16,7 +16,10 @@ package sample; +import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RequestMapping; /** @@ -33,4 +36,11 @@ public String index() { return "index"; } + @GetMapping("/string") + @ResponseBody + public String string(HttpSession session) { + session.setAttribute("sample", "value"); + return "ok"; + } + } diff --git a/spring-session-samples/spring-session-sample-boot-jdbc/src/main/java/sample/config/SecurityConfig.java b/spring-session-samples/spring-session-sample-boot-jdbc/src/main/java/sample/config/SecurityConfig.java index 517700366..3d10a6e5e 100644 --- a/spring-session-samples/spring-session-sample-boot-jdbc/src/main/java/sample/config/SecurityConfig.java +++ b/spring-session-samples/spring-session-sample-boot-jdbc/src/main/java/sample/config/SecurityConfig.java @@ -47,6 +47,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests((authorize) -> authorize .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() + .requestMatchers("/string").permitAll() .anyRequest().authenticated() ) .formLogin((formLogin) -> formLogin diff --git a/spring-session-samples/spring-session-sample-boot-redis/src/integration-test/java/sample/BootTests.java b/spring-session-samples/spring-session-sample-boot-redis/src/integration-test/java/sample/BootTests.java index 12294afc0..f1d1a4c23 100644 --- a/spring-session-samples/spring-session-sample-boot-redis/src/integration-test/java/sample/BootTests.java +++ b/spring-session-samples/spring-session-sample-boot-redis/src/integration-test/java/sample/BootTests.java @@ -36,6 +36,11 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + /** * @author Eddú Meléndez * @author Vedran Pavic @@ -85,6 +90,15 @@ void logout() { login.assertAt(); } + @Test + void stringResponseSetsSessionCookie() throws Exception { + this.mockMvc.perform(get("/string")) + .andExpect(status().isOk()) + .andExpect(content().string("ok")) + .andExpect(cookie().exists("SESSION")) + .andExpect(cookie().doesNotExist("JSESSIONID")); + } + @TestConfiguration static class Config { diff --git a/spring-session-samples/spring-session-sample-boot-redis/src/main/java/sample/config/IndexController.java b/spring-session-samples/spring-session-sample-boot-redis/src/main/java/sample/config/IndexController.java index ba795ff26..c8405c544 100644 --- a/spring-session-samples/spring-session-sample-boot-redis/src/main/java/sample/config/IndexController.java +++ b/spring-session-samples/spring-session-sample-boot-redis/src/main/java/sample/config/IndexController.java @@ -16,8 +16,10 @@ package sample.config; +import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; /** * An index controller. @@ -32,4 +34,11 @@ String index() { return "index"; } + @GetMapping("/string") + @ResponseBody + String string(HttpSession session) { + session.setAttribute("sample", "value"); + return "ok"; + } + } diff --git a/spring-session-samples/spring-session-sample-boot-redis/src/main/java/sample/config/SecurityConfig.java b/spring-session-samples/spring-session-sample-boot-redis/src/main/java/sample/config/SecurityConfig.java index 430a245f7..8161e5935 100644 --- a/spring-session-samples/spring-session-sample-boot-redis/src/main/java/sample/config/SecurityConfig.java +++ b/spring-session-samples/spring-session-sample-boot-redis/src/main/java/sample/config/SecurityConfig.java @@ -38,6 +38,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests((authorize) -> authorize .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() + .requestMatchers("/string").permitAll() .anyRequest().authenticated() ) .formLogin((formLogin) -> formLogin