Skip to content

Commit 715ec39

Browse files
authored
Adding Method to Retryable Exception for evaluation (#744)
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. * Refactored Request Method Attribute on Requests * Added `HttpMethod` enum that represents the supported HTTP methods replacing String handling. * Deprecated `Request#method()` in favor of `Request#httpMethod()`
1 parent 6409b25 commit 715ec39

File tree

27 files changed

+240
-58
lines changed

27 files changed

+240
-58
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
@@ -470,6 +470,37 @@ MyApi myApi = Feign.builder()
470470
.target(MyApi.class, "https://api.hostname.com");
471471
```
472472

473+
### Error Handling
474+
If you need more control over handling unexpected responses, Feign instances can
475+
register a custom `ErrorDecoder` via the builder.
476+
477+
```java
478+
MyApi myApi = Feign.builder()
479+
.errorDecoder(new MyErrorDecoder())
480+
.target(MyApi.class, "https://api.hostname.com");
481+
```
482+
483+
All responses that result in an HTTP status not in the 2xx range will trigger the `ErrorDecoder`'s `decode` method, allowing
484+
you to handle the response, wrap the failure into a custom exception or perform any additional processing.
485+
If you want to retry the request again, throw a `RetryableException`. This will invoke the registered
486+
`Retyer`.
487+
488+
### Retry
489+
Feign, by default, will automatically retry `IOException`s, regardless of HTTP method, treating them as transient network
490+
related exceptions, and any `RetryableException` thrown from an `ErrorDecoder`. To customize this
491+
behavior, register a custom `Retryer` instance via the builder.
492+
493+
```java
494+
MyApi myApi = Feign.builder()
495+
.retryer(new MyRetryer())
496+
.target(MyApi.class, "https://api.hostname.com");
497+
```
498+
499+
`Retryer`s are responsible for determining if a retry should occur by returning either a `true` or
500+
`false` from the method `continueOrPropagate(RetryableException e);` A `Retryer` instance will be
501+
created for each `Client` execution, allowing you to maintain state bewteen each request if desired.
502+
If the retry is determined to be unsucessful, the last `RetryException` will be thrown.
503+
473504
#### Static and Default Methods
474505
Interfaces targeted by Feign may have static or default methods (if using Java 8+).
475506
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;
@@ -77,6 +78,7 @@ public void buildResponse() {
7778
Response.builder()
7879
.status(200)
7980
.reason("OK")
81+
.request(Request.create("GET", "/", Collections.emptyMap(), null, Util.UTF_8))
8082
.headers(Collections.emptyMap())
8183
.body(carsJson(Integer.valueOf(size)), Util.UTF_8)
8284
.build();

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

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
package feign.benchmark;
1515

1616
import feign.Feign;
17+
import feign.Logger;
18+
import feign.Logger.Level;
1719
import feign.Response;
20+
import feign.Retryer;
1821
import io.netty.buffer.ByteBuf;
1922
import io.reactivex.netty.RxNetty;
2023
import io.reactivex.netty.protocol.http.server.HttpServer;
21-
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
22-
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
23-
import io.reactivex.netty.protocol.http.server.RequestHandler;
2424
import java.io.IOException;
2525
import java.util.concurrent.TimeUnit;
2626
import okhttp3.OkHttpClient;
@@ -53,21 +53,16 @@ public class RealRequestBenchmarks {
5353

5454
@Setup
5555
public void setup() {
56-
server =
57-
RxNetty.createHttpServer(
58-
SERVER_PORT,
59-
new RequestHandler<ByteBuf, ByteBuf>() {
60-
public rx.Observable handle(
61-
HttpServerRequest<ByteBuf> request, HttpServerResponse<ByteBuf> response) {
62-
return response.flush();
63-
}
64-
});
56+
server = RxNetty.createHttpServer(SERVER_PORT, (request, response) -> response.flush());
6557
server.start();
6658
client = new OkHttpClient();
6759
client.retryOnConnectionFailure();
6860
okFeign =
6961
Feign.builder()
7062
.client(new feign.okhttp.OkHttpClient(client))
63+
.logLevel(Level.NONE)
64+
.logger(new Logger.ErrorLogger())
65+
.retryer(new Retryer.Default())
7166
.target(FeignTestInterface.class, "http://localhost:" + SERVER_PORT);
7267
queryRequest =
7368
new Request.Builder()
@@ -90,7 +85,10 @@ public okhttp3.Response query_baseCaseUsingOkHttp() throws IOException {
9085

9186
/** How fast can we execute get commands synchronously using Feign? */
9287
@Benchmark
93-
public Response query_feignUsingOkHttp() {
94-
return okFeign.query();
88+
public boolean query_feignUsingOkHttp() {
89+
/* auto close the response */
90+
try (Response ignored = okFeign.query()) {
91+
return true;
92+
}
9593
}
9694
}

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

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

6868
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
@@ -81,7 +81,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
8181
connection.setReadTimeout(options.readTimeoutMillis());
8282
connection.setAllowUserInteraction(false);
8383
connection.setInstanceFollowRedirects(options.isFollowRedirects());
84-
connection.setRequestMethod(request.method());
84+
connection.setRequestMethod(request.httpMethod().name());
8585

8686
Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
8787
boolean gzipEncodedRequest =
@@ -136,7 +136,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
136136
return connection;
137137
}
138138

139-
Response convertResponse(HttpURLConnection connection) throws IOException {
139+
Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
140140
int status = connection.getResponseCode();
141141
String reason = connection.getResponseMessage();
142142

@@ -169,6 +169,7 @@ Response convertResponse(HttpURLConnection connection) throws IOException {
169169
.status(status)
170170
.reason(reason)
171171
.headers(headers)
172+
.request(request)
172173
.body(stream, length)
173174
.build();
174175
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public int status() {
4242

4343
static FeignException errorReading(Request request, Response ignored, IOException cause) {
4444
return new FeignException(
45-
format("%s reading %s %s", cause.getMessage(), request.method(), request.url()), cause);
45+
format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()), cause);
4646
}
4747

4848
public static FeignException errorStatus(String methodKey, Response response) {
@@ -59,7 +59,8 @@ public static FeignException errorStatus(String methodKey, Response response) {
5959

6060
static FeignException errorExecuting(Request request, IOException cause) {
6161
return new RetryableException(
62-
format("%s executing %s %s", cause.getMessage(), request.method(), request.url()),
62+
format("%s executing %s %s", cause.getMessage(), request.httpMethod(), request.url()),
63+
request.httpMethod(),
6364
cause,
6465
null);
6566
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ protected static String methodTag(String configKey) {
4646
protected abstract void log(String configKey, String format, Object... args);
4747

4848
protected void logRequest(String configKey, Level logLevel, Request request) {
49-
log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url());
49+
log(configKey, "---> %s %s HTTP/1.1", request.httpMethod().name(), request.url());
5050
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
5151

5252
for (String field : request.headers().keySet()) {

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

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,41 +24,90 @@
2424
/** An immutable request to an http server. */
2525
public final class Request {
2626

27+
public enum HttpMethod {
28+
GET,
29+
HEAD,
30+
POST,
31+
PUT,
32+
DELETE,
33+
CONNECT,
34+
OPTIONS,
35+
TRACE,
36+
PATCH
37+
}
38+
2739
/**
2840
* No parameters can be null except {@code body} and {@code charset}. All parameters must be
2941
* effectively immutable, via safe copies, not mutating or otherwise.
42+
*
43+
* @deprecated {@link #create(HttpMethod, String, Map, byte[], Charset)}
3044
*/
3145
public static Request create(
3246
String method,
3347
String url,
3448
Map<String, Collection<String>> headers,
3549
byte[] body,
3650
Charset charset) {
37-
return new Request(method, url, headers, body, charset);
51+
checkNotNull(method, "httpMethod of %s", method);
52+
HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
53+
return create(httpMethod, url, headers, body, charset);
54+
}
55+
56+
/**
57+
* Builds a Request. All parameters must be effectively immutable, via safe copies.
58+
*
59+
* @param httpMethod for the request.
60+
* @param url for the request.
61+
* @param headers to include.
62+
* @param body of the request, can be {@literal null}
63+
* @param charset of the request, can be {@literal null}
64+
* @return a Request
65+
*/
66+
public static Request create(
67+
HttpMethod httpMethod,
68+
String url,
69+
Map<String, Collection<String>> headers,
70+
byte[] body,
71+
Charset charset) {
72+
return new Request(httpMethod, url, headers, body, charset);
3873
}
3974

40-
private final String method;
75+
private final HttpMethod httpMethod;
4176
private final String url;
4277
private final Map<String, Collection<String>> headers;
4378
private final byte[] body;
4479
private final Charset charset;
4580

4681
Request(
47-
String method,
82+
HttpMethod method,
4883
String url,
4984
Map<String, Collection<String>> headers,
5085
byte[] body,
5186
Charset charset) {
52-
this.method = checkNotNull(method, "method of %s", url);
87+
this.httpMethod = checkNotNull(method, "httpMethod of %s", method.name());
5388
this.url = checkNotNull(url, "url");
5489
this.headers = checkNotNull(headers, "headers of %s %s", method, url);
5590
this.body = body; // nullable
5691
this.charset = charset; // nullable
5792
}
5893

59-
/* Method to invoke on the server. */
94+
/**
95+
* Http Method for this request.
96+
*
97+
* @return the HttpMethod string
98+
* @deprecated @see {@link #httpMethod()}
99+
*/
60100
public String method() {
61-
return method;
101+
return httpMethod.name();
102+
}
103+
104+
/**
105+
* Http Method for the request.
106+
*
107+
* @return the HttpMethod.
108+
*/
109+
public HttpMethod httpMethod() {
110+
return this.httpMethod;
62111
}
63112

64113
/* Fully resolved URL including query. */
@@ -93,7 +142,7 @@ public byte[] body() {
93142
@Override
94143
public String toString() {
95144
StringBuilder builder = new StringBuilder();
96-
builder.append(method).append(' ').append(url).append(" HTTP/1.1\n");
145+
builder.append(httpMethod).append(' ').append(url).append(" HTTP/1.1\n");
97146
for (String field : headers.keySet()) {
98147
for (String value : valuesOrEmpty(headers, field)) {
99148
builder.append(field).append(": ").append(value).append('\n');

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

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

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

0 commit comments

Comments
 (0)