Skip to content

Commit 71604b5

Browse files
authored
Add gzip and deflate compression support in Http2Client (#2738)
* Enable Http2Client tests * Fix Http2Client timeoutTest * Override some AbstractClientTests as Http2Client does not expose reason phrase * Add gzip and deflate compression support in Http2Client
1 parent bc45d6f commit 71604b5

File tree

3 files changed

+193
-20
lines changed

3 files changed

+193
-20
lines changed

core/src/test/java/feign/client/AbstractClientTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public void reasonPhraseIsOptional() throws IOException, InterruptedException {
127127
}
128128

129129
@Test
130-
void parsesErrorResponse() {
130+
public void parsesErrorResponse() {
131131

132132
server.enqueue(new MockResponse().setResponseCode(500).setBody("ARGHH"));
133133

@@ -244,7 +244,7 @@ public void noResponseBodyForPatch() {
244244
}
245245

246246
@Test
247-
void parsesResponseMissingLength() throws IOException {
247+
public void parsesResponseMissingLength() throws IOException {
248248
server.enqueue(new MockResponse().setChunkedBody("foo", 1));
249249

250250
TestInterface api =
@@ -332,7 +332,7 @@ public void contentTypeDefaultsToRequestCharset() throws Exception {
332332
}
333333

334334
@Test
335-
void defaultCollectionFormat() throws Exception {
335+
public void defaultCollectionFormat() throws Exception {
336336
server.enqueue(new MockResponse().setBody("body"));
337337

338338
TestInterface api =
@@ -349,7 +349,7 @@ void defaultCollectionFormat() throws Exception {
349349
}
350350

351351
@Test
352-
void headersWithNullParams() throws InterruptedException {
352+
public void headersWithNullParams() throws InterruptedException {
353353
server.enqueue(new MockResponse().setBody("body"));
354354

355355
TestInterface api =
@@ -367,7 +367,7 @@ void headersWithNullParams() throws InterruptedException {
367367
}
368368

369369
@Test
370-
void headersWithNotEmptyParams() throws InterruptedException {
370+
public void headersWithNotEmptyParams() throws InterruptedException {
371371
server.enqueue(new MockResponse().setBody("body"));
372372

373373
TestInterface api =
@@ -385,7 +385,7 @@ void headersWithNotEmptyParams() throws InterruptedException {
385385
}
386386

387387
@Test
388-
void alternativeCollectionFormat() throws Exception {
388+
public void alternativeCollectionFormat() throws Exception {
389389
server.enqueue(new MockResponse().setBody("body"));
390390

391391
TestInterface api =

java11/src/main/java/feign/http2client/Http2Client.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package feign.http2client;
1717

18-
import static feign.Util.enumForName;
18+
import static feign.Util.*;
1919

2020
import feign.AsyncClient;
2121
import feign.Client;
@@ -54,6 +54,8 @@
5454
import java.util.concurrent.ConcurrentHashMap;
5555
import java.util.function.Function;
5656
import java.util.stream.Collectors;
57+
import java.util.zip.GZIPInputStream;
58+
import java.util.zip.InflaterInputStream;
5759

5860
public class Http2Client implements Client, AsyncClient<Object> {
5961

@@ -129,9 +131,20 @@ public CompletableFuture<Response> execute(
129131
protected Response toFeignResponse(Request request, HttpResponse<InputStream> httpResponse) {
130132
final OptionalLong length = httpResponse.headers().firstValueAsLong("Content-Length");
131133

134+
InputStream body = httpResponse.body();
135+
136+
if (httpResponse.headers().allValues(CONTENT_ENCODING).contains(ENCODING_GZIP)) {
137+
try {
138+
body = new GZIPInputStream(body);
139+
} catch (IOException ignored) {
140+
}
141+
} else if (httpResponse.headers().allValues(CONTENT_ENCODING).contains(ENCODING_DEFLATE)) {
142+
body = new InflaterInputStream(body);
143+
}
144+
132145
return Response.builder()
133146
.protocolVersion(enumForName(ProtocolVersion.class, httpResponse.version()))
134-
.body(httpResponse.body(), length.isPresent() ? (int) length.getAsLong() : null)
147+
.body(body, length.isPresent() ? (int) length.getAsLong() : null)
135148
.reason(httpResponse.headers().firstValue("Reason-Phrase").orElse(null))
136149
.request(request)
137150
.status(httpResponse.statusCode())

java11/src/test/java/feign/http2client/test/Http2ClientTest.java

Lines changed: 172 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,27 @@
1515
*/
1616
package feign.http2client.test;
1717

18+
import static feign.Util.UTF_8;
1819
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.Assertions.entry;
1921
import static org.junit.jupiter.api.Assertions.assertThrows;
2022

21-
import feign.Body;
22-
import feign.Feign;
23-
import feign.FeignException;
24-
import feign.Headers;
25-
import feign.Request;
26-
import feign.RequestLine;
27-
import feign.Response;
28-
import feign.Retryer;
23+
import feign.*;
24+
import feign.assertj.MockWebServerAssertions;
2925
import feign.client.AbstractClientTest;
3026
import feign.http2client.Http2Client;
27+
import java.io.ByteArrayInputStream;
3128
import java.io.IOException;
3229
import java.net.http.HttpTimeoutException;
30+
import java.util.Arrays;
31+
import java.util.Collections;
32+
import java.util.List;
3333
import java.util.concurrent.TimeUnit;
3434
import okhttp3.mockwebserver.MockResponse;
35-
import org.junit.jupiter.api.Disabled;
35+
import okhttp3.mockwebserver.RecordedRequest;
3636
import org.junit.jupiter.api.Test;
3737

3838
/** Tests client-specific behavior, such as ensuring Content-Length is sent when specified. */
39-
@Disabled
4039
public class Http2ClientTest extends AbstractClientTest {
4140

4241
public interface TestInterface {
@@ -59,6 +58,24 @@ public interface TestInterface {
5958
@RequestLine("DELETE /anything")
6059
@Body("some request body")
6160
String deleteWithBody();
61+
62+
@RequestLine("POST /?foo=bar&foo=baz&qux=")
63+
@Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"})
64+
Response post(String body);
65+
66+
@RequestLine("GET /")
67+
@Headers("Accept: text/plain")
68+
String get();
69+
70+
@RequestLine("GET /?foo={multiFoo}")
71+
Response get(@Param("multiFoo") List<String> multiFoo);
72+
73+
@Headers({"Authorization: {authorization}"})
74+
@RequestLine("GET /")
75+
Response getWithHeaders(@Param("authorization") String authorization);
76+
77+
@RequestLine(value = "GET /?foo={multiFoo}", collectionFormat = CollectionFormat.CSV)
78+
Response getCSV(@Param("multiFoo") List<String> multiFoo);
6279
}
6380

6481
@Override
@@ -117,12 +134,13 @@ public void veryLongResponseNullLength() {
117134

118135
@Test
119136
void timeoutTest() {
120-
server.enqueue(new MockResponse().setBody("foo").setBodyDelay(30, TimeUnit.SECONDS));
137+
server.enqueue(new MockResponse().setBody("foo").setHeadersDelay(1, TimeUnit.SECONDS));
121138

122139
final TestInterface api =
123140
newBuilder()
124141
.retryer(Retryer.NEVER_RETRY)
125-
.options(new Request.Options(1, TimeUnit.SECONDS, 1, TimeUnit.SECONDS, true))
142+
.options(
143+
new Request.Options(500, TimeUnit.MILLISECONDS, 500, TimeUnit.MILLISECONDS, true))
126144
.target(TestInterface.class, server.url("/").toString());
127145

128146
FeignException exception = assertThrows(FeignException.class, () -> api.timeout());
@@ -145,6 +163,148 @@ void deleteWithRequestBody() {
145163
assertThat(result).contains("\"data\": \"some request body\"");
146164
}
147165

166+
@Override
167+
@Test
168+
public void parsesResponseMissingLength() throws IOException {
169+
server.enqueue(new MockResponse().setChunkedBody("foo", 1));
170+
171+
TestInterface api =
172+
newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort());
173+
174+
Response response = api.post("testing");
175+
assertThat(response.status()).isEqualTo(200);
176+
// assertThat(response.reason()).isEqualTo("OK");
177+
assertThat(response.body().length()).isNull();
178+
assertThat(response.body().asInputStream())
179+
.hasSameContentAs(new ByteArrayInputStream("foo".getBytes(UTF_8)));
180+
}
181+
182+
@Override
183+
@Test
184+
public void parsesErrorResponse() {
185+
186+
server.enqueue(new MockResponse().setResponseCode(500).setBody("ARGHH"));
187+
188+
TestInterface api =
189+
newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort());
190+
191+
Throwable exception = assertThrows(FeignException.class, () -> api.get());
192+
assertThat(exception.getMessage())
193+
.contains(
194+
"[500] during [GET] to [http://localhost:"
195+
+ server.getPort()
196+
+ "/] [TestInterface#get()]: [ARGHH]");
197+
}
198+
199+
@Override
200+
@Test
201+
public void defaultCollectionFormat() throws Exception {
202+
server.enqueue(new MockResponse().setBody("body"));
203+
204+
TestInterface api =
205+
newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort());
206+
207+
Response response = api.get(Arrays.asList("bar", "baz"));
208+
209+
assertThat(response.status()).isEqualTo(200);
210+
// assertThat(response.reason()).isEqualTo("OK");
211+
212+
MockWebServerAssertions.assertThat(server.takeRequest())
213+
.hasMethod("GET")
214+
.hasPath("/?foo=bar&foo=baz");
215+
}
216+
217+
@Override
218+
@Test
219+
public void headersWithNotEmptyParams() throws InterruptedException {
220+
server.enqueue(new MockResponse().setBody("body"));
221+
222+
TestInterface api =
223+
newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort());
224+
225+
Response response = api.getWithHeaders("token");
226+
227+
assertThat(response.status()).isEqualTo(200);
228+
// assertThat(response.reason()).isEqualTo("OK");
229+
230+
MockWebServerAssertions.assertThat(server.takeRequest())
231+
.hasMethod("GET")
232+
.hasPath("/")
233+
.hasHeaders(entry("authorization", Collections.singletonList("token")));
234+
}
235+
236+
@Override
237+
@Test
238+
public void headersWithNullParams() throws InterruptedException {
239+
server.enqueue(new MockResponse().setBody("body"));
240+
241+
TestInterface api =
242+
newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort());
243+
244+
Response response = api.getWithHeaders(null);
245+
246+
assertThat(response.status()).isEqualTo(200);
247+
// assertThat(response.reason()).isEqualTo("OK");
248+
249+
MockWebServerAssertions.assertThat(server.takeRequest())
250+
.hasMethod("GET")
251+
.hasPath("/")
252+
.hasNoHeaderNamed("Authorization");
253+
}
254+
255+
@Test
256+
public void alternativeCollectionFormat() throws Exception {
257+
server.enqueue(new MockResponse().setBody("body"));
258+
259+
TestInterface api =
260+
newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort());
261+
262+
Response response = api.getCSV(Arrays.asList("bar", "baz"));
263+
264+
assertThat(response.status()).isEqualTo(200);
265+
// assertThat(response.reason()).isEqualTo("OK");
266+
267+
// Some HTTP libraries percent-encode commas in query parameters and others
268+
// don't.
269+
MockWebServerAssertions.assertThat(server.takeRequest())
270+
.hasMethod("GET")
271+
.hasOneOfPath("/?foo=bar,baz", "/?foo=bar%2Cbaz");
272+
}
273+
274+
@Override
275+
@Test
276+
public void parsesRequestAndResponse() throws IOException, InterruptedException {
277+
server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar"));
278+
279+
TestInterface api =
280+
newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort());
281+
282+
Response response = api.post("foo");
283+
284+
assertThat(response.status()).isEqualTo(200);
285+
// assertThat(response.reason()).isEqualTo("OK");
286+
assertThat(response.headers())
287+
.hasEntrySatisfying(
288+
"Content-Length",
289+
value -> {
290+
assertThat(value).contains("3");
291+
})
292+
.hasEntrySatisfying(
293+
"Foo",
294+
value -> {
295+
assertThat(value).contains("Bar");
296+
});
297+
assertThat(response.body().asInputStream())
298+
.hasSameContentAs(new ByteArrayInputStream("foo".getBytes(UTF_8)));
299+
300+
RecordedRequest recordedRequest = server.takeRequest();
301+
assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("POST");
302+
assertThat(recordedRequest.getHeader("Foo")).isEqualToIgnoringCase("Bar, Baz");
303+
assertThat(recordedRequest.getHeader("Accept")).isEqualToIgnoringCase("*/*");
304+
assertThat(recordedRequest.getHeader("Content-Length")).isEqualToIgnoringCase("3");
305+
assertThat(recordedRequest.getBody().readUtf8()).isEqualToIgnoringCase("foo");
306+
}
307+
148308
@Override
149309
public Feign.Builder newBuilder() {
150310
return Feign.builder().client(new Http2Client());

0 commit comments

Comments
 (0)