From 0dd978275bdec6fb0e307bcaf23977e15ac742a5 Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Wed, 28 Aug 2013 19:39:51 -0400 Subject: [PATCH] default client: add support for gzip-encoded request bodies (#52) Enhances the default client to GZIP-encode request bodies when the appropriate content-encoding header is set in the interface's method definition. https://github.com/Netflix/feign/issues/52 --- CHANGES.md | 4 +++ core/src/main/java/feign/Client.java | 17 ++++++++-- core/src/main/java/feign/Util.java | 8 +++++ core/src/test/java/feign/FeignTest.java | 31 ++++++++++++++++- core/src/test/java/feign/GZIPStreams.java | 41 +++++++++++++++++++++++ 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 core/src/test/java/feign/GZIPStreams.java diff --git a/CHANGES.md b/CHANGES.md index d5aa4c39..6a030b02 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +### Version 4.4 +* Support overriding default HostnameVerifier +* Support GZIP content encoding for request bodies + ### Version 4.3 * Add ability to configure zero or more RequestInterceptors. * Remove `overrides = true` on codec modules. diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java index 324e1290..be6a0ba1 100644 --- a/core/src/main/java/feign/Client.java +++ b/core/src/main/java/feign/Client.java @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.zip.GZIPOutputStream; import javax.inject.Inject; import javax.net.ssl.HostnameVerifier; @@ -35,7 +36,9 @@ import javax.net.ssl.SSLSocketFactory; import dagger.Lazy; import feign.Request.Options; +import static feign.Util.CONTENT_ENCODING; import static feign.Util.CONTENT_LENGTH; +import static feign.Util.ENCODING_GZIP; import static feign.Util.UTF_8; /** @@ -81,13 +84,20 @@ public interface Client { connection.setInstanceFollowRedirects(true); connection.setRequestMethod(request.method()); + Collection contentEncodingValues = request.headers().get(CONTENT_ENCODING); + boolean gzipEncodedRequest = contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP); + Integer contentLength = null; for (String field : request.headers().keySet()) { for (String value : request.headers().get(field)) { if (field.equals(CONTENT_LENGTH)) { - contentLength = Integer.valueOf(value); + if (!gzipEncodedRequest) { + contentLength = Integer.valueOf(value); + connection.addRequestProperty(field, value); + } + } else { + connection.addRequestProperty(field, value); } - connection.addRequestProperty(field, value); } } @@ -99,6 +109,9 @@ public interface Client { } connection.setDoOutput(true); OutputStream out = connection.getOutputStream(); + if (gzipEncodedRequest) { + out = new GZIPOutputStream(out); + } try { out.write(request.body().getBytes(UTF_8)); } finally { diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java index eceb6139..c251c31b 100644 --- a/core/src/main/java/feign/Util.java +++ b/core/src/main/java/feign/Util.java @@ -39,10 +39,18 @@ public class Util { * The HTTP Content-Length header field name. */ public static final String CONTENT_LENGTH = "Content-Length"; + /** + * The HTTP Content-Encoding header field name. + */ + public static final String CONTENT_ENCODING = "Content-Encoding"; /** * The HTTP Retry-After header field name. */ public static final String RETRY_AFTER = "Retry-After"; + /** + * Value for the Content-Encoding header that indicates that GZIP encoding is in use. + */ + public static final String ENCODING_GZIP = "gzip"; // com.google.common.base.Charsets /** diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 0512e3ac..224cb65e 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -16,6 +16,8 @@ package feign; import com.google.common.base.Joiner; +import com.google.common.io.ByteStreams; +import com.google.common.io.CharStreams; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; @@ -47,7 +49,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import static dagger.Provides.Type.SET; +import static feign.Util.UTF_8; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -80,6 +84,8 @@ public class FeignTest { @RequestLine("POST /") void body(List contents); + @RequestLine("POST /") @Headers("Content-Encoding: gzip") void gzipBody(List contents); + @RequestLine("POST /") void form( @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password); @@ -310,7 +316,30 @@ public class FeignTest { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.body(Arrays.asList("netflix", "denominator", "password")); - assertEquals(new String(server.takeRequest().getBody()), "[netflix, denominator, password]"); + RecordedRequest request = server.takeRequest(); + assertEquals(request.getHeader("Content-Length"), "32"); + assertEquals(new String(request.getBody()), "[netflix, denominator, password]"); + } finally { + server.shutdown(); + } + } + + @Test + public void postGZIPEncodedBodyParam() throws IOException, InterruptedException { + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setResponseCode(200).setBody("foo")); + server.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + + api.gzipBody(Arrays.asList("netflix", "denominator", "password")); + RecordedRequest request = server.takeRequest(); + assertNull(request.getHeader("Content-Length")); + byte[] compressedBody = request.getBody(); + String uncompressedBody = CharStreams.toString(CharStreams.newReaderSupplier( + GZIPStreams.newInputStreamSupplier(ByteStreams.newInputStreamSupplier(compressedBody)), UTF_8)); + assertEquals(uncompressedBody, "[netflix, denominator, password]"); } finally { server.shutdown(); } diff --git a/core/src/test/java/feign/GZIPStreams.java b/core/src/test/java/feign/GZIPStreams.java new file mode 100644 index 00000000..42b28868 --- /dev/null +++ b/core/src/test/java/feign/GZIPStreams.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign; + +import com.google.common.io.InputSupplier; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +class GZIPStreams { + static InputSupplier newInputStreamSupplier(InputSupplier supplier) { + return new GZIPInputStreamSupplier(supplier); + } + + private static class GZIPInputStreamSupplier implements InputSupplier { + private final InputSupplier supplier; + + GZIPInputStreamSupplier(InputSupplier supplier) { + this.supplier = supplier; + } + + @Override + public GZIPInputStream getInput() throws IOException { + return new GZIPInputStream(supplier.getInput()); + } + } +}