Skip to content

Commit 12c3dc0

Browse files
committed
Fix compressed HEAD requests handling in JDK client
Prior to this commit, the `JdkClientHttpRequestFactory` would support decompressing gziped/deflate encoded response bodies but would fail if the response has no body but has a "Content-Encoding" response header. This happens as a response to HEAD requests. This commit ensures that only responses with actual message bodies are decompressed. Fixes gh-35966
1 parent df27627 commit 12c3dc0

File tree

3 files changed

+77
-18
lines changed

3 files changed

+77
-18
lines changed

spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.FilterInputStream;
2020
import java.io.IOException;
2121
import java.io.InputStream;
22+
import java.io.PushbackInputStream;
2223
import java.io.UncheckedIOException;
2324
import java.net.URI;
2425
import java.net.http.HttpClient;
@@ -60,6 +61,7 @@
6061
*
6162
* @author Marten Deinum
6263
* @author Arjen Poutsma
64+
* @author Brian Clozel
6365
* @since 6.1
6466
*/
6567
class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
@@ -325,30 +327,61 @@ public void handleCancellationException(CancellationException ex) throws HttpTim
325327
*/
326328
private static final class DecompressingBodyHandler implements BodyHandler<InputStream> {
327329

330+
328331
@Override
329332
public BodySubscriber<InputStream> apply(ResponseInfo responseInfo) {
330-
String contentEncoding = responseInfo.headers().firstValue(HttpHeaders.CONTENT_ENCODING).orElse("");
331-
if (contentEncoding.equalsIgnoreCase("gzip")) {
332-
return BodySubscribers.mapping(
333+
334+
String contentEncoding = responseInfo.headers()
335+
.firstValue(HttpHeaders.CONTENT_ENCODING)
336+
.orElse("")
337+
.toLowerCase(Locale.ROOT);
338+
339+
return switch (contentEncoding) {
340+
case "gzip", "deflate" -> BodySubscribers.mapping(
333341
BodySubscribers.ofInputStream(),
334-
(InputStream is) -> {
335-
try {
336-
return new GZIPInputStream(is);
337-
}
338-
catch (IOException ex) {
339-
throw new UncheckedIOException(ex);
340-
}
341-
});
342+
(InputStream is) -> decompressStream(is, contentEncoding));
343+
default -> BodySubscribers.ofInputStream();
344+
};
345+
}
346+
347+
private static InputStream decompressStream(InputStream original, String contentEncoding) {
348+
PushbackInputStream wrapped = new PushbackInputStream(original);
349+
try {
350+
if (hasResponseBody(wrapped)) {
351+
if (contentEncoding.equals("gzip")) {
352+
return new GZIPInputStream(wrapped);
353+
}
354+
else if (contentEncoding.equals("deflate")) {
355+
return new InflaterInputStream(wrapped);
356+
}
357+
}
358+
else {
359+
return wrapped;
360+
}
342361
}
343-
else if (contentEncoding.equalsIgnoreCase("deflate")) {
344-
return BodySubscribers.mapping(
345-
BodySubscribers.ofInputStream(),
346-
InflaterInputStream::new);
362+
catch (IOException ex) {
363+
throw new UncheckedIOException(ex);
347364
}
348-
else {
349-
return BodySubscribers.ofInputStream();
365+
return wrapped;
366+
}
367+
368+
private static boolean hasResponseBody(PushbackInputStream inputStream) {
369+
try {
370+
int b = inputStream.read();
371+
if (b == -1) {
372+
return false;
373+
}
374+
else {
375+
inputStream.unread(b);
376+
return true;
377+
}
378+
379+
}
380+
catch (IOException exc) {
381+
return false;
350382
}
351383
}
352384
}
353385

386+
354387
}

spring-web/src/test/java/org/springframework/http/client/AbstractMockWebServerTests.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ else if(request.getTarget().startsWith("/header/")) {
112112
String headerName = request.getTarget().replace("/header/","");
113113
return new MockResponse.Builder().body(headerName + ":" + request.getHeaders().get(headerName)).code(200).build();
114114
}
115-
else if(request.getTarget().startsWith("/compress/") && request.getBody() != null) {
115+
else if(request.getMethod().equals("POST") && request.getTarget().startsWith("/compress/") && request.getBody() != null) {
116116
String encoding = request.getTarget().replace("/compress/","");
117117
String requestBody = request.getBody().utf8();
118118
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
@@ -139,6 +139,13 @@ else if(request.getTarget().startsWith("/compress/") && request.getBody() != nul
139139
builder.setHeader(HttpHeaders.CONTENT_LENGTH, buffer.size());
140140
return builder.build();
141141
}
142+
else if (request.getMethod().equals("HEAD") && request.getTarget().startsWith("/headforcompress/")) {
143+
String encoding = request.getTarget().replace("/headforcompress/","");
144+
MockResponse.Builder builder = new MockResponse.Builder().code(200)
145+
.setHeader(HttpHeaders.CONTENT_LENGTH, 500)
146+
.setHeader(HttpHeaders.CONTENT_ENCODING, encoding);
147+
return builder.build();
148+
}
142149
return new MockResponse.Builder().code(404).build();
143150
}
144151
catch (Throwable ex) {

spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestFactoryTests.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.junit.jupiter.api.Test;
2727
import org.junit.jupiter.api.condition.EnabledForJreRange;
2828
import org.junit.jupiter.api.condition.JRE;
29+
import org.junit.jupiter.params.ParameterizedTest;
30+
import org.junit.jupiter.params.provider.ValueSource;
2931

3032
import org.springframework.http.HttpMethod;
3133
import org.springframework.http.HttpStatus;
@@ -159,6 +161,23 @@ void compressionDeflate() throws IOException {
159161
}
160162
}
161163

164+
@ParameterizedTest
165+
@ValueSource(strings = {"gzip", "deflate"})
166+
void gzipCompressionWithHeadRequest(String compression) throws IOException {
167+
URI uri = URI.create(baseUrl + "/headforcompress/" + compression);
168+
JdkClientHttpRequestFactory requestFactory = (JdkClientHttpRequestFactory) this.factory;
169+
requestFactory.enableCompression(true);
170+
ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.HEAD);
171+
try (ClientHttpResponse response = request.execute()) {
172+
assertThat(response.getStatusCode()).as("Invalid response status").isEqualTo(HttpStatus.OK);
173+
assertThat(response.getHeaders().getFirst("Content-Encoding"))
174+
.as("Content Encoding should be removed").isNull();
175+
assertThat(response.getHeaders().getFirst("Content-Length"))
176+
.as("Content-Length should be removed").isNull();
177+
assertThat(response.getBody()).as("Invalid response body").isEmpty();
178+
}
179+
}
180+
162181
@Test // gh-34971
163182
@EnabledForJreRange(min = JRE.JAVA_19) // behavior fixed in Java 19
164183
void requestContentLengthHeaderWhenNoBody() throws Exception {

0 commit comments

Comments
 (0)