Ignore flushes on ServletServerHttpResponse output stream

Prior to this commit, flush calls on the output stream returned by
`ServletServerHttpResponse#getBody` would be delegated to the Servlet
response output stream.
This can cause performance issues when `HttpMessageConverter` and other
web components write and flush multiple times to the response body.
Here, the Servlet container is in a better position to flush to the
network at the optimal time and buffer the response body until then.

This is particularly true for `HttpMessageConverters` when they flush
many times the output stream, sometimes due to the underlying codec
library. Instead of revisiting the entire message converter contract, we
are here ignoring flush calls to that output stream.

This change does not affect the client side, nor the
`ServletServerHttpResponse#flush` calls.

This commit also introduces a new Spring property
`"spring.http.response.flush.enabled"` that reverts this behavior change
if necessary.

Closes gh-36385
This commit is contained in:
Brian Clozel
2026-02-24 21:44:48 +01:00
parent f2b64ad09c
commit e0b54e244e
5 changed files with 74 additions and 9 deletions
@@ -19,9 +19,12 @@ package org.springframework.http.server;
import java.nio.charset.StandardCharsets;
import java.util.List;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.SpringProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -29,8 +32,13 @@ import org.springframework.util.FileCopyUtils;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link ServletServerHttpResponse}.
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Juergen Hoeller
@@ -120,4 +128,33 @@ class ServletServerHttpResponseTests {
assertThat(mockResponse.getContentAsByteArray()).as("Invalid content written").isEqualTo(content);
}
@Test
void skipFlushCallsOnOutputStream() throws Exception {
ServletOutputStream mockStream = mock();
HttpServletResponse mockResponse = mock();
when(mockResponse.getOutputStream()).thenReturn(mockStream);
this.response = new ServletServerHttpResponse(mockResponse);
byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8);
FileCopyUtils.copy(content, response.getBody());
response.getBody().flush();
verify(mockStream, never()).flush();
}
@Test
void appliesFlushCallsOnOutputStream() throws Exception {
SpringProperties.setProperty(ServletServerHttpResponse.BODY_FLUSH_ENABLED, Boolean.TRUE.toString());
ServletOutputStream mockStream = mock();
HttpServletResponse mockResponse = mock();
when(mockResponse.getOutputStream()).thenReturn(mockStream);
this.response = new ServletServerHttpResponse(mockResponse);
byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8);
FileCopyUtils.copy(content, response.getBody());
response.getBody().flush();
verify(mockStream).flush();
SpringProperties.setProperty(ServletServerHttpResponse.BODY_FLUSH_ENABLED, null);
}
}