Skip to content

Commit 91e4d82

Browse files
committed
Support binary request/response bodies (OpenFeign#57)
Request/Response/RequestTemplate are now fundamentally based on a byte[] body field. For Request/RequestTemplate, if a charset is provided, it can be treated as text. For many users of the library, the change should barely be noticeable, as the methods that were changed were mostly used internally. There were some non-backwards-compatible signature changes that require a major version bump, however.
1 parent c92025c commit 91e4d82

23 files changed

Lines changed: 330 additions & 125 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### Version 6.0
2+
* Support binary request and response bodies.
3+
14
### Version 5.4.0
25
* Add `BasicAuthRequestInterceptor`
36
* Add Jackson integration

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,29 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod
144144
### Decoders
145145
`Feign.builder()` allows you to specify additional configuration such as how to decode a response.
146146

147-
If any methods in your interface return types besides `Response`, `String` or `void`, you'll need to configure a `Decoder`.
147+
If any methods in your interface return types besides `Response`, `String`, `byte[]` or `void`, you'll need to configure a non-default `Decoder`.
148148

149-
Here's how to configure json decoding (using the `feign-gson` extension):
149+
Here's how to configure JSON decoding (using the `feign-gson` extension):
150150

151151
```java
152152
GitHub github = Feign.builder()
153153
.decoder(new GsonDecoder())
154154
.target(GitHub.class, "https://api.github.com");
155155
```
156156

157+
### Encoders
158+
`Feign.builder()` allows you to specify additional configuration such as how to encode a request.
159+
160+
If any methods in your interface use parameters types besides `String` or `byte[]`, you'll need to configure a non-default `Encoder`.
161+
162+
Here's how to configure JSON encoding (using the `feign-gson` extension):
163+
164+
```json
165+
GitHub github = Feign.builder()
166+
.encoder(new GsonEncoder())
167+
.target(GitHub.class, "https://api.github.com");
168+
```
169+
157170
### Advanced usage and Dagger
158171
#### Dagger
159172
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.

core/src/main/java/feign/Client.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717

1818
import java.io.IOException;
1919
import java.io.InputStream;
20-
import java.io.InputStreamReader;
2120
import java.io.OutputStream;
22-
import java.io.Reader;
2321
import java.net.HttpURLConnection;
2422
import java.net.URL;
2523
import java.util.Collection;
@@ -39,7 +37,6 @@
3937
import static feign.Util.CONTENT_ENCODING;
4038
import static feign.Util.CONTENT_LENGTH;
4139
import static feign.Util.ENCODING_GZIP;
42-
import static feign.Util.UTF_8;
4340

4441
/**
4542
* Submits HTTP {@link Request requests}. Implementations are expected to be
@@ -113,7 +110,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
113110
out = new GZIPOutputStream(out);
114111
}
115112
try {
116-
out.write(request.body().getBytes(UTF_8));
113+
out.write(request.body());
117114
} finally {
118115
try {
119116
out.close();
@@ -144,8 +141,7 @@ Response convertResponse(HttpURLConnection connection) throws IOException {
144141
} else {
145142
stream = connection.getInputStream();
146143
}
147-
Reader body = stream != null ? new InputStreamReader(stream, UTF_8) : null;
148-
return Response.create(status, reason, headers, body, length);
144+
return Response.create(status, reason, headers, stream, length);
149145
}
150146
}
151147
}

core/src/main/java/feign/Logger.java

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

18-
import java.io.BufferedReader;
1918
import java.io.IOException;
2019
import java.io.PrintWriter;
21-
import java.io.Reader;
2220
import java.io.StringWriter;
2321
import java.util.logging.FileHandler;
2422
import java.util.logging.LogRecord;
2523
import java.util.logging.SimpleFormatter;
2624

25+
import static feign.Util.decodeOrDefault;
2726
import static feign.Util.UTF_8;
28-
import static feign.Util.ensureClosed;
2927
import static feign.Util.valuesOrEmpty;
3028

3129
/**
@@ -145,15 +143,16 @@ void logRequest(String configKey, Level logLevel, Request request) {
145143
}
146144
}
147145

148-
int bytes = 0;
146+
int bodyLength = 0;
149147
if (request.body() != null) {
150-
bytes = request.body().getBytes(UTF_8).length;
148+
bodyLength = request.body().length;
151149
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
150+
String bodyText = request.charset() != null ? new String(request.body(), request.charset()) : null;
152151
log(configKey, ""); // CRLF
153-
log(configKey, "%s", request.body());
152+
log(configKey, "%s", bodyText != null ? bodyText : "Binary data");
154153
}
155154
}
156-
log(configKey, "---> END HTTP (%s-byte body)", bytes);
155+
log(configKey, "---> END HTTP (%s-byte body)", bodyLength);
157156
}
158157
}
159158

@@ -171,27 +170,20 @@ Response logAndRebufferResponse(String configKey, Level logLevel, Response respo
171170
}
172171
}
173172

173+
int bodyLength = 0;
174174
if (response.body() != null) {
175175
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
176176
log(configKey, ""); // CRLF
177177
}
178-
179-
BufferedReader reader = new BufferedReader(response.body().asReader());
180-
try {
181-
StringBuilder buffered = new StringBuilder();
182-
String line;
183-
while ((line = reader.readLine()) != null) {
184-
buffered.append(line);
185-
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
186-
log(configKey, "%s", line);
187-
}
188-
}
189-
String bodyAsString = buffered.toString();
190-
log(configKey, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length);
191-
return Response.create(response.status(), response.reason(), response.headers(), bodyAsString);
192-
} finally {
193-
ensureClosed(reader);
178+
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
179+
bodyLength = bodyData.length;
180+
if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
181+
log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
194182
}
183+
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
184+
return Response.create(response.status(), response.reason(), response.headers(), bodyData);
185+
} else {
186+
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
195187
}
196188
}
197189
return response;

core/src/main/java/feign/MethodHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,9 @@ Object executeAndDecode(RequestTemplate template) throws Throwable {
141141
if (response.body() == null) {
142142
return response;
143143
}
144-
String bodyString = Util.toString(response.body().asReader());
145-
return Response.create(response.status(), response.reason(), response.headers(), bodyString);
144+
// Ensure the response body is disconnected
145+
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
146+
return Response.create(response.status(), response.reason(), response.headers(), bodyData);
146147
} else if (void.class == metadata.returnType()) {
147148
return null;
148149
} else {

core/src/main/java/feign/Request.java

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

18+
import java.nio.charset.Charset;
1819
import java.util.Collection;
1920
import java.util.Collections;
2021
import java.util.LinkedHashMap;
@@ -25,26 +26,23 @@
2526

2627
/**
2728
* An immutable request to an http server.
28-
* <br>
29-
* <br><br><b>Note</b><br>
30-
* <br>
31-
* Since {@link Feign} is designed for non-binary apis, and expectations are
32-
* that any request can be replayed, we only support a String body.
3329
*/
3430
public final class Request {
3531

3632
private final String method;
3733
private final String url;
3834
private final Map<String, Collection<String>> headers;
39-
private final String body;
35+
private final byte[] body;
36+
private final Charset charset;
4037

41-
Request(String method, String url, Map<String, Collection<String>> headers, String body) {
38+
Request(String method, String url, Map<String, Collection<String>> headers, byte[] body, Charset charset) {
4239
this.method = checkNotNull(method, "method of %s", url);
4340
this.url = checkNotNull(url, "url");
4441
LinkedHashMap<String, Collection<String>> copyOf = new LinkedHashMap<String, Collection<String>>();
4542
copyOf.putAll(checkNotNull(headers, "headers of %s %s", method, url));
4643
this.headers = Collections.unmodifiableMap(copyOf);
4744
this.body = body; // nullable
45+
this.charset = charset; // nullable
4846
}
4947

5048
/* Method to invoke on the server. */
@@ -62,8 +60,20 @@ public Map<String, Collection<String>> headers() {
6260
return headers;
6361
}
6462

65-
/* If present, this is the replayable body to send to the server. */
66-
public String body() {
63+
/**
64+
* The character set with which the body is encoded, or null if unknown or not applicable. When this is
65+
* present, you can use {@code new String(req.body(), req.charset())} to access the body as a String.
66+
*/
67+
public Charset charset() {
68+
return charset;
69+
}
70+
71+
/**
72+
* If present, this is the replayable body to send to the server. In some cases, this may be interpretable as text.
73+
*
74+
* @see #charset()
75+
*/
76+
public byte[] body() {
6777
return body;
6878
}
6979

@@ -110,7 +120,7 @@ public int readTimeoutMillis() {
110120
}
111121
}
112122
if (body != null) {
113-
builder.append('\n').append(body);
123+
builder.append('\n').append(charset != null ? new String(body, charset) : "Binary data");
114124
}
115125
return builder.toString();
116126
}

core/src/main/java/feign/RequestTemplate.java

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.UnsupportedEncodingException;
2020
import java.net.URLDecoder;
2121
import java.net.URLEncoder;
22+
import java.nio.charset.Charset;
2223
import java.util.ArrayList;
2324
import java.util.Arrays;
2425
import java.util.Collection;
@@ -54,7 +55,8 @@ public final class RequestTemplate implements Serializable {
5455
private StringBuilder url = new StringBuilder();
5556
private final Map<String, Collection<String>> queries = new LinkedHashMap<String, Collection<String>>();
5657
private final Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>();
57-
private String body;
58+
private transient Charset charset;
59+
private byte[] body;
5860
private String bodyTemplate;
5961

6062
public RequestTemplate() {
@@ -68,6 +70,7 @@ public RequestTemplate(RequestTemplate toCopy) {
6870
this.url.append(toCopy.url);
6971
this.queries.putAll(toCopy.queries);
7072
this.headers.putAll(toCopy.headers);
73+
this.charset = toCopy.charset;
7174
this.body = toCopy.body;
7275
this.bodyTemplate = toCopy.bodyTemplate;
7376
}
@@ -117,7 +120,7 @@ public RequestTemplate resolve(Map<String, ?> unencoded) {
117120
/* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */
118121
public Request request() {
119122
return new Request(method, new StringBuilder(url).append(queryLine()).toString(),
120-
headers, body);
123+
headers, body, charset);
121124
}
122125

123126
private static String urlDecode(String arg) {
@@ -391,18 +394,39 @@ public Map<String, Collection<String>> headers() {
391394
*
392395
* @see Request#body()
393396
*/
394-
public RequestTemplate body(String body) {
395-
this.body = body;
396-
if (this.body != null) {
397-
byte[] contentLength = body.getBytes(UTF_8);
398-
header(CONTENT_LENGTH, String.valueOf(contentLength.length));
399-
}
397+
public RequestTemplate body(byte[] bodyData, Charset charset) {
400398
this.bodyTemplate = null;
399+
this.charset = charset;
400+
this.body = bodyData;
401+
int bodyLength = bodyData != null ? bodyData.length : 0;
402+
header(CONTENT_LENGTH, String.valueOf(bodyLength));
401403
return this;
402404
}
403405

404-
/* @see Request#body() */
405-
public String body() {
406+
/**
407+
* replaces the {@link feign.Util#CONTENT_LENGTH} header.
408+
* <br>
409+
* Usually populated by an {@link feign.codec.Encoder}.
410+
*
411+
* @see Request#body()
412+
*/
413+
public RequestTemplate body(String bodyText) {
414+
byte[] bodyData = bodyText != null ? bodyText.getBytes(UTF_8) : null;
415+
return body(bodyData, UTF_8);
416+
}
417+
418+
/**
419+
* The character set with which the body is encoded, or null if unknown or not applicable. When this is
420+
* present, you can use {@code new String(req.body(), req.charset())} to access the body as a String.
421+
*/
422+
public Charset charset() {
423+
return charset;
424+
}
425+
426+
/**
427+
* @see Request#body()
428+
*/
429+
public byte[] body() {
406430
return body;
407431
}
408432

@@ -413,6 +437,7 @@ public String body() {
413437
*/
414438
public RequestTemplate bodyTemplate(String bodyTemplate) {
415439
this.bodyTemplate = bodyTemplate;
440+
this.charset = null;
416441
this.body = null;
417442
return this;
418443
}
@@ -426,10 +451,7 @@ public String bodyTemplate() {
426451
}
427452

428453
/**
429-
* if there are any query params in the {@link #body()}, this will extract
430-
* them out.
431-
*
432-
* @return
454+
* if there are any query params in the URL, this will extract them out.
433455
*/
434456
private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) {
435457
// parse out queries

0 commit comments

Comments
 (0)