From 6bdbb5d89d1db951e62bc541ef06c2ead3825f4a Mon Sep 17 00:00:00 2001 From: Alhuda Khan Date: Wed, 24 Jun 2026 18:29:42 +0530 Subject: [PATCH] clear response body length after gzip/deflate decode --- core/src/main/java/feign/DefaultClient.java | 3 + .../java/feign/client/DefaultClientTest.java | 48 ++++++++++ .../java/feign/http2client/Http2Client.java | 5 +- .../Http2ClientContentLengthTest.java | 93 +++++++++++++++++-- 4 files changed, 139 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/feign/DefaultClient.java b/core/src/main/java/feign/DefaultClient.java index d8e02fb4ad..509e66196b 100644 --- a/core/src/main/java/feign/DefaultClient.java +++ b/core/src/main/java/feign/DefaultClient.java @@ -122,8 +122,11 @@ Response convertResponse(HttpURLConnection connection, Request request) throws I } if (stream != null && this.isGzip(headers.get(CONTENT_ENCODING))) { stream = new GZIPInputStream(stream); + // the body is now decompressed, the Content-Length described the compressed bytes + length = null; } else if (stream != null && this.isDeflate(headers.get(CONTENT_ENCODING))) { stream = new InflaterInputStream(stream); + length = null; } return Response.builder() .status(status) diff --git a/core/src/test/java/feign/client/DefaultClientTest.java b/core/src/test/java/feign/client/DefaultClientTest.java index c61b1b0a4d..8b0ab4edbd 100644 --- a/core/src/test/java/feign/client/DefaultClientTest.java +++ b/core/src/test/java/feign/client/DefaultClientTest.java @@ -24,13 +24,25 @@ import feign.DefaultClient; import feign.Feign; import feign.Feign.Builder; +import feign.Request; +import feign.Request.HttpMethod; +import feign.Request.Options; +import feign.Response; import feign.RetryableException; +import feign.Util; import feign.assertj.MockWebServerAssertions; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.*; +import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.zip.GZIPOutputStream; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.SocketPolicy; +import okio.Buffer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; @@ -58,6 +70,42 @@ void retriesFailedHandshake() throws IOException, InterruptedException { assertThat(server.getRequestCount()).isEqualTo(2); } + @Test + void gzipDecodedBodyReportsUnknownLength() throws Exception { + // Accept-Encoding is set explicitly so HttpURLConnection leaves the gzip body for the client + // to decode, exercising DefaultClient's own decompression path. + server.enqueue( + new MockResponse() + .addHeader("Content-Encoding", "gzip") + .setBody(new Buffer().write(gzip("Compressed Data")))); + + Map> headers = new LinkedHashMap<>(); + headers.put("Accept-Encoding", Collections.singletonList("gzip")); + Request request = + Request.create( + HttpMethod.GET, + "http://localhost:" + server.getPort(), + headers, + null, + StandardCharsets.UTF_8, + null); + + Response response = new DefaultClient(null, null, false).execute(request, new Options()); + + // the body is decompressed, so the compressed Content-Length must not be reported as the length + assertThat(response.body().length()).isNull(); + assertThat(Util.toString(response.body().asReader(StandardCharsets.UTF_8))) + .isEqualTo("Compressed Data"); + } + + private static byte[] gzip(String data) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) { + gzip.write(data.getBytes(StandardCharsets.UTF_8)); + } + return bos.toByteArray(); + } + @Test void canOverrideSSLSocketFactory() throws IOException, InterruptedException { server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index 9eb718df97..cfe30c28b0 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -130,7 +130,7 @@ public CompletableFuture execute( protected Response toFeignResponse(Request request, HttpResponse httpResponse) { final OptionalLong length = httpResponse.headers().firstValueAsLong("Content-Length"); - final Integer contentLength = + Integer contentLength = length.isPresent() && length.getAsLong() >= 0 && length.getAsLong() <= Integer.MAX_VALUE ? (int) length.getAsLong() : null; @@ -140,10 +140,13 @@ protected Response toFeignResponse(Request request, HttpResponse ht if (httpResponse.headers().allValues(CONTENT_ENCODING).contains(ENCODING_GZIP)) { try { body = new GZIPInputStream(body); + // the body is now decompressed, the Content-Length described the compressed bytes + contentLength = null; } catch (IOException ignored) { } } else if (httpResponse.headers().allValues(CONTENT_ENCODING).contains(ENCODING_DEFLATE)) { body = new InflaterInputStream(body); + contentLength = null; } return Response.builder() diff --git a/java11/src/test/java/feign/http2client/Http2ClientContentLengthTest.java b/java11/src/test/java/feign/http2client/Http2ClientContentLengthTest.java index ff6ed83fa5..bb39705951 100644 --- a/java11/src/test/java/feign/http2client/Http2ClientContentLengthTest.java +++ b/java11/src/test/java/feign/http2client/Http2ClientContentLengthTest.java @@ -20,7 +20,9 @@ import feign.Request; import feign.Request.HttpMethod; import feign.Response; +import feign.Util; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URI; import java.net.http.HttpClient.Version; @@ -32,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.zip.GZIPOutputStream; import javax.net.ssl.SSLSession; import org.junit.jupiter.api.Test; @@ -83,16 +86,76 @@ public Version version() { }; } + private static HttpResponse gzipResponseWithContentLength(byte[] compressedBody) { + final HttpHeaders headers = + HttpHeaders.of( + Map.of( + "Content-Length", List.of(String.valueOf(compressedBody.length)), + "Content-Encoding", List.of("gzip")), + (name, value) -> true); + return new HttpResponse<>() { + @Override + public int statusCode() { + return 200; + } + + @Override + public HttpRequest request() { + return null; + } + + @Override + public Optional> previousResponse() { + return Optional.empty(); + } + + @Override + public HttpHeaders headers() { + return headers; + } + + @Override + public InputStream body() { + return new ByteArrayInputStream(compressedBody); + } + + @Override + public Optional sslSession() { + return Optional.empty(); + } + + @Override + public URI uri() { + return URI.create("http://localhost"); + } + + @Override + public Version version() { + return Version.HTTP_2; + } + }; + } + + private static byte[] gzip(String data) throws Exception { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) { + gzip.write(data.getBytes(StandardCharsets.UTF_8)); + } + return bos.toByteArray(); + } + + private static Request request() { + return Request.create( + HttpMethod.GET, + "http://localhost", + Collections.emptyMap(), + null, + StandardCharsets.UTF_8, + null); + } + private static Response decode(String contentLength) { - final Request request = - Request.create( - HttpMethod.GET, - "http://localhost", - Collections.emptyMap(), - null, - StandardCharsets.UTF_8, - null); - return new Http2Client().toFeignResponse(request, responseWithContentLength(contentLength)); + return new Http2Client().toFeignResponse(request(), responseWithContentLength(contentLength)); } @Test @@ -110,4 +173,16 @@ void negativeContentLengthIsReportedAsUnknown() { void contentLengthWithinIntRangeIsPreserved() { assertThat(decode("1024").body().length()).isEqualTo(1024); } + + @Test + void gzipDecodedBodyReportsUnknownLength() throws Exception { + final byte[] compressed = gzip("Compressed Data"); + final Response response = + new Http2Client().toFeignResponse(request(), gzipResponseWithContentLength(compressed)); + // the body is transparently decompressed, so the compressed Content-Length is not the body + // length and must be reported as unknown + assertThat(response.body().length()).isNull(); + assertThat(Util.toString(response.body().asReader(StandardCharsets.UTF_8))) + .isEqualTo("Compressed Data"); + } }