Skip to content

Commit 9875a16

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 68f04c9 commit 9875a16

File tree

28 files changed

+225
-65
lines changed

28 files changed

+225
-65
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;
@@ -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: 4 additions & 3 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);
6969
}
7070

7171
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
@@ -84,7 +84,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
8484
connection.setReadTimeout(options.readTimeoutMillis());
8585
connection.setAllowUserInteraction(false);
8686
connection.setInstanceFollowRedirects(options.isFollowRedirects());
87-
connection.setRequestMethod(request.method());
87+
connection.setRequestMethod(request.httpMethod().name());
8888

8989
Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
9090
boolean gzipEncodedRequest =
@@ -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: 5 additions & 3 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.
@@ -43,7 +43,7 @@ public int status() {
4343

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

@@ -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.httpMethod(), request.url()),
65+
request.httpMethod(),
66+
cause,
6567
null);
6668
}
6769
}

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

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

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

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

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

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,48 +13,89 @@
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 HttpMethod {
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.
35+
*
36+
* @deprecated {@link #create(HttpMethod, String, Map, byte[], Charset)}
3137
*/
3238
public static Request create(String method,
3339
String url,
3440
Map<String, Collection<String>> headers,
3541
byte[] body,
3642
Charset charset) {
37-
return new Request(method, url, headers, body, charset);
43+
checkNotNull(method, "httpMethod of %s", method);
44+
HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
45+
return create(httpMethod, url, headers, body, charset);
3846
}
3947

40-
private final String method;
48+
/**
49+
* Builds a Request. All parameters must be effectively immutable, via safe copies.
50+
*
51+
* @param httpMethod for the request.
52+
* @param url for the request.
53+
* @param headers to include.
54+
* @param body of the request, can be {@literal null}
55+
* @param charset of the request, can be {@literal null}
56+
* @return a Request
57+
*/
58+
public static Request create(HttpMethod httpMethod,
59+
String url,
60+
Map<String, Collection<String>> headers,
61+
byte[] body,
62+
Charset charset) {
63+
return new Request(httpMethod, url, headers, body, charset);
64+
65+
}
66+
67+
private final HttpMethod httpMethod;
4168
private final String url;
4269
private final Map<String, Collection<String>> headers;
4370
private final byte[] body;
4471
private final Charset charset;
4572

46-
Request(String method, String url, Map<String, Collection<String>> headers, byte[] body,
73+
Request(HttpMethod method, String url, Map<String, Collection<String>> headers, byte[] body,
4774
Charset charset) {
48-
this.method = checkNotNull(method, "method of %s", url);
75+
this.httpMethod = checkNotNull(method, "httpMethod of %s", method.name());
4976
this.url = checkNotNull(url, "url");
5077
this.headers = checkNotNull(headers, "headers of %s %s", method, url);
5178
this.body = body; // nullable
5279
this.charset = charset; // nullable
5380
}
5481

55-
/* Method to invoke on the server. */
82+
/**
83+
* Http Method for this request.
84+
*
85+
* @return the HttpMethod string
86+
* @deprecated @see {@link #httpMethod()}
87+
*/
5688
public String method() {
57-
return method;
89+
return httpMethod.name();
90+
}
91+
92+
/**
93+
* Http Method for the request.
94+
*
95+
* @return the HttpMethod.
96+
*/
97+
public HttpMethod httpMethod() {
98+
return this.httpMethod;
5899
}
59100

60101
/* Fully resolved URL including query. */
@@ -89,7 +130,7 @@ public byte[] body() {
89130
@Override
90131
public String toString() {
91132
StringBuilder builder = new StringBuilder();
92-
builder.append(method).append(' ').append(url).append(" HTTP/1.1\n");
133+
builder.append(httpMethod).append(' ').append(url).append(" HTTP/1.1\n");
93134
for (String field : headers.keySet()) {
94135
for (String value : valuesOrEmpty(headers, field)) {
95136
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
@@ -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
}

0 commit comments

Comments
 (0)