diff --git a/CHANGES.md b/CHANGES.md
index 00f5e219f8..88225348c0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,6 @@
+### Version 6.0
+* Support binary request and response bodies.
+
### Version 5.4.0
* Add `BasicAuthRequestInterceptor`
* Add Jackson integration
diff --git a/README.md b/README.md
index e2a56ef1ed..7339c831fd 100644
--- a/README.md
+++ b/README.md
@@ -144,9 +144,9 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod
### Decoders
`Feign.builder()` allows you to specify additional configuration such as how to decode a response.
-If any methods in your interface return types besides `Response`, `String` or `void`, you'll need to configure a `Decoder`.
+If any methods in your interface return types besides `Response`, `String`, `byte[]` or `void`, you'll need to configure a non-default `Decoder`.
-Here's how to configure json decoding (using the `feign-gson` extension):
+Here's how to configure JSON decoding (using the `feign-gson` extension):
```java
GitHub github = Feign.builder()
@@ -154,6 +154,19 @@ GitHub github = Feign.builder()
.target(GitHub.class, "https://api.github.com");
```
+### Encoders
+`Feign.builder()` allows you to specify additional configuration such as how to encode a request.
+
+If any methods in your interface use parameters types besides `String` or `byte[]`, you'll need to configure a non-default `Encoder`.
+
+Here's how to configure JSON encoding (using the `feign-gson` extension):
+
+```json
+GitHub github = Feign.builder()
+ .encoder(new GsonEncoder())
+ .target(GitHub.class, "https://api.github.com");
+```
+
### Advanced usage and Dagger
#### Dagger
Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger.
diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java
index 64e97682b8..aab143daa1 100644
--- a/core/src/main/java/feign/Client.java
+++ b/core/src/main/java/feign/Client.java
@@ -17,9 +17,7 @@
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.OutputStream;
-import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
@@ -39,7 +37,6 @@
import static feign.Util.CONTENT_ENCODING;
import static feign.Util.CONTENT_LENGTH;
import static feign.Util.ENCODING_GZIP;
-import static feign.Util.UTF_8;
/**
* Submits HTTP {@link Request requests}. Implementations are expected to be
@@ -113,7 +110,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
out = new GZIPOutputStream(out);
}
try {
- out.write(request.body().getBytes(UTF_8));
+ out.write(request.body());
} finally {
try {
out.close();
@@ -144,8 +141,7 @@ Response convertResponse(HttpURLConnection connection) throws IOException {
} else {
stream = connection.getInputStream();
}
- Reader body = stream != null ? new InputStreamReader(stream, UTF_8) : null;
- return Response.create(status, reason, headers, body, length);
+ return Response.create(status, reason, headers, stream, length);
}
}
}
diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java
index b07e206453..dd68d9a89f 100644
--- a/core/src/main/java/feign/Logger.java
+++ b/core/src/main/java/feign/Logger.java
@@ -15,17 +15,15 @@
*/
package feign;
-import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
-import java.io.Reader;
import java.io.StringWriter;
import java.util.logging.FileHandler;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
+import static feign.Util.decodeOrDefault;
import static feign.Util.UTF_8;
-import static feign.Util.ensureClosed;
import static feign.Util.valuesOrEmpty;
/**
@@ -145,15 +143,16 @@ void logRequest(String configKey, Level logLevel, Request request) {
}
}
- int bytes = 0;
+ int bodyLength = 0;
if (request.body() != null) {
- bytes = request.body().getBytes(UTF_8).length;
+ bodyLength = request.body().length;
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
+ String bodyText = request.charset() != null ? new String(request.body(), request.charset()) : null;
log(configKey, ""); // CRLF
- log(configKey, "%s", request.body());
+ log(configKey, "%s", bodyText != null ? bodyText : "Binary data");
}
}
- log(configKey, "---> END HTTP (%s-byte body)", bytes);
+ log(configKey, "---> END HTTP (%s-byte body)", bodyLength);
}
}
@@ -171,27 +170,20 @@ Response logAndRebufferResponse(String configKey, Level logLevel, Response respo
}
}
+ int bodyLength = 0;
if (response.body() != null) {
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
log(configKey, ""); // CRLF
}
-
- BufferedReader reader = new BufferedReader(response.body().asReader());
- try {
- StringBuilder buffered = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- buffered.append(line);
- if (logLevel.ordinal() >= Level.FULL.ordinal()) {
- log(configKey, "%s", line);
- }
- }
- String bodyAsString = buffered.toString();
- log(configKey, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length);
- return Response.create(response.status(), response.reason(), response.headers(), bodyAsString);
- } finally {
- ensureClosed(reader);
+ byte[] bodyData = Util.toByteArray(response.body().asInputStream());
+ bodyLength = bodyData.length;
+ if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
+ log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
}
+ log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
+ return Response.create(response.status(), response.reason(), response.headers(), bodyData);
+ } else {
+ log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
}
}
return response;
diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java
index 864759ef39..141e644252 100644
--- a/core/src/main/java/feign/MethodHandler.java
+++ b/core/src/main/java/feign/MethodHandler.java
@@ -141,8 +141,9 @@ Object executeAndDecode(RequestTemplate template) throws Throwable {
if (response.body() == null) {
return response;
}
- String bodyString = Util.toString(response.body().asReader());
- return Response.create(response.status(), response.reason(), response.headers(), bodyString);
+ // Ensure the response body is disconnected
+ byte[] bodyData = Util.toByteArray(response.body().asInputStream());
+ return Response.create(response.status(), response.reason(), response.headers(), bodyData);
} else if (void.class == metadata.returnType()) {
return null;
} else {
diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java
index b40c956a05..76d0f54f59 100644
--- a/core/src/main/java/feign/Request.java
+++ b/core/src/main/java/feign/Request.java
@@ -15,6 +15,7 @@
*/
package feign;
+import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -25,26 +26,23 @@
/**
* An immutable request to an http server.
- *
- *
Note
- *
- * Since {@link Feign} is designed for non-binary apis, and expectations are
- * that any request can be replayed, we only support a String body.
*/
public final class Request {
private final String method;
private final String url;
private final Map> headers;
- private final String body;
+ private final byte[] body;
+ private final Charset charset;
- Request(String method, String url, Map> headers, String body) {
+ Request(String method, String url, Map> headers, byte[] body, Charset charset) {
this.method = checkNotNull(method, "method of %s", url);
this.url = checkNotNull(url, "url");
LinkedHashMap> copyOf = new LinkedHashMap>();
copyOf.putAll(checkNotNull(headers, "headers of %s %s", method, url));
this.headers = Collections.unmodifiableMap(copyOf);
this.body = body; // nullable
+ this.charset = charset; // nullable
}
/* Method to invoke on the server. */
@@ -62,8 +60,20 @@ public Map> headers() {
return headers;
}
- /* If present, this is the replayable body to send to the server. */
- public String body() {
+ /**
+ * The character set with which the body is encoded, or null if unknown or not applicable. When this is
+ * present, you can use {@code new String(req.body(), req.charset())} to access the body as a String.
+ */
+ public Charset charset() {
+ return charset;
+ }
+
+ /**
+ * If present, this is the replayable body to send to the server. In some cases, this may be interpretable as text.
+ *
+ * @see #charset()
+ */
+ public byte[] body() {
return body;
}
@@ -110,7 +120,7 @@ public int readTimeoutMillis() {
}
}
if (body != null) {
- builder.append('\n').append(body);
+ builder.append('\n').append(charset != null ? new String(body, charset) : "Binary data");
}
return builder.toString();
}
diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java
index fc3f7bd138..6a3916593a 100644
--- a/core/src/main/java/feign/RequestTemplate.java
+++ b/core/src/main/java/feign/RequestTemplate.java
@@ -19,6 +19,7 @@
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -54,7 +55,8 @@ public final class RequestTemplate implements Serializable {
private StringBuilder url = new StringBuilder();
private final Map> queries = new LinkedHashMap>();
private final Map> headers = new LinkedHashMap>();
- private String body;
+ private transient Charset charset;
+ private byte[] body;
private String bodyTemplate;
public RequestTemplate() {
@@ -68,6 +70,7 @@ public RequestTemplate(RequestTemplate toCopy) {
this.url.append(toCopy.url);
this.queries.putAll(toCopy.queries);
this.headers.putAll(toCopy.headers);
+ this.charset = toCopy.charset;
this.body = toCopy.body;
this.bodyTemplate = toCopy.bodyTemplate;
}
@@ -117,7 +120,7 @@ public RequestTemplate resolve(Map unencoded) {
/* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */
public Request request() {
return new Request(method, new StringBuilder(url).append(queryLine()).toString(),
- headers, body);
+ headers, body, charset);
}
private static String urlDecode(String arg) {
@@ -391,18 +394,39 @@ public Map> headers() {
*
* @see Request#body()
*/
- public RequestTemplate body(String body) {
- this.body = body;
- if (this.body != null) {
- byte[] contentLength = body.getBytes(UTF_8);
- header(CONTENT_LENGTH, String.valueOf(contentLength.length));
- }
+ public RequestTemplate body(byte[] bodyData, Charset charset) {
this.bodyTemplate = null;
+ this.charset = charset;
+ this.body = bodyData;
+ int bodyLength = bodyData != null ? bodyData.length : 0;
+ header(CONTENT_LENGTH, String.valueOf(bodyLength));
return this;
}
- /* @see Request#body() */
- public String body() {
+ /**
+ * replaces the {@link feign.Util#CONTENT_LENGTH} header.
+ *
+ * Usually populated by an {@link feign.codec.Encoder}.
+ *
+ * @see Request#body()
+ */
+ public RequestTemplate body(String bodyText) {
+ byte[] bodyData = bodyText != null ? bodyText.getBytes(UTF_8) : null;
+ return body(bodyData, UTF_8);
+ }
+
+ /**
+ * The character set with which the body is encoded, or null if unknown or not applicable. When this is
+ * present, you can use {@code new String(req.body(), req.charset())} to access the body as a String.
+ */
+ public Charset charset() {
+ return charset;
+ }
+
+ /**
+ * @see Request#body()
+ */
+ public byte[] body() {
return body;
}
@@ -413,6 +437,7 @@ public String body() {
*/
public RequestTemplate bodyTemplate(String bodyTemplate) {
this.bodyTemplate = bodyTemplate;
+ this.charset = null;
this.body = null;
return this;
}
@@ -426,10 +451,7 @@ public String bodyTemplate() {
}
/**
- * if there are any query params in the {@link #body()}, this will extract
- * them out.
- *
- * @return
+ * if there are any query params in the URL, this will extract them out.
*/
private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) {
// parse out queries
diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java
index 6baa2b8426..2324254d74 100644
--- a/core/src/main/java/feign/Response.java
+++ b/core/src/main/java/feign/Response.java
@@ -15,16 +15,20 @@
*/
package feign;
+import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.Reader;
-import java.io.StringReader;
+import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static feign.Util.UTF_8;
+import static feign.Util.decodeOrDefault;
import static feign.Util.checkNotNull;
import static feign.Util.checkState;
import static feign.Util.valuesOrEmpty;
@@ -40,12 +44,18 @@ public final class Response {
private final Body body;
public static Response create(int status, String reason, Map> headers,
- Reader chars, Integer length) {
- return new Response(status, reason, headers, ReaderBody.orNull(chars, length));
+ InputStream inputStream, Integer length) {
+ return new Response(status, reason, headers, InputStreamBody.orNull(inputStream, length));
}
- public static Response create(int status, String reason, Map> headers, String chars) {
- return new Response(status, reason, headers, StringBody.orNull(chars));
+ public static Response create(int status, String reason, Map> headers,
+ byte[] data) {
+ return new Response(status, reason, headers, ByteArrayBody.orNull(data));
+ }
+
+ public static Response create(int status, String reason, Map> headers,
+ String text, Charset charset) {
+ return new Response(status, reason, headers, ByteArrayBody.orNull(text, charset));
}
private Response(int status, String reason, Map> headers, Body body) {
@@ -94,28 +104,34 @@ public interface Body extends Closeable {
Integer length();
/**
- * True if {@link #asReader()} can be called more than once.
+ * True if {@link #asInputStream()} and {@link #asReader()} can be called more than once.
*/
boolean isRepeatable();
+ /**
+ * It is the responsibility of the caller to close the stream.
+ */
+ InputStream asInputStream() throws IOException;
+
/**
* It is the responsibility of the caller to close the stream.
*/
Reader asReader() throws IOException;
}
- private static final class ReaderBody implements Response.Body {
- private static Body orNull(Reader chars, Integer length) {
- if (chars == null)
+ private static final class InputStreamBody implements Response.Body {
+ private static Body orNull(InputStream inputStream, Integer length) {
+ if (inputStream == null) {
return null;
- return new ReaderBody(chars, length);
+ }
+ return new InputStreamBody(inputStream, length);
}
- private final Reader chars;
+ private final InputStream inputStream;
private final Integer length;
- private ReaderBody(Reader chars, Integer length) {
- this.chars = chars;
+ private InputStreamBody(InputStream inputStream, Integer length) {
+ this.inputStream = inputStream;
this.length = length;
}
@@ -127,50 +143,62 @@ private ReaderBody(Reader chars, Integer length) {
return false;
}
+ @Override public InputStream asInputStream() throws IOException {
+ return inputStream;
+ }
+
@Override public Reader asReader() throws IOException {
- return chars;
+ return new InputStreamReader(inputStream, UTF_8);
}
@Override public void close() throws IOException {
- chars.close();
+ inputStream.close();
}
}
- private static final class StringBody implements Response.Body {
- private static Body orNull(String chars) {
- if (chars == null)
+ private static final class ByteArrayBody implements Response.Body {
+ private static Body orNull(byte[] data) {
+ if (data == null) {
return null;
- return new StringBody(chars);
+ }
+ return new ByteArrayBody(data);
}
- private final String chars;
-
- public StringBody(String chars) {
- this.chars = chars;
+ private static Body orNull(String text, Charset charset) {
+ if (text == null) {
+ return null;
+ }
+ checkNotNull(charset, "charset");
+ return new ByteArrayBody(text.getBytes(charset));
}
- private volatile Integer length;
+ private final byte[] data;
+
+ public ByteArrayBody(byte[] data) {
+ this.data = data;
+ }
@Override public Integer length() {
- if (length == null) {
- length = chars.getBytes(UTF_8).length;
- }
- return length;
+ return data.length;
}
@Override public boolean isRepeatable() {
return true;
}
+ @Override public InputStream asInputStream() throws IOException {
+ return new ByteArrayInputStream(data);
+ }
+
@Override public Reader asReader() throws IOException {
- return new StringReader(chars);
+ return new InputStreamReader(asInputStream(), UTF_8);
}
- public String toString() {
- return chars;
+ @Override public void close() throws IOException {
}
- @Override public void close() {
+ @Override public String toString() {
+ return decodeOrDefault(data, UTF_8, "Binary data");
}
}
diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java
index 0036be7bf0..2b847fa6c6 100644
--- a/core/src/main/java/feign/Util.java
+++ b/core/src/main/java/feign/Util.java
@@ -15,14 +15,19 @@
*/
package feign;
+import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
+import java.nio.ByteBuffer;
import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
@@ -192,4 +197,50 @@ public static String toString(Reader reader) throws IOException {
ensureClosed(reader);
}
}
+
+ /**
+ * Adapted from {@code com.google.common.io.ByteStreams.toByteArray()}.
+ */
+ public static byte[] toByteArray(InputStream in) throws IOException {
+ checkNotNull(in, "in");
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ copy(in, out);
+ return out.toByteArray();
+ } finally {
+ ensureClosed(in);
+ }
+ }
+
+ /**
+ * Adapted from {@code com.google.common.io.ByteStreams.copy()}.
+ */
+ private static long copy(InputStream from, OutputStream to)
+ throws IOException {
+ checkNotNull(from, "from");
+ checkNotNull(to, "to");
+ byte[] buf = new byte[BUF_SIZE];
+ long total = 0;
+ while (true) {
+ int r = from.read(buf);
+ if (r == -1) {
+ break;
+ }
+ to.write(buf, 0, r);
+ total += r;
+ }
+ return total;
+ }
+
+ static String decodeOrDefault(byte[] data, Charset charset, String defaultValue) {
+ if (data == null) {
+ return defaultValue;
+ }
+ checkNotNull(charset, "charset");
+ try {
+ return charset.newDecoder().decode(ByteBuffer.wrap(data)).toString();
+ } catch (CharacterCodingException ex) {
+ return defaultValue;
+ }
+ }
}
diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java
index 8167854c26..346b149bfb 100644
--- a/core/src/main/java/feign/codec/Decoder.java
+++ b/core/src/main/java/feign/codec/Decoder.java
@@ -17,6 +17,7 @@
import feign.FeignException;
import feign.Response;
+import feign.Util;
import java.io.IOException;
import java.lang.reflect.Type;
@@ -76,5 +77,16 @@ public interface Decoder {
* Default implementation of {@code Decoder}.
*/
public class Default extends StringDecoder {
+ @Override
+ public Object decode(Response response, Type type) throws IOException {
+ Response.Body body = response.body();
+ if (body == null) {
+ return null;
+ }
+ if (byte[].class.equals(type)) {
+ return Util.toByteArray(body.asInputStream());
+ }
+ return super.decode(response, type);
+ }
}
}
diff --git a/core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java
index ab7e39f8c7..c3b07d591a 100644
--- a/core/src/main/java/feign/codec/Encoder.java
+++ b/core/src/main/java/feign/codec/Encoder.java
@@ -69,13 +69,15 @@ public interface Encoder {
void encode(Object object, RequestTemplate template) throws EncodeException;
/**
- * Default implementation of {@code Encoder} that supports {@code String}s only.
+ * Default implementation of {@code Encoder}.
*/
public class Default implements Encoder {
@Override
public void encode(Object object, RequestTemplate template) throws EncodeException {
if (object instanceof String) {
template.body(object.toString());
+ } else if (object instanceof byte[]) {
+ template.body((byte[]) object, null);
} else if (object != null) {
throw new EncodeException(format("%s is not a type supported by this encoder.", object.getClass()));
}
diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java
index 7dae475857..e268fb7f77 100644
--- a/core/src/test/java/feign/DefaultContractTest.java
+++ b/core/src/test/java/feign/DefaultContractTest.java
@@ -29,6 +29,8 @@
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
+import static feign.Util.UTF_8;
+
/**
* Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign
* .RequestTemplate template}
@@ -154,8 +156,9 @@ interface BodyWithoutParameters {
}
@Test public void bodyWithoutParameters() throws Exception {
+ String expectedBody = "";
MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
- assertEquals(md.template().body(), "");
+ assertEquals(md.template().body(), expectedBody.getBytes(UTF_8));
assertFalse(md.template().bodyTemplate() != null);
assertTrue(md.formParams().isEmpty());
assertTrue(md.indexToName().isEmpty());
diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java
index fa86f19da5..801422e255 100644
--- a/core/src/test/java/feign/FeignTest.java
+++ b/core/src/test/java/feign/FeignTest.java
@@ -113,6 +113,10 @@ public void iterableQueryParams() throws IOException, InterruptedException {
interface OtherTestInterface {
@RequestLine("POST /") String post();
+
+ @RequestLine("POST /") byte[] binaryResponseBody();
+
+ @RequestLine("POST /") void binaryRequestBody(byte[] contents);
}
@Module(library = true, overrides = true)
@@ -499,4 +503,37 @@ static class DisableHostnameVerification {
assertEquals(i1.hashCode(), i1.hashCode());
assertEquals(i1.hashCode(), i2.hashCode());
}
+
+ @Test public void decodeLogicSupportsByteArray() throws Exception {
+ byte[] expectedResponse = {12, 34, 56};
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse().setBody(expectedResponse));
+ server.play();
+
+ try {
+ OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort());
+
+ byte[] actualResponse = api.binaryResponseBody();
+ assertEquals(actualResponse, expectedResponse);
+ } finally {
+ server.shutdown();
+ }
+ }
+
+ @Test public void encodeLogicSupportsByteArray() throws Exception {
+ byte[] expectedRequest = {12, 34, 56};
+ final MockWebServer server = new MockWebServer();
+ server.enqueue(new MockResponse());
+ server.play();
+
+ try {
+ OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort());
+
+ api.binaryRequestBody(expectedRequest);
+ byte[] actualRequest = server.takeRequest().getBody();
+ assertEquals(actualRequest, expectedRequest);
+ } finally {
+ server.shutdown();
+ }
+ }
}
diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java
index c5b58f8688..e270df5b53 100644
--- a/core/src/test/java/feign/codec/DefaultDecoderTest.java
+++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java
@@ -19,7 +19,8 @@
import org.testng.annotations.Test;
import org.w3c.dom.Document;
-import java.io.StringReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -28,6 +29,8 @@
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
+import static feign.Util.UTF_8;
+
public class DefaultDecoderTest {
private final Decoder decoder = new Decoder.Default();
@@ -38,6 +41,13 @@ public class DefaultDecoderTest {
assertEquals(decodedObject.toString(), "response body");
}
+ @Test public void testDecodesToByteArray() throws Exception {
+ Response response = knownResponse();
+ Object decodedObject = decoder.decode(response, byte[].class);
+ assertEquals(decodedObject.getClass(), byte[].class);
+ assertEquals((byte[]) decodedObject, "response body".getBytes(UTF_8));
+ }
+
@Test public void testDecodesNullBodyToNull() throws Exception {
assertNull(decoder.decode(nullBodyResponse(), Document.class));
}
@@ -49,10 +59,10 @@ public void testRefusesToDecodeOtherTypes() throws Exception {
private Response knownResponse() {
String content = "response body";
- StringReader reader = new StringReader(content);
+ InputStream inputStream = new ByteArrayInputStream(content.getBytes(UTF_8));
Map> headers = new HashMap>();
headers.put("Content-Type", Collections.singleton("text/plain"));
- return Response.create(200, "OK", headers, reader, content.length());
+ return Response.create(200, "OK", headers, inputStream, content.length());
}
private Response nullBodyResponse() {
diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java
index 21f93026fa..1dc4fe5985 100644
--- a/core/src/test/java/feign/codec/DefaultEncoderTest.java
+++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java
@@ -22,6 +22,8 @@
import static org.testng.Assert.assertEquals;
+import static feign.Util.UTF_8;
+
public class DefaultEncoderTest {
private final Encoder encoder = new Encoder.Default();
@@ -29,6 +31,13 @@ public class DefaultEncoderTest {
String content = "This is my content";
RequestTemplate template = new RequestTemplate();
encoder.encode(content, template);
+ assertEquals(template.body(), content.getBytes(UTF_8));
+ }
+
+ @Test public void testEncodesByteArray() throws Exception {
+ byte[] content = {12, 34, 56};
+ RequestTemplate template = new RequestTemplate();
+ encoder.encode(content, template);
assertEquals(template.body(), content);
}
diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
index efab2c9a76..e6173bca6c 100644
--- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
+++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
@@ -27,6 +27,7 @@
import feign.RetryableException;
import static feign.Util.RETRY_AFTER;
+import static feign.Util.UTF_8;
public class DefaultErrorDecoderTest {
ErrorDecoder errorDecoder = new ErrorDecoder.Default();
@@ -42,7 +43,7 @@ public void throwsFeignException() throws Throwable {
@Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\); content:\nhello world")
public void throwsFeignExceptionIncludingBody() throws Throwable {
Response response = Response.create(500, "Internal server error", ImmutableMap.>of(),
- "hello world");
+ "hello world", UTF_8);
throw errorDecoder.decode("Service#foo()", response);
}
diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java
index 2c94476105..d0bce2abfc 100644
--- a/gson/src/test/java/feign/gson/GsonModuleTest.java
+++ b/gson/src/test/java/feign/gson/GsonModuleTest.java
@@ -40,6 +40,8 @@
import static org.testng.Assert.assertEquals;
+import static feign.Util.UTF_8;
+
@Test
public class GsonModuleTest {
@Module(includes = GsonModule.class, injects = EncoderAndDecoderBindings.class)
@@ -62,6 +64,11 @@ static class EncoderBindings {
}
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
+ String expectedBody = ""
+ + "{\n"
+ + " \"foo\": 1\n"
+ + "}";
+
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
@@ -70,13 +77,18 @@ static class EncoderBindings {
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(map, template);
- assertEquals(template.body(), ""//
- + "{\n" //
- + " \"foo\": 1\n" //
- + "}");
+ assertEquals(template.body(), expectedBody.getBytes(UTF_8));
}
@Test public void encodesFormParams() throws Exception {
+ String expectedBody = ""//
+ + "{\n" //
+ + " \"foo\": 1,\n" //
+ + " \"bar\": [\n" //
+ + " 2,\n" //
+ + " 3\n" //
+ + " ]\n" //
+ + "}";
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
@@ -87,14 +99,7 @@ static class EncoderBindings {
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(form, template);
- assertEquals(template.body(), ""//
- + "{\n" //
- + " \"foo\": 1,\n" //
- + " \"bar\": [\n" //
- + " 2,\n" //
- + " 3\n" //
- + " ]\n" //
- + "}");
+ assertEquals(template.body(), expectedBody.getBytes(UTF_8));
}
static class Zone extends LinkedHashMap {
@@ -128,7 +133,8 @@ static class DecoderBindings {
zones.add(new Zone("denominator.io."));
zones.add(new Zone("denominator.io.", "ABCD"));
- Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson);
+ Response response =
+ Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8);
assertEquals(bindings.decoder.decode(response, new TypeToken>() {
}.getType()), zones);
}
@@ -184,7 +190,8 @@ static class CustomTypeAdapter {
zones.add(new Zone("DENOMINATOR.IO."));
zones.add(new Zone("DENOMINATOR.IO.", "ABCD"));
- Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson);
+ Response response =
+ Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8);
assertEquals(bindings.decoder.decode(response, new TypeToken>() {
}.getType()), zones);
}
diff --git a/jackson/src/main/java/feign/jackson/JacksonDecoder.java b/jackson/src/main/java/feign/jackson/JacksonDecoder.java
index 83400afc14..f0734d3768 100644
--- a/jackson/src/main/java/feign/jackson/JacksonDecoder.java
+++ b/jackson/src/main/java/feign/jackson/JacksonDecoder.java
@@ -22,7 +22,7 @@
import feign.codec.Decoder;
import java.io.IOException;
-import java.io.Reader;
+import java.io.InputStream;
import java.lang.reflect.Type;
public class JacksonDecoder implements Decoder {
@@ -40,9 +40,9 @@ public JacksonDecoder(ObjectMapper mapper) {
if (response.body() == null) {
return null;
}
- Reader reader = response.body().asReader();
+ InputStream inputStream = response.body().asInputStream();
try {
- return mapper.readValue(reader, mapper.constructType(type));
+ return mapper.readValue(inputStream, mapper.constructType(type));
} catch (RuntimeJsonMappingException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
diff --git a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java
index c13583f6db..a4f9dfa8ef 100644
--- a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java
+++ b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java
@@ -21,6 +21,8 @@
import static org.testng.Assert.assertEquals;
+import static feign.Util.UTF_8;
+
@Test
public class JacksonModuleTest {
@Module(includes = JacksonModule.class, injects = EncoderAndDecoderBindings.class)
@@ -54,7 +56,7 @@ static class EncoderBindings {
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(map, template);
- assertEquals(template.body(), ""//
+ assertEquals(new String(template.body(), UTF_8), ""//
+ "{\n" //
+ " \"foo\" : 1\n" //
+ "}");
@@ -70,7 +72,7 @@ static class EncoderBindings {
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(form, template);
- assertEquals(template.body(), ""//
+ assertEquals(new String(template.body(), UTF_8), ""//
+ "{\n" //
+ " \"foo\" : 1,\n" //
+ " \"bar\" : [ 2, 3 ]\n" //
@@ -109,7 +111,8 @@ static class DecoderBindings {
zones.add(new Zone("denominator.io."));
zones.add(new Zone("denominator.io.", "ABCD"));
- Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson);
+ Response response =
+ Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8);
assertEquals(bindings.decoder.decode(response, new TypeToken>() {
}.getType()), zones);
}
@@ -177,7 +180,8 @@ com.fasterxml.jackson.databind.Module upperZone() {
zones.add(new Zone("DENOMINATOR.IO."));
zones.add(new Zone("DENOMINATOR.IO.", "ABCD"));
- Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson);
+ Response response =
+ Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8);
assertEquals(bindings.decoder.decode(response, new TypeToken>() {
}.getType()), zones);
}
diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java
index 134c289bf4..a6d79205a2 100644
--- a/ribbon/src/main/java/feign/ribbon/LBClient.java
+++ b/ribbon/src/main/java/feign/ribbon/LBClient.java
@@ -93,7 +93,8 @@ Request toRequest() {
.method(request.method())
.append(getUri().toASCIIString())
.headers(request.headers())
- .body(request.body()).request();
+ .body(request.body(), request.charset())
+ .request();
}
public Object clone() {
diff --git a/sax/src/main/java/feign/sax/SAXDecoder.java b/sax/src/main/java/feign/sax/SAXDecoder.java
index 17981d734f..0afc817737 100644
--- a/sax/src/main/java/feign/sax/SAXDecoder.java
+++ b/sax/src/main/java/feign/sax/SAXDecoder.java
@@ -26,7 +26,7 @@
import javax.inject.Provider;
import java.io.IOException;
-import java.io.Reader;
+import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
@@ -154,11 +154,11 @@ public Object decode(Response response, Type type) throws IOException, DecodeExc
xmlReader.setFeature("http://xml.org/sax/features/namespaces", false);
xmlReader.setFeature("http://xml.org/sax/features/validation", false);
xmlReader.setContentHandler(handler);
- Reader reader = response.body().asReader();
+ InputStream inputStream = response.body().asInputStream();
try {
- xmlReader.parse(new InputSource(reader));
+ xmlReader.parse(new InputSource(inputStream));
} finally {
- ensureClosed(reader);
+ ensureClosed(inputStream);
}
return handler.result();
} catch (SAXException e) {
diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java
index d10fd4fe9a..c4b9abf07c 100644
--- a/sax/src/test/java/feign/sax/SAXDecoderTest.java
+++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java
@@ -32,6 +32,8 @@
import static org.testng.Assert.assertEquals;
+import static feign.Util.UTF_8;
+
// unbound wildcards are not currently injectable in dagger.
@SuppressWarnings("rawtypes")
public class SAXDecoderTest {
@@ -64,7 +66,7 @@ public void niceErrorOnUnconfiguredType() throws ParseException, IOException {
}
private Response statusFailedResponse() {
- return Response.create(200, "OK", Collections.>emptyMap(), statusFailed);
+ return Response.create(200, "OK", Collections.>emptyMap(), statusFailed, UTF_8);
}
static String statusFailed = ""//
diff --git a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java
index 282be5f786..c229587b8c 100644
--- a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java
+++ b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java
@@ -128,9 +128,10 @@ private String canonicalString(RequestTemplate input, Multimap s
canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n');
// HexEncode(Hash(Payload))
- if (input.body() != null) {
- canonicalRequest.append(base16().lowerCase().encode(
- sha256().hashString(input.body() != null ? input.body() : "", UTF_8).asBytes()));
+ String bodyText =
+ input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null;
+ if (bodyText != null) {
+ canonicalRequest.append(base16().lowerCase().encode(sha256().hashString(bodyText, UTF_8).asBytes()));
} else {
canonicalRequest.append(EMPTY_STRING_HASH);
}