From 3647c46ca0f701703a6ab4bea90774cb19350549 Mon Sep 17 00:00:00 2001 From: AtharvUrunkar Date: Fri, 5 Dec 2025 14:31:49 +0530 Subject: [PATCH] Fix gzip handling for HEAD requests in JdkClientHttpRequest (Issue #35966) Skip gzip decompression when the response has Content-Length: 0 or HEAD method, preventing GZIPInputStream errors on empty bodies. Add test ensuring HEAD + gzip + empty body does not throw. Signed-off-by: AtharvUrunkar --- .../http/client/JdkClientHttpRequest.java | 39 +++++++++++++--- .../client/JdkClientHttpRequestTests.java | 45 +++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java index 0052e4a62b9e..eae51efbcaeb 100644 --- a/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java @@ -113,7 +113,7 @@ protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body TimeoutHandler timeoutHandler = null; try { HttpRequest request = buildRequest(headers, body); - responseFuture = this.httpClient.sendAsync(request, this.compression ? new DecompressingBodyHandler() : HttpResponse.BodyHandlers.ofInputStream()); + responseFuture = this.httpClient.sendAsync(request, this.compression ? new DecompressingBodyHandler(this.method) : HttpResponse.BodyHandlers.ofInputStream()); if (this.timeout != null) { timeoutHandler = new TimeoutHandler(responseFuture, this.timeout); HttpResponse response = responseFuture.get(); @@ -325,13 +325,37 @@ public void handleCancellationException(CancellationException ex) throws HttpTim */ private static final class DecompressingBodyHandler implements BodyHandler { + private final HttpMethod method; + + private DecompressingBodyHandler(HttpMethod method) { + this.method = method; + } + @Override public BodySubscriber apply(ResponseInfo responseInfo) { - String contentEncoding = responseInfo.headers().firstValue(HttpHeaders.CONTENT_ENCODING).orElse(""); + + String contentEncoding = responseInfo.headers() + .firstValue(HttpHeaders.CONTENT_ENCODING) + .orElse(""); + + // Skip gzip/deflate if HEAD request (HEAD has no body) + if (this.method == HttpMethod.HEAD) { + return BodySubscribers.replacing(InputStream.nullInputStream()); + } + + // Skip if Content-Length = 0 (empty body) + String contentLength = responseInfo.headers() + .firstValue(HttpHeaders.CONTENT_LENGTH) + .orElse(null); + + if ("0".equals(contentLength)) { + return BodySubscribers.replacing(InputStream.nullInputStream()); + } + if (contentEncoding.equalsIgnoreCase("gzip")) { return BodySubscribers.mapping( BodySubscribers.ofInputStream(), - (InputStream is) -> { + is -> { try { return new GZIPInputStream(is); } @@ -340,15 +364,16 @@ public BodySubscriber apply(ResponseInfo responseInfo) { } }); } - else if (contentEncoding.equalsIgnoreCase("deflate")) { + + if (contentEncoding.equalsIgnoreCase("deflate")) { return BodySubscribers.mapping( BodySubscribers.ofInputStream(), InflaterInputStream::new); } - else { - return BodySubscribers.ofInputStream(); - } + + return BodySubscribers.ofInputStream(); } } + } diff --git a/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java b/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java index 5b2f0bdc42b8..61c0d7a43c6a 100644 --- a/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java @@ -38,6 +38,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.net.InetSocketAddress; + +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.web.client.RestClient; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + + /** * Unit tests for {@link JdkClientHttpRequest}. @@ -74,5 +82,42 @@ void futureCancelled() { private JdkClientHttpRequest createRequest(Duration timeout) { return new JdkClientHttpRequest(client, URI.create("https://abc.com"), HttpMethod.GET, executor, timeout, false); } + @Test + void headRequestWithGzipContentEncodingShouldNotFail() throws Exception { + com.sun.net.httpserver.HttpServer server = + com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(0), 0); + + server.createContext("/test", exchange -> { + // Simulate HEAD-like response: gzip header + no body + exchange.getResponseHeaders().add("Content-Encoding", "gzip"); + exchange.getResponseHeaders().add("Content-Length", "0"); + exchange.sendResponseHeaders(200, 0); // no body + exchange.close(); + }); + + server.start(); + int port = server.getAddress().getPort(); + + try { + RestClient client = RestClient.builder() + .requestFactory(new JdkClientHttpRequestFactory()) + .build(); + + // The original bug: this line used to blow up with gzip + empty body. + assertDoesNotThrow(() -> + client.head() + .uri("http://localhost:" + port + "/test") + .retrieve() + .toBodilessEntity() + ); + } + finally { + server.stop(0); + } + } + + + + }