Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/src/main/java/feign/DefaultClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 48 additions & 0 deletions core/src/test/java/feign/client/DefaultClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<String, Collection<String>> 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);
Expand Down
5 changes: 4 additions & 1 deletion java11/src/main/java/feign/http2client/Http2Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public CompletableFuture<Response> execute(

protected Response toFeignResponse(Request request, HttpResponse<InputStream> 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;
Expand All @@ -140,10 +140,13 @@ protected Response toFeignResponse(Request request, HttpResponse<InputStream> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -83,16 +86,76 @@ public Version version() {
};
}

private static HttpResponse<InputStream> 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<HttpResponse<InputStream>> previousResponse() {
return Optional.empty();
}

@Override
public HttpHeaders headers() {
return headers;
}

@Override
public InputStream body() {
return new ByteArrayInputStream(compressedBody);
}

@Override
public Optional<SSLSession> 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
Expand All @@ -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");
}
}