Skip to content

Commit f2e357b

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 3b459b5 commit f2e357b

File tree

4 files changed

+133
-6
lines changed

4 files changed

+133
-6
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.hc.client5.http.classic.HttpClient;
3636
import org.apache.hc.client5.http.config.Configurable;
3737
import org.apache.hc.client5.http.config.RequestConfig;
38+
import org.apache.hc.client5.http.entity.GzipCompressingEntity;
3839
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
3940
import org.apache.hc.client5.http.protocol.HttpClientContext;
4041
import org.apache.hc.core5.http.ClassicHttpRequest;
@@ -122,6 +123,7 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options
122123

123124
// request headers
124125
boolean hasAcceptHeader = false;
126+
boolean isGzip = false;
125127
for (final Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
126128
final String headerName = headerEntry.getKey();
127129
if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
@@ -133,7 +135,16 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options
133135
// doesn't like us to set it as well.
134136
continue;
135137
}
136-
138+
if (headerName.equalsIgnoreCase(Util.CONTENT_ENCODING)) {
139+
isGzip = headerEntry.getValue().stream().anyMatch(Util.ENCODING_GZIP::equalsIgnoreCase);
140+
boolean isDeflate =
141+
headerEntry.getValue().stream().anyMatch(Util.ENCODING_DEFLATE::equalsIgnoreCase);
142+
if (isDeflate) {
143+
// DeflateCompressingEntity not available in hc5 yet
144+
throw new IllegalArgumentException(
145+
"Deflate Content-Encoding is not supported by feign-hc5");
146+
}
147+
}
137148
for (final String headerValue : headerEntry.getValue()) {
138149
requestBuilder.addHeader(headerName, headerValue);
139150
}
@@ -160,7 +171,9 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options
160171
}
161172
entity = new StringEntity(content, contentType);
162173
}
163-
174+
if (isGzip) {
175+
entity = new GzipCompressingEntity(entity);
176+
}
164177
requestBuilder.setEntity(entity);
165178
} else {
166179
requestBuilder.setEntity(new ByteArrayEntity(new byte[0], null));

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717

1818
import feign.*;
1919
import feign.Request.Options;
20+
import java.io.ByteArrayOutputStream;
21+
import java.io.IOException;
2022
import java.util.*;
2123
import java.util.concurrent.CompletableFuture;
24+
import java.util.zip.GZIPOutputStream;
2225
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
2326
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
2427
import org.apache.hc.client5.http.config.Configurable;
@@ -115,6 +118,7 @@ SimpleHttpRequest toClassicHttpRequest(Request request, Request.Options options)
115118

116119
// request headers
117120
boolean hasAcceptHeader = false;
121+
boolean isGzip = false;
118122
for (final Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
119123
final String headerName = headerEntry.getKey();
120124
if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
@@ -126,6 +130,16 @@ SimpleHttpRequest toClassicHttpRequest(Request request, Request.Options options)
126130
// doesn't like us to set it as well.
127131
continue;
128132
}
133+
if (headerName.equalsIgnoreCase(Util.CONTENT_ENCODING)) {
134+
isGzip = headerEntry.getValue().stream().anyMatch(Util.ENCODING_GZIP::equalsIgnoreCase);
135+
boolean isDeflate =
136+
headerEntry.getValue().stream().anyMatch(Util.ENCODING_DEFLATE::equalsIgnoreCase);
137+
if (isDeflate) {
138+
// DeflateCompressingEntity not available in hc5 yet
139+
throw new IllegalArgumentException(
140+
"Deflate Content-Encoding is not supported by feign-hc5");
141+
}
142+
}
129143

130144
for (final String headerValue : headerEntry.getValue()) {
131145
httpRequest.addHeader(headerName, headerValue);
@@ -138,7 +152,17 @@ SimpleHttpRequest toClassicHttpRequest(Request request, Request.Options options)
138152

139153
// request body
140154
// final Body requestBody = request.requestBody();
141-
final byte[] data = request.body();
155+
byte[] data = request.body();
156+
if (isGzip && data != null && data.length > 0) {
157+
// compress if needed
158+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
159+
GZIPOutputStream gzipOs = new GZIPOutputStream(baos, true)) {
160+
gzipOs.write(data);
161+
gzipOs.flush();
162+
data = baos.toByteArray();
163+
} catch (IOException suppressed) { // NOPMD
164+
}
165+
}
142166
if (data != null) {
143167
httpRequest.setBody(data, getContentType(request));
144168
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ public void headerMapWithHeaderAnnotations() throws Exception {
255255
// header map should be additive for headers provided by annotations
256256
assertThat(server.takeRequest())
257257
.hasHeaders(
258-
entry("Content-Encoding", Arrays.asList("deflate")),
258+
entry("Content-Encoding", Arrays.asList("gzip")),
259259
entry("Custom-Header", Arrays.asList("fooValue")));
260260

261261
server.enqueue(new MockResponse());
@@ -269,7 +269,7 @@ public void headerMapWithHeaderAnnotations() throws Exception {
269269
*/
270270
assertThat(server.takeRequest())
271271
.hasHeaders(
272-
entry("Content-Encoding", Arrays.asList("deflate", "overrideFromMap")),
272+
entry("Content-Encoding", Arrays.asList("gzip", "overrideFromMap")),
273273
entry("Custom-Header", Arrays.asList("fooValue")));
274274

275275
checkCFCompletedSoon(cf);
@@ -952,7 +952,7 @@ CompletableFuture<Void> expandArray(
952952
CompletableFuture<Void> headerMap(@HeaderMap Map<String, Object> headerMap);
953953

954954
@RequestLine("GET /")
955-
@Headers("Content-Encoding: deflate")
955+
@Headers("Content-Encoding: gzip")
956956
CompletableFuture<Void> headerMapWithHeaderAnnotations(
957957
@HeaderMap Map<String, Object> headerMap);
958958

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 feign.Feign;
20+
import feign.Feign.Builder;
21+
import feign.RequestLine;
22+
import feign.client.AbstractClientTest;
23+
import java.io.ByteArrayInputStream;
24+
import java.io.IOException;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.zip.GZIPInputStream;
27+
import okhttp3.mockwebserver.MockResponse;
28+
import okhttp3.mockwebserver.RecordedRequest;
29+
import org.junit.jupiter.api.Test;
30+
31+
/** Tests that 'Content-Encoding: gzip' is handled correctly */
32+
public class GzipHttp5ClientTest extends AbstractClientTest {
33+
34+
@Override
35+
public Builder newBuilder() {
36+
return Feign.builder().client(new ApacheHttp5Client());
37+
}
38+
39+
@Test
40+
public void testWithCompressedBody() throws InterruptedException, IOException {
41+
final TestInterface testInterface = buildTestInterface(true);
42+
43+
server.enqueue(new MockResponse().setBody("foo"));
44+
45+
assertEquals("foo", testInterface.withBody("bar"));
46+
final RecordedRequest request1 = server.takeRequest();
47+
assertEquals("/test", request1.getPath());
48+
49+
ByteArrayInputStream bodyContentIs =
50+
new ByteArrayInputStream(request1.getBody().readByteArray());
51+
byte[] uncompressed = new GZIPInputStream(bodyContentIs).readAllBytes();
52+
53+
assertEquals("bar", new String(uncompressed, StandardCharsets.UTF_8));
54+
}
55+
56+
@Test
57+
public void testWithUncompressedBody() throws InterruptedException, IOException {
58+
final TestInterface testInterface = buildTestInterface(false);
59+
60+
server.enqueue(new MockResponse().setBody("foo"));
61+
62+
assertEquals("foo", testInterface.withBody("bar"));
63+
final RecordedRequest request1 = server.takeRequest();
64+
assertEquals("/test", request1.getPath());
65+
66+
assertEquals("bar", request1.getBody().readString(StandardCharsets.UTF_8));
67+
}
68+
69+
private TestInterface buildTestInterface(boolean compress) {
70+
return newBuilder()
71+
.requestInterceptor(req -> req.header("Content-Encoding", compress ? "gzip" : ""))
72+
.target(TestInterface.class, "http://localhost:" + server.getPort());
73+
}
74+
75+
@Override
76+
public void testVeryLongResponseNullLength() {
77+
assumeTrue("HC5 client seems to hang with response size equalto Long.MAX", false);
78+
}
79+
80+
@Override
81+
public void testContentTypeDefaultsToRequestCharset() throws Exception {
82+
assumeTrue("this test is flaky on windows, but works fine.", false);
83+
}
84+
85+
public interface TestInterface {
86+
87+
@RequestLine("POST /test")
88+
String withBody(String body);
89+
}
90+
}

0 commit comments

Comments
 (0)