Skip to content

Commit a2addbf

Browse files
benmanbsadriancole
authored andcommitted
add doNotCloseAfterDecode flag to Feign builder (OpenFeign#649)
This commit adds the `doNotCloseAfterDecode` flag to the Feign builder object. This allows you to lazily evaluate the response in your Decoder, in order to support Iterators or Java 8 Streams. This is a pretty light weight change, to support a do-it-yourself approach to lazy instantiation. Fixes OpenFeign#514.
1 parent 0ad007a commit a2addbf

File tree

4 files changed

+97
-4
lines changed

4 files changed

+97
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### Version 9.6
2+
* Feign builder now supports flag `doNotCloseAfterDecode` to support lazy iteration of responses.
3+
14
### Version 9.5.1
25
* When specified, Content-Type header is now included on OkHttp requests lacking a body.
36
* Sets empty HttpEntity if apache request body is null.

core/src/main/java/feign/Feign.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ public static class Builder {
108108
private InvocationHandlerFactory invocationHandlerFactory =
109109
new InvocationHandlerFactory.Default();
110110
private boolean decode404;
111+
private boolean closeAfterDecode = true;
111112

112113
public Builder logLevel(Logger.Level logLevel) {
113114
this.logLevel = logLevel;
@@ -210,6 +211,25 @@ public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandl
210211
return this;
211212
}
212213

214+
/**
215+
* This flag indicates that the response should not be automatically closed
216+
* upon completion of decoding the message. This should be set if you plan on
217+
* processing the response into a lazy-evaluated construct, such as a
218+
* {@link java.util.Iterator}.
219+
*
220+
* </p>Feign standard decoders do not have built in support for this flag. If
221+
* you are using this flag, you MUST also use a custom Decoder, and be sure to
222+
* close all resources appropriately somewhere in the Decoder (you can use
223+
* {@link Util.ensureClosed} for convenience).
224+
*
225+
* @since 9.6
226+
*
227+
*/
228+
public Builder doNotCloseAfterDecode() {
229+
this.closeAfterDecode = false;
230+
return this;
231+
}
232+
213233
public <T> T target(Class<T> apiType, String url) {
214234
return target(new HardCodedTarget<T>(apiType, url));
215235
}
@@ -221,7 +241,7 @@ public <T> T target(Target<T> target) {
221241
public Feign build() {
222242
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
223243
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
224-
logLevel, decode404);
244+
logLevel, decode404, closeAfterDecode);
225245
ParseHandlersByName handlersByName =
226246
new ParseHandlersByName(contract, options, encoder, decoder,
227247
errorDecoder, synchronousMethodHandlerFactory);

core/src/main/java/feign/SynchronousMethodHandler.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ final class SynchronousMethodHandler implements MethodHandler {
4646
private final Decoder decoder;
4747
private final ErrorDecoder errorDecoder;
4848
private final boolean decode404;
49+
private final boolean closeAfterDecode;
4950

5051
private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer,
5152
List<RequestInterceptor> requestInterceptors, Logger logger,
5253
Logger.Level logLevel, MethodMetadata metadata,
5354
RequestTemplate.Factory buildTemplateFromArgs, Options options,
54-
Decoder decoder, ErrorDecoder errorDecoder, boolean decode404) {
55+
Decoder decoder, ErrorDecoder errorDecoder, boolean decode404,
56+
boolean closeAfterDecode) {
5557
this.target = checkNotNull(target, "target");
5658
this.client = checkNotNull(client, "client for %s", target);
5759
this.retryer = checkNotNull(retryer, "retryer for %s", target);
@@ -65,6 +67,7 @@ private SynchronousMethodHandler(Target<?> target, Client client, Retryer retrye
6567
this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target);
6668
this.decoder = checkNotNull(decoder, "decoder for %s", target);
6769
this.decode404 = decode404;
70+
this.closeAfterDecode = closeAfterDecode;
6871
}
6972

7073
@Override
@@ -130,9 +133,11 @@ Object executeAndDecode(RequestTemplate template) throws Throwable {
130133
if (void.class == metadata.returnType()) {
131134
return null;
132135
} else {
136+
shouldClose = closeAfterDecode;
133137
return decode(response);
134138
}
135139
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
140+
shouldClose = closeAfterDecode;
136141
return decode(response);
137142
} else {
138143
throw errorDecoder.decode(metadata.configKey(), response);
@@ -178,23 +183,25 @@ static class Factory {
178183
private final Logger logger;
179184
private final Logger.Level logLevel;
180185
private final boolean decode404;
186+
private final boolean closeAfterDecode;
181187

182188
Factory(Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors,
183-
Logger logger, Logger.Level logLevel, boolean decode404) {
189+
Logger logger, Logger.Level logLevel, boolean decode404, boolean closeAfterDecode) {
184190
this.client = checkNotNull(client, "client");
185191
this.retryer = checkNotNull(retryer, "retryer");
186192
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
187193
this.logger = checkNotNull(logger, "logger");
188194
this.logLevel = checkNotNull(logLevel, "logLevel");
189195
this.decode404 = decode404;
196+
this.closeAfterDecode = closeAfterDecode;
190197
}
191198

192199
public MethodHandler create(Target<?> target, MethodMetadata md,
193200
RequestTemplate.Factory buildTemplateFromArgs,
194201
Options options, Decoder decoder, ErrorDecoder errorDecoder) {
195202
return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
196203
logLevel, md, buildTemplateFromArgs, options, decoder,
197-
errorDecoder, decode404);
204+
errorDecoder, decode404, closeAfterDecode);
198205
}
199206
}
200207
}

core/src/test/java/feign/FeignBuilderTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
import org.junit.Rule;
2222
import org.junit.Test;
2323

24+
import java.io.IOException;
2425
import java.lang.reflect.InvocationHandler;
2526
import java.lang.reflect.Method;
2627
import java.lang.reflect.Type;
2728
import java.util.Arrays;
29+
import java.util.Iterator;
2830
import java.util.List;
2931
import java.util.Map;
3032
import java.util.concurrent.atomic.AtomicInteger;
@@ -35,6 +37,8 @@
3537
import static feign.assertj.MockWebServerAssertions.assertThat;
3638
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
3739
import static org.junit.Assert.assertEquals;
40+
import static org.junit.Assert.assertFalse;
41+
import static org.junit.Assert.assertTrue;
3842
import static org.junit.Assert.fail;
3943

4044
public class FeignBuilderTest {
@@ -242,6 +246,62 @@ public void testDefaultCallingProxiedMethod() throws Exception {
242246
assertThat(server.takeRequest()).hasPath("/");
243247
}
244248

249+
/**
250+
* This test ensures that the doNotCloseAfterDecode flag functions.
251+
*
252+
* It does so by creating a custom Decoder that lazily retrieves the
253+
* response body when asked for it and pops the value into an Iterator.
254+
*
255+
* Without the doNoCloseAfterDecode flag, the test will fail with a
256+
* "stream is closed" exception.
257+
*
258+
* @throws Exception
259+
*/
260+
@Test
261+
public void testDoNotCloseAfterDecode() throws Exception {
262+
server.enqueue(new MockResponse().setBody("success!"));
263+
264+
String url = "http://localhost:" + server.getPort();
265+
Decoder decoder = new Decoder() {
266+
@Override
267+
public Iterator decode(Response response, Type type) {
268+
return new Iterator() {
269+
private boolean called = false;
270+
271+
@Override
272+
public boolean hasNext() {
273+
return !called;
274+
}
275+
276+
@Override
277+
public Object next() {
278+
try {
279+
return Util.toString(response.body().asReader());
280+
} catch (IOException e) {
281+
fail(e.getMessage());
282+
return null;
283+
} finally {
284+
Util.ensureClosed(response);
285+
called = true;
286+
}
287+
}
288+
};
289+
}
290+
};
291+
292+
TestInterface api = Feign.builder()
293+
.decoder(decoder)
294+
.doNotCloseAfterDecode()
295+
.target(TestInterface.class, url);
296+
Iterator<String> iterator = api.decodedLazyPost();
297+
298+
assertTrue(iterator.hasNext());
299+
assertEquals("success!", iterator.next());
300+
assertFalse(iterator.hasNext());
301+
302+
assertEquals(1, server.getRequestCount());
303+
}
304+
245305
interface TestInterface {
246306
@RequestLine("GET")
247307
Response getNoPath();
@@ -257,6 +317,9 @@ interface TestInterface {
257317

258318
@RequestLine("POST /")
259319
String decodedPost();
320+
321+
@RequestLine("POST /")
322+
Iterator<String> decodedLazyPost();
260323

261324
@RequestLine(value = "GET /api/queues/{vhost}", decodeSlash = false)
262325
byte[] getQueues(@Param("vhost") String vhost);

0 commit comments

Comments
 (0)