Skip to content

Commit 893cb1e

Browse files
fbusfabienbussvelo
authored
Add support for request body gzip Content-Encoding (#2247)
* Add support for request body gzip Content-Encoding * do not use 'deflate' in unit test. It is not supported for requests --------- Co-authored-by: Fabien Bussiron <[email protected]> Co-authored-by: Marvin <[email protected]>
1 parent 63820a9 commit 893cb1e

File tree

4 files changed

+137
-7
lines changed

4 files changed

+137
-7
lines changed

hc5/src/main/java/feign/hc5/ApacheHttp5Client.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.hc.client5.http.classic.HttpClient;
3535
import org.apache.hc.client5.http.config.Configurable;
3636
import org.apache.hc.client5.http.config.RequestConfig;
37+
import org.apache.hc.client5.http.entity.GzipCompressingEntity;
3738
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
3839
import org.apache.hc.client5.http.protocol.HttpClientContext;
3940
import org.apache.hc.core5.http.ClassicHttpRequest;
@@ -121,6 +122,7 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options
121122

122123
// request headers
123124
boolean hasAcceptHeader = false;
125+
boolean isGzip = false;
124126
for (final Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
125127
final String headerName = headerEntry.getKey();
126128
if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
@@ -132,7 +134,14 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options
132134
// doesn't like us to set it as well.
133135
continue;
134136
}
135-
137+
if (headerName.equalsIgnoreCase(Util.CONTENT_ENCODING)) {
138+
isGzip = headerEntry.getValue().stream().anyMatch(Util.ENCODING_GZIP::equalsIgnoreCase);
139+
boolean isDeflate = headerEntry.getValue().stream().anyMatch(Util.ENCODING_DEFLATE::equalsIgnoreCase);
140+
if (isDeflate) {
141+
// DeflateCompressingEntity not available in hc5 yet
142+
throw new IllegalArgumentException("Deflate Content-Encoding is not supported by feign-hc5");
143+
}
144+
}
136145
for (final String headerValue : headerEntry.getValue()) {
137146
requestBuilder.addHeader(headerName, headerValue);
138147
}
@@ -159,7 +168,9 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options
159168
}
160169
entity = new StringEntity(content, contentType);
161170
}
162-
171+
if(isGzip) {
172+
entity = new GzipCompressingEntity(entity);
173+
}
163174
requestBuilder.setEntity(entity);
164175
} else {
165176
requestBuilder.setEntity(new ByteArrayEntity(new byte[0], null));

hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
import org.apache.hc.core5.http.ContentType;
2525
import org.apache.hc.core5.http.Header;
2626
import org.apache.hc.core5.io.CloseMode;
27+
import java.io.ByteArrayOutputStream;
28+
import java.io.IOException;
2729
import java.util.*;
30+
import java.util.zip.GZIPOutputStream;
2831
import java.util.concurrent.CompletableFuture;
2932
import feign.*;
3033
import feign.Request.Options;
@@ -114,6 +117,7 @@ SimpleHttpRequest toClassicHttpRequest(Request request,
114117

115118
// request headers
116119
boolean hasAcceptHeader = false;
120+
boolean isGzip = false;
117121
for (final Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
118122
final String headerName = headerEntry.getKey();
119123
if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
@@ -125,7 +129,15 @@ SimpleHttpRequest toClassicHttpRequest(Request request,
125129
// doesn't like us to set it as well.
126130
continue;
127131
}
128-
132+
if (headerName.equalsIgnoreCase(Util.CONTENT_ENCODING)) {
133+
isGzip = headerEntry.getValue().stream().anyMatch(Util.ENCODING_GZIP::equalsIgnoreCase);
134+
boolean isDeflate = headerEntry.getValue().stream().anyMatch(Util.ENCODING_DEFLATE::equalsIgnoreCase);
135+
if(isDeflate) {
136+
// DeflateCompressingEntity not available in hc5 yet
137+
throw new IllegalArgumentException("Deflate Content-Encoding is not supported by feign-hc5");
138+
}
139+
}
140+
129141
for (final String headerValue : headerEntry.getValue()) {
130142
httpRequest.addHeader(headerName, headerValue);
131143
}
@@ -137,7 +149,17 @@ SimpleHttpRequest toClassicHttpRequest(Request request,
137149

138150
// request body
139151
// final Body requestBody = request.requestBody();
140-
final byte[] data = request.body();
152+
byte[] data = request.body();
153+
if(isGzip && data != null && data.length > 0) {
154+
// compress if needed
155+
try(ByteArrayOutputStream baos = new ByteArrayOutputStream();
156+
GZIPOutputStream gzipOs = new GZIPOutputStream(baos, true)) {
157+
gzipOs.write(data);
158+
gzipOs.flush();
159+
data = baos.toByteArray();
160+
} catch (IOException suppressed) { // NOPMD
161+
}
162+
}
141163
if (data != null) {
142164
httpRequest.setBody(data, getContentType(request));
143165
}

hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ public void headerMapWithHeaderAnnotations() throws Exception {
243243
api.headerMapWithHeaderAnnotations(headerMap);
244244

245245
// header map should be additive for headers provided by annotations
246-
assertThat(server.takeRequest()).hasHeaders(entry("Content-Encoding", Arrays.asList("deflate")),
246+
assertThat(server.takeRequest()).hasHeaders(entry("Content-Encoding", Arrays.asList("gzip")),
247247
entry("Custom-Header", Arrays.asList("fooValue")));
248248

249249
server.enqueue(new MockResponse());
@@ -256,7 +256,7 @@ public void headerMapWithHeaderAnnotations() throws Exception {
256256
* valid to have more than one value for a header.
257257
*/
258258
assertThat(server.takeRequest()).hasHeaders(
259-
entry("Content-Encoding", Arrays.asList("deflate", "overrideFromMap")),
259+
entry("Content-Encoding", Arrays.asList("gzip", "overrideFromMap")),
260260
entry("Custom-Header", Arrays.asList("fooValue")));
261261

262262
checkCFCompletedSoon(cf);
@@ -880,7 +880,7 @@ CompletableFuture<Void> expandArray(@Param(value = "clock",
880880
CompletableFuture<Void> headerMap(@HeaderMap Map<String, Object> headerMap);
881881

882882
@RequestLine("GET /")
883-
@Headers("Content-Encoding: deflate")
883+
@Headers("Content-Encoding: gzip")
884884
CompletableFuture<Void> headerMapWithHeaderAnnotations(@HeaderMap Map<String, Object> headerMap);
885885

886886
@RequestLine("GET /")
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2012-2023 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.hc5;
15+
16+
import static org.junit.Assume.assumeTrue;
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.zip.GZIPInputStream;
23+
24+
import org.junit.jupiter.api.Test;
25+
26+
import feign.Feign;
27+
import feign.Feign.Builder;
28+
import feign.RequestLine;
29+
import feign.client.AbstractClientTest;
30+
import okhttp3.mockwebserver.MockResponse;
31+
import okhttp3.mockwebserver.RecordedRequest;
32+
33+
/**
34+
* Tests that 'Content-Encoding: gzip' is handled correctly
35+
*/
36+
public class GzipHttp5ClientTest extends AbstractClientTest {
37+
38+
@Override
39+
public Builder newBuilder() {
40+
return Feign.builder().client(new ApacheHttp5Client());
41+
}
42+
43+
@Test
44+
public void testWithCompressedBody() throws InterruptedException, IOException {
45+
final TestInterface testInterface = buildTestInterface(true);
46+
47+
server.enqueue(new MockResponse().setBody("foo"));
48+
49+
assertEquals("foo", testInterface.withBody("bar"));
50+
final RecordedRequest request1 = server.takeRequest();
51+
assertEquals("/test", request1.getPath());
52+
53+
ByteArrayInputStream bodyContentIs = new ByteArrayInputStream(request1.getBody().readByteArray());
54+
byte[] uncompressed = new GZIPInputStream(bodyContentIs).readAllBytes();
55+
56+
assertEquals("bar", new String(uncompressed, StandardCharsets.UTF_8));
57+
58+
}
59+
60+
@Test
61+
public void testWithUncompressedBody() throws InterruptedException, IOException {
62+
final TestInterface testInterface = buildTestInterface(false);
63+
64+
server.enqueue(new MockResponse().setBody("foo"));
65+
66+
assertEquals("foo", testInterface.withBody("bar"));
67+
final RecordedRequest request1 = server.takeRequest();
68+
assertEquals("/test", request1.getPath());
69+
70+
assertEquals("bar", request1.getBody().readString(StandardCharsets.UTF_8));
71+
}
72+
73+
74+
private TestInterface buildTestInterface(boolean compress) {
75+
return newBuilder()
76+
.requestInterceptor(req -> req.header("Content-Encoding", compress ? "gzip" : ""))
77+
.target(TestInterface.class, "http://localhost:" + server.getPort());
78+
}
79+
80+
81+
@Override
82+
public void testVeryLongResponseNullLength() {
83+
assumeTrue("HC5 client seems to hang with response size equalto Long.MAX", false);
84+
}
85+
86+
@Override
87+
public void testContentTypeDefaultsToRequestCharset() throws Exception {
88+
assumeTrue("this test is flaky on windows, but works fine.", false);
89+
}
90+
91+
public interface TestInterface {
92+
93+
@RequestLine("POST /test")
94+
String withBody(String body);
95+
96+
}
97+
}

0 commit comments

Comments
 (0)