diff --git a/spring-web/src/main/java/org/springframework/web/client/IntrospectingClientHttpResponse.java b/spring-web/src/main/java/org/springframework/web/client/IntrospectingClientHttpResponse.java index ad68051254..d2df16e017 100644 --- a/spring-web/src/main/java/org/springframework/web/client/IntrospectingClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/web/client/IntrospectingClientHttpResponse.java @@ -16,6 +16,7 @@ package org.springframework.web.client; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; @@ -34,7 +35,6 @@ import org.springframework.http.client.ClientHttpResponse; * @author Brian Clozel * @author Rossen Stoyanchev * @since 4.1.5 - * @see RFC 7230 Section 3.3.3 */ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator { @@ -47,14 +47,18 @@ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator { /** - * Indicates whether the response has a message body. + * Indicates whether the response might have a message body. *

Implementation returns {@code false} for: *

- * @return {@code true} if the response has a message body, {@code false} otherwise + *

In other cases, the server could use a {@code Transfer-Encoding} header or just + * write the body and close the response. Reading the message body is then the only way + * to check for the presence of a body. + * @return {@code true} if the response might have a message body, {@code false} otherwise * @throws IOException in case of I/O errors + * @see RFC 7230 Section 3.3.3 */ public boolean hasMessageBody() throws IOException { HttpStatusCode statusCode = getStatusCode(); @@ -62,10 +66,7 @@ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator { statusCode == HttpStatus.NOT_MODIFIED) { return false; } - if (getHeaders().getContentLength() == 0) { - return false; - } - return true; + return getHeaders().getContentLength() != 0; } /** @@ -73,6 +74,7 @@ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator { *

Implementation tries to read the first bytes of the response stream: *

* @return {@code true} if the response has a zero-length message body, {@code false} otherwise @@ -85,26 +87,31 @@ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator { if (body == null) { return true; } - if (body.markSupported()) { - body.mark(1); - if (body.read() == -1) { - return true; + try { + if (body.markSupported()) { + body.mark(1); + if (body.read() == -1) { + return true; + } + else { + body.reset(); + return false; + } } else { - body.reset(); - return false; + this.pushbackInputStream = new PushbackInputStream(body); + int b = this.pushbackInputStream.read(); + if (b == -1) { + return true; + } + else { + this.pushbackInputStream.unread(b); + return false; + } } } - else { - this.pushbackInputStream = new PushbackInputStream(body); - int b = this.pushbackInputStream.read(); - if (b == -1) { - return true; - } - else { - this.pushbackInputStream.unread(b); - return false; - } + catch (EOFException exc) { + return true; } } diff --git a/spring-web/src/test/java/org/springframework/web/client/IntrospectingClientHttpResponseTests.java b/spring-web/src/test/java/org/springframework/web/client/IntrospectingClientHttpResponseTests.java index 55e5b048d5..c34881d228 100644 --- a/spring-web/src/test/java/org/springframework/web/client/IntrospectingClientHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/IntrospectingClientHttpResponseTests.java @@ -17,11 +17,18 @@ package org.springframework.web.client; import java.io.ByteArrayInputStream; +import java.io.EOFException; import java.io.InputStream; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.testfixture.http.client.MockClientHttpResponse; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -30,27 +37,57 @@ import static org.mockito.Mockito.mock; /** * Tests for {@link IntrospectingClientHttpResponse}. * - * @since 5.3.10 - * @author Yin-Jui Liao + * @author Brian Clozel */ class IntrospectingClientHttpResponseTests { - private final ClientHttpResponse response = mock(); - private final IntrospectingClientHttpResponse wrappedResponse = new IntrospectingClientHttpResponse(response); + @ParameterizedTest + @MethodSource("noBodyHttpStatus") + void noMessageBodyWhenStatus(HttpStatus status) throws Exception { + var response = new MockClientHttpResponse(new byte[0], status); + var wrapped = new IntrospectingClientHttpResponse(response); + assertThat(wrapped.hasMessageBody()).isFalse(); + } + + static Stream noBodyHttpStatus() { + return Stream.of(HttpStatus.NO_CONTENT, HttpStatus.EARLY_HINTS, HttpStatus.NOT_MODIFIED); + } @Test - void messageBodyDoesNotExist() throws Exception { - given(response.getBody()).willReturn(null); - assertThat(wrappedResponse.hasEmptyMessageBody()).isTrue(); + void noMessageBodyWhenContentLength0() throws Exception { + var response = new MockClientHttpResponse(new byte[0], HttpStatus.OK); + response.getHeaders().setContentLength(0); + var wrapped = new IntrospectingClientHttpResponse(response); + + assertThat(wrapped.hasMessageBody()).isFalse(); + } + + @Test + void emptyMessageWhenNullInputStream() throws Exception { + ClientHttpResponse mockResponse = mock(); + given(mockResponse.getBody()).willReturn(null); + var wrappedMock = new IntrospectingClientHttpResponse(mockResponse); + assertThat(wrappedMock.hasEmptyMessageBody()).isTrue(); } @Test void messageBodyExists() throws Exception { - InputStream stream = new ByteArrayInputStream("content".getBytes()); - given(response.getBody()).willReturn(stream); - assertThat(wrappedResponse.hasEmptyMessageBody()).isFalse(); + var stream = new ByteArrayInputStream("content".getBytes()); + var response = new MockClientHttpResponse(stream, HttpStatus.OK); + var wrapped = new IntrospectingClientHttpResponse(response); + assertThat(wrapped.hasEmptyMessageBody()).isFalse(); + } + + @Test + void emptyMessageWhenEOFException() throws Exception { + ClientHttpResponse mockResponse = mock(); + InputStream stream = mock(); + given(mockResponse.getBody()).willReturn(stream); + given(stream.read()).willThrow(new EOFException()); + var wrappedMock = new IntrospectingClientHttpResponse(mockResponse); + assertThat(wrappedMock.hasEmptyMessageBody()).isTrue(); } }