Skip to content

Commit 806149c

Browse files
committed
Adding Method to Retryable Exception for evaluation
Closes #719 This change adds the original Request Method to `RetryableException`, allowing implementers to determine if a retry should occur based on method and exception type. To support this, `Response` objects now require that the original `Request` be present. Test Cases, benchmarks, and documentation have been added.
1 parent 985c682 commit 806149c

File tree

27 files changed

+180
-56
lines changed

27 files changed

+180
-56
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
### Version 10.0
22
* Feign baseline is now JDK 8
33
* Removed @Deprecated methods marked for removal on feign 10
4+
* `RetryException` includes the `Method` used for the offending `Request`
5+
* `Response` objects now contain the `Request` used.
46

57
### Version 9.6
68
* Feign builder now supports flag `doNotCloseAfterDecode` to support lazy iteration of responses.

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,37 @@ MyApi myApi = Feign.builder()
473473
.target(MyApi.class, "https://api.hostname.com");
474474
```
475475

476+
### Error Handling
477+
If you need more control over handling unexpected responses, Feign instances can
478+
register a custom `ErrorDecoder` via the builder.
479+
480+
```java
481+
MyApi myApi = Feign.builder()
482+
.errorDecoder(new MyErrorDecoder())
483+
.target(MyApi.class, "https://api.hostname.com");
484+
```
485+
486+
All responses that result in an HTTP status not in the 2xx range will trigger the `ErrorDecoder`'s `decode` method, allowing
487+
you to handle the response, wrap the failure into a custom exception or perform any additional processing.
488+
If you want to retry the request again, throw a `RetryableException`. This will invoke the registered
489+
`Retyer`.
490+
491+
### Retry
492+
Feign, by default, will automatically retry `IOException`s, regardless of HTTP method, treating them as transient network
493+
related exceptions, and any `RetryableException` thrown from an `ErrorDecoder`. To customize this
494+
behavior, register a custom `Retryer` instance via the builder.
495+
496+
```java
497+
MyApi myApi = Feign.builder()
498+
.retryer(new MyRetryer())
499+
.target(MyApi.class, "https://api.hostname.com");
500+
```
501+
502+
`Retryer`s are responsible for determining if a retry should occur by returning either a `true` or
503+
`false` from the method `continueOrPropagate(RetryableException e);` A `Retryer` instance will be
504+
created for each `Client` execution, allowing you to maintain state bewteen each request if desired.
505+
If the retry is determined to be unsucessful, the last `RetryException` will be thrown.
506+
476507
#### Static and Default Methods
477508
Interfaces targeted by Feign may have static or default methods (if using Java 8+).
478509
These allows Feign clients to contain logic that is not expressly defined by the underlying API.

benchmark/pom.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,17 @@
6666
<dependency>
6767
<groupId>io.reactivex</groupId>
6868
<artifactId>rxnetty</artifactId>
69-
<version>0.4.14</version>
69+
<version>0.5.1</version>
7070
</dependency>
7171
<dependency>
72-
<groupId>io.reactivex</groupId>
73-
<artifactId>rxjava</artifactId>
74-
<version>1.0.17</version>
72+
<groupId>io.netty</groupId>
73+
<artifactId>netty-buffer</artifactId>
74+
<version>4.1.0.Beta7</version>
7575
</dependency>
7676
<dependency>
77-
<groupId>io.netty</groupId>
78-
<artifactId>netty-codec-http</artifactId>
79-
<version>4.1.0.Beta8</version>
77+
<groupId>io.reactivex</groupId>
78+
<artifactId>rxjava</artifactId>
79+
<version>1.0.14</version>
8080
</dependency>
8181
<dependency>
8282
<groupId>org.openjdk.jmh</groupId>

benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package feign.benchmark;
1515

1616
import com.fasterxml.jackson.core.type.TypeReference;
17+
import feign.Request;
1718
import feign.Response;
1819
import feign.Util;
1920
import feign.codec.Decoder;
@@ -78,6 +79,7 @@ public void buildResponse() {
7879
response = Response.builder()
7980
.status(200)
8081
.reason("OK")
82+
.request(Request.create("GET", "/", Collections.emptyMap(), null, Util.UTF_8))
8183
.headers(Collections.emptyMap())
8284
.body(carsJson(Integer.valueOf(size)), Util.UTF_8)
8385
.build();

benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
*/
1414
package feign.benchmark;
1515

16+
import feign.Logger;
17+
import feign.Logger.Level;
18+
import feign.Retryer;
19+
import io.netty.buffer.ByteBuf;
20+
import io.reactivex.netty.RxNetty;
21+
import io.reactivex.netty.protocol.http.server.HttpServer;
22+
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
23+
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
24+
import io.reactivex.netty.protocol.http.server.RequestHandler;
25+
import io.reactivex.netty.server.ErrorHandler;
1626
import okhttp3.OkHttpClient;
1727
import okhttp3.Request;
1828
import org.openjdk.jmh.annotations.Benchmark;
@@ -30,12 +40,7 @@
3040
import java.util.concurrent.TimeUnit;
3141
import feign.Feign;
3242
import feign.Response;
33-
import io.netty.buffer.ByteBuf;
34-
import io.reactivex.netty.RxNetty;
35-
import io.reactivex.netty.protocol.http.server.HttpServer;
36-
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
37-
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
38-
import io.reactivex.netty.protocol.http.server.RequestHandler;
43+
import rx.Observable;
3944

4045
@Measurement(iterations = 5, time = 1)
4146
@Warmup(iterations = 10, time = 1)
@@ -53,17 +58,15 @@ public class RealRequestBenchmarks {
5358

5459
@Setup
5560
public void setup() {
56-
server = RxNetty.createHttpServer(SERVER_PORT, new RequestHandler<ByteBuf, ByteBuf>() {
57-
public rx.Observable handle(HttpServerRequest<ByteBuf> request,
58-
HttpServerResponse<ByteBuf> response) {
59-
return response.flush();
60-
}
61-
});
61+
server = RxNetty.createHttpServer(SERVER_PORT, (request, response) -> response.flush());
6262
server.start();
6363
client = new OkHttpClient();
6464
client.retryOnConnectionFailure();
6565
okFeign = Feign.builder()
6666
.client(new feign.okhttp.OkHttpClient(client))
67+
.logLevel(Level.NONE)
68+
.logger(new Logger.ErrorLogger())
69+
.retryer(new Retryer.Default())
6770
.target(FeignTestInterface.class, "http://localhost:" + SERVER_PORT);
6871
queryRequest = new Request.Builder()
6972
.url("http://localhost:" + SERVER_PORT + "/?Action=GetUser&Version=2010-05-08&limit=1")
@@ -89,7 +92,10 @@ public okhttp3.Response query_baseCaseUsingOkHttp() throws IOException {
8992
* How fast can we execute get commands synchronously using Feign?
9093
*/
9194
@Benchmark
92-
public Response query_feignUsingOkHttp() {
93-
return okFeign.query();
95+
public boolean query_feignUsingOkHttp() {
96+
/* auto close the response */
97+
try (Response ignored = okFeign.query()) {
98+
return true;
99+
}
94100
}
95101
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVeri
6565
@Override
6666
public Response execute(Request request, Options options) throws IOException {
6767
HttpURLConnection connection = convertAndSend(request, options);
68-
return convertResponse(connection).toBuilder().request(request).build();
68+
return convertResponse(connection, request).toBuilder().request(request).build();
6969
}
7070

7171
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
@@ -139,7 +139,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
139139
return connection;
140140
}
141141

142-
Response convertResponse(HttpURLConnection connection) throws IOException {
142+
Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
143143
int status = connection.getResponseCode();
144144
String reason = connection.getResponseMessage();
145145

@@ -170,6 +170,7 @@ Response convertResponse(HttpURLConnection connection) throws IOException {
170170
.status(status)
171171
.reason(reason)
172172
.headers(headers)
173+
.request(request)
173174
.body(stream, length)
174175
.build();
175176
}

core/src/main/java/feign/FeignException.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
*/
1414
package feign;
1515

16-
import java.io.IOException;
1716
import static java.lang.String.format;
17+
import java.io.IOException;
1818

1919
/**
2020
* Origin exception type for all Http Apis.
@@ -61,7 +61,9 @@ public static FeignException errorStatus(String methodKey, Response response) {
6161

6262
static FeignException errorExecuting(Request request, IOException cause) {
6363
return new RetryableException(
64-
format("%s executing %s %s", cause.getMessage(), request.method(), request.url()), cause,
64+
format("%s executing %s %s", cause.getMessage(), request.method(), request.url()),
65+
request.method(),
66+
cause,
6567
null);
6668
}
6769
}

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,22 @@
1313
*/
1414
package feign;
1515

16+
import static feign.Util.checkNotNull;
17+
import static feign.Util.valuesOrEmpty;
1618
import java.net.HttpURLConnection;
1719
import java.nio.charset.Charset;
1820
import java.util.Collection;
1921
import java.util.Map;
20-
import static feign.Util.checkNotNull;
21-
import static feign.Util.valuesOrEmpty;
2222

2323
/**
2424
* An immutable request to an http server.
2525
*/
2626
public final class Request {
2727

28+
public enum Methods {
29+
GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
30+
}
31+
2832
/**
2933
* No parameters can be null except {@code body} and {@code charset}. All parameters must be
3034
* effectively immutable, via safe copies, not mutating or otherwise.
@@ -37,15 +41,15 @@ public static Request create(String method,
3741
return new Request(method, url, headers, body, charset);
3842
}
3943

40-
private final String method;
44+
private final Methods method;
4145
private final String url;
4246
private final Map<String, Collection<String>> headers;
4347
private final byte[] body;
4448
private final Charset charset;
4549

4650
Request(String method, String url, Map<String, Collection<String>> headers, byte[] body,
4751
Charset charset) {
48-
this.method = checkNotNull(method, "method of %s", url);
52+
this.method = Methods.valueOf(checkNotNull(method, "method of %s", url));
4953
this.url = checkNotNull(url, "url");
5054
this.headers = checkNotNull(headers, "headers of %s %s", method, url);
5155
this.body = body; // nullable
@@ -54,7 +58,7 @@ public static Request create(String method,
5458

5559
/* Method to invoke on the server. */
5660
public String method() {
57-
return method;
61+
return method.name();
5862
}
5963

6064
/* Fully resolved URL including query. */

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public static String expand(String template, Map<String, ?> variables) {
166166
}
167167

168168
private static Map<String, Collection<String>> parseAndDecodeQueries(String queryLine) {
169-
Map<String, Collection<String>> map = new LinkedHashMap<String, Collection<String>>();
169+
Map<String, Collection<String>> map = new LinkedHashMap<>();
170170
if (emptyToNull(queryLine) == null) {
171171
return map;
172172
}

core/src/main/java/feign/Response.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@ public final class Response implements Closeable {
4545

4646
private Response(Builder builder) {
4747
checkState(builder.status >= 200, "Invalid status code: %s", builder.status);
48+
checkState(builder.request != null, "original request is required");
4849
this.status = builder.status;
50+
this.request = builder.request;
4951
this.reason = builder.reason; // nullable
5052
this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers));
5153
this.body = builder.body; // nullable
52-
this.request = builder.request; // nullable
5354
}
5455

5556
public Builder toBuilder() {
@@ -121,12 +122,13 @@ public Builder body(String text, Charset charset) {
121122

122123
/**
123124
* @see Response#request
124-
*
125-
* NOTE: will add null check in version 10 which may require changes to custom feign.Client
126-
* or loggers
127125
*/
128126
public Builder request(Request request) {
129-
this.request = request;
127+
checkNotNull(request, "the original request is required on all responses");
128+
129+
/* don't keep the body, we don't want to tie up memory on large requests */
130+
this.request = Request.create(
131+
request.method(), request.url(), request.headers(), null, request.charset());
130132
return this;
131133
}
132134

0 commit comments

Comments
 (0)