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() { 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