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
This commit is contained in:
Brian Clozel
2025-12-08 15:23:55 +01:00
parent df27627516
commit 12c3dc0cbe
3 changed files with 77 additions and 18 deletions
@@ -19,6 +19,7 @@ package org.springframework.http.client;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
@@ -60,6 +61,7 @@ import org.springframework.util.StringUtils;
*
* @author Marten Deinum
* @author Arjen Poutsma
* @author Brian Clozel
* @since 6.1
*/
class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
@@ -325,30 +327,61 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
*/
private static final class DecompressingBodyHandler implements BodyHandler<InputStream> {
@Override
public BodySubscriber<InputStream> apply(ResponseInfo responseInfo) {
String contentEncoding = responseInfo.headers().firstValue(HttpHeaders.CONTENT_ENCODING).orElse("");
if (contentEncoding.equalsIgnoreCase("gzip")) {
return BodySubscribers.mapping(
String contentEncoding = responseInfo.headers()
.firstValue(HttpHeaders.CONTENT_ENCODING)
.orElse("")
.toLowerCase(Locale.ROOT);
return switch (contentEncoding) {
case "gzip", "deflate" -> BodySubscribers.mapping(
BodySubscribers.ofInputStream(),
(InputStream is) -> {
try {
return new GZIPInputStream(is);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
});
(InputStream is) -> decompressStream(is, contentEncoding));
default -> BodySubscribers.ofInputStream();
};
}
private static InputStream decompressStream(InputStream original, String contentEncoding) {
PushbackInputStream wrapped = new PushbackInputStream(original);
try {
if (hasResponseBody(wrapped)) {
if (contentEncoding.equals("gzip")) {
return new GZIPInputStream(wrapped);
}
else if (contentEncoding.equals("deflate")) {
return new InflaterInputStream(wrapped);
}
}
else {
return wrapped;
}
}
else if (contentEncoding.equalsIgnoreCase("deflate")) {
return BodySubscribers.mapping(
BodySubscribers.ofInputStream(),
InflaterInputStream::new);
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
else {
return BodySubscribers.ofInputStream();
return wrapped;
}
private static boolean hasResponseBody(PushbackInputStream inputStream) {
try {
int b = inputStream.read();
if (b == -1) {
return false;
}
else {
inputStream.unread(b);
return true;
}
}
catch (IOException exc) {
return false;
}
}
}
}