diff --git a/CHANGELOG.md b/CHANGELOG.md index 220fa35c1..c26ac6596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ### Version 10.0 * Feign baseline is now JDK 8 * Removed @Deprecated methods marked for removal on feign 10 +* `RetryException` includes the `Method` used for the offending `Request` +* `Response` objects now contain the `Request` used. ### Version 9.6 * Feign builder now supports flag `doNotCloseAfterDecode` to support lazy iteration of responses. diff --git a/README.md b/README.md index d38540f61..46c6e3a24 100644 --- a/README.md +++ b/README.md @@ -470,6 +470,37 @@ MyApi myApi = Feign.builder() .target(MyApi.class, "https://api.hostname.com"); ``` +### Error Handling +If you need more control over handling unexpected responses, Feign instances can +register a custom `ErrorDecoder` via the builder. + +```java +MyApi myApi = Feign.builder() + .errorDecoder(new MyErrorDecoder()) + .target(MyApi.class, "https://api.hostname.com"); +``` + +All responses that result in an HTTP status not in the 2xx range will trigger the `ErrorDecoder`'s `decode` method, allowing +you to handle the response, wrap the failure into a custom exception or perform any additional processing. +If you want to retry the request again, throw a `RetryableException`. This will invoke the registered +`Retyer`. + +### Retry +Feign, by default, will automatically retry `IOException`s, regardless of HTTP method, treating them as transient network +related exceptions, and any `RetryableException` thrown from an `ErrorDecoder`. To customize this +behavior, register a custom `Retryer` instance via the builder. + +```java +MyApi myApi = Feign.builder() + .retryer(new MyRetryer()) + .target(MyApi.class, "https://api.hostname.com"); +``` + +`Retryer`s are responsible for determining if a retry should occur by returning either a `true` or +`false` from the method `continueOrPropagate(RetryableException e);` A `Retryer` instance will be +created for each `Client` execution, allowing you to maintain state bewteen each request if desired. +If the retry is determined to be unsucessful, the last `RetryException` will be thrown. + #### Static and Default Methods Interfaces targeted by Feign may have static or default methods (if using Java 8+). These allows Feign clients to contain logic that is not expressly defined by the underlying API. diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 1aeb823e0..b118c6f96 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -66,17 +66,17 @@ io.reactivex rxnetty - 0.4.14 + 0.5.1 - io.reactivex - rxjava - 1.0.17 + io.netty + netty-buffer + 4.1.0.Beta7 - io.netty - netty-codec-http - 4.1.0.Beta8 + io.reactivex + rxjava + 1.0.14 org.openjdk.jmh diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index 94c9d4e25..b006b384e 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -14,6 +14,7 @@ package feign.benchmark; import com.fasterxml.jackson.core.type.TypeReference; +import feign.Request; import feign.Response; import feign.Util; import feign.codec.Decoder; @@ -77,6 +78,7 @@ public void buildResponse() { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(carsJson(Integer.valueOf(size)), Util.UTF_8) .build(); diff --git a/benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java b/benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java index 2e4cd6e7c..22bd1f06a 100644 --- a/benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java +++ b/benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java @@ -14,13 +14,13 @@ package feign.benchmark; import feign.Feign; +import feign.Logger; +import feign.Logger.Level; import feign.Response; +import feign.Retryer; import io.netty.buffer.ByteBuf; import io.reactivex.netty.RxNetty; import io.reactivex.netty.protocol.http.server.HttpServer; -import io.reactivex.netty.protocol.http.server.HttpServerRequest; -import io.reactivex.netty.protocol.http.server.HttpServerResponse; -import io.reactivex.netty.protocol.http.server.RequestHandler; import java.io.IOException; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; @@ -53,21 +53,16 @@ public class RealRequestBenchmarks { @Setup public void setup() { - server = - RxNetty.createHttpServer( - SERVER_PORT, - new RequestHandler() { - public rx.Observable handle( - HttpServerRequest request, HttpServerResponse response) { - return response.flush(); - } - }); + server = RxNetty.createHttpServer(SERVER_PORT, (request, response) -> response.flush()); server.start(); client = new OkHttpClient(); client.retryOnConnectionFailure(); okFeign = Feign.builder() .client(new feign.okhttp.OkHttpClient(client)) + .logLevel(Level.NONE) + .logger(new Logger.ErrorLogger()) + .retryer(new Retryer.Default()) .target(FeignTestInterface.class, "http://localhost:" + SERVER_PORT); queryRequest = new Request.Builder() @@ -90,7 +85,10 @@ public okhttp3.Response query_baseCaseUsingOkHttp() throws IOException { /** How fast can we execute get commands synchronously using Feign? */ @Benchmark - public Response query_feignUsingOkHttp() { - return okFeign.query(); + public boolean query_feignUsingOkHttp() { + /* auto close the response */ + try (Response ignored = okFeign.query()) { + return true; + } } } diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java index 24667d20a..13fb9d079 100644 --- a/core/src/main/java/feign/Client.java +++ b/core/src/main/java/feign/Client.java @@ -62,7 +62,7 @@ public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVeri @Override public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = convertAndSend(request, options); - return convertResponse(connection).toBuilder().request(request).build(); + return convertResponse(connection, request); } HttpURLConnection convertAndSend(Request request, Options options) throws IOException { @@ -81,7 +81,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce connection.setReadTimeout(options.readTimeoutMillis()); connection.setAllowUserInteraction(false); connection.setInstanceFollowRedirects(options.isFollowRedirects()); - connection.setRequestMethod(request.method()); + connection.setRequestMethod(request.httpMethod().name()); Collection contentEncodingValues = request.headers().get(CONTENT_ENCODING); boolean gzipEncodedRequest = @@ -136,7 +136,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce return connection; } - Response convertResponse(HttpURLConnection connection) throws IOException { + Response convertResponse(HttpURLConnection connection, Request request) throws IOException { int status = connection.getResponseCode(); String reason = connection.getResponseMessage(); @@ -169,6 +169,7 @@ Response convertResponse(HttpURLConnection connection) throws IOException { .status(status) .reason(reason) .headers(headers) + .request(request) .body(stream, length) .build(); } diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java index cb3e82d44..f780be0b3 100644 --- a/core/src/main/java/feign/FeignException.java +++ b/core/src/main/java/feign/FeignException.java @@ -42,7 +42,7 @@ public int status() { static FeignException errorReading(Request request, Response ignored, IOException cause) { return new FeignException( - format("%s reading %s %s", cause.getMessage(), request.method(), request.url()), cause); + format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()), cause); } public static FeignException errorStatus(String methodKey, Response response) { @@ -59,7 +59,8 @@ public static FeignException errorStatus(String methodKey, Response response) { static FeignException errorExecuting(Request request, IOException cause) { return new RetryableException( - format("%s executing %s %s", cause.getMessage(), request.method(), request.url()), + format("%s executing %s %s", cause.getMessage(), request.httpMethod(), request.url()), + request.httpMethod(), cause, null); } diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java index e22c08c64..e523f1a32 100644 --- a/core/src/main/java/feign/Logger.java +++ b/core/src/main/java/feign/Logger.java @@ -46,7 +46,7 @@ protected static String methodTag(String configKey) { protected abstract void log(String configKey, String format, Object... args); protected void logRequest(String configKey, Level logLevel, Request request) { - log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url()); + log(configKey, "---> %s %s HTTP/1.1", request.httpMethod().name(), request.url()); if (logLevel.ordinal() >= Level.HEADERS.ordinal()) { for (String field : request.headers().keySet()) { diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 98b0d6d7c..5becfea6a 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -24,9 +24,23 @@ /** An immutable request to an http server. */ public final class Request { + public enum HttpMethod { + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH + } + /** * No parameters can be null except {@code body} and {@code charset}. All parameters must be * effectively immutable, via safe copies, not mutating or otherwise. + * + * @deprecated {@link #create(HttpMethod, String, Map, byte[], Charset)} */ public static Request create( String method, @@ -34,31 +48,66 @@ public static Request create( Map> headers, byte[] body, Charset charset) { - return new Request(method, url, headers, body, charset); + checkNotNull(method, "httpMethod of %s", method); + HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase()); + return create(httpMethod, url, headers, body, charset); + } + + /** + * Builds a Request. All parameters must be effectively immutable, via safe copies. + * + * @param httpMethod for the request. + * @param url for the request. + * @param headers to include. + * @param body of the request, can be {@literal null} + * @param charset of the request, can be {@literal null} + * @return a Request + */ + public static Request create( + HttpMethod httpMethod, + String url, + Map> headers, + byte[] body, + Charset charset) { + return new Request(httpMethod, url, headers, body, charset); } - private final String method; + private final HttpMethod httpMethod; private final String url; private final Map> headers; private final byte[] body; private final Charset charset; Request( - String method, + HttpMethod method, String url, Map> headers, byte[] body, Charset charset) { - this.method = checkNotNull(method, "method of %s", url); + this.httpMethod = checkNotNull(method, "httpMethod of %s", method.name()); this.url = checkNotNull(url, "url"); this.headers = checkNotNull(headers, "headers of %s %s", method, url); this.body = body; // nullable this.charset = charset; // nullable } - /* Method to invoke on the server. */ + /** + * Http Method for this request. + * + * @return the HttpMethod string + * @deprecated @see {@link #httpMethod()} + */ public String method() { - return method; + return httpMethod.name(); + } + + /** + * Http Method for the request. + * + * @return the HttpMethod. + */ + public HttpMethod httpMethod() { + return this.httpMethod; } /* Fully resolved URL including query. */ @@ -93,7 +142,7 @@ public byte[] body() { @Override public String toString() { StringBuilder builder = new StringBuilder(); - builder.append(method).append(' ').append(url).append(" HTTP/1.1\n"); + builder.append(httpMethod).append(' ').append(url).append(" HTTP/1.1\n"); for (String field : headers.keySet()) { for (String value : valuesOrEmpty(headers, field)) { builder.append(field).append(": ").append(value).append('\n'); diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 53ee1ee11..9ca6e9fc4 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -168,7 +168,7 @@ public static String expand(String template, Map variables) { } private static Map> parseAndDecodeQueries(String queryLine) { - Map> map = new LinkedHashMap>(); + Map> map = new LinkedHashMap<>(); if (emptyToNull(queryLine) == null) { return map; } diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index c50da0e4b..4f8fa17e8 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -44,11 +44,12 @@ public final class Response implements Closeable { private Response(Builder builder) { checkState(builder.status >= 200, "Invalid status code: %s", builder.status); + checkState(builder.request != null, "original request is required"); this.status = builder.status; + this.request = builder.request; this.reason = builder.reason; // nullable this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers)); this.body = builder.body; // nullable - this.request = builder.request; // nullable } public Builder toBuilder() { @@ -134,10 +135,9 @@ public Builder body(String text, Charset charset) { /** * @see Response#request - *

NOTE: will add null check in version 10 which may require changes to custom - * feign.Client or loggers */ public Builder request(Request request) { + checkNotNull(request, "request is required"); this.request = request; return this; } @@ -175,7 +175,7 @@ public Body body() { return body; } - /** if present, the request that generated this response */ + /** the request that generated this response */ public Request request() { return request; } diff --git a/core/src/main/java/feign/RetryableException.java b/core/src/main/java/feign/RetryableException.java index 5c433b8d9..fa26050fa 100644 --- a/core/src/main/java/feign/RetryableException.java +++ b/core/src/main/java/feign/RetryableException.java @@ -13,6 +13,7 @@ */ package feign; +import feign.Request.HttpMethod; import java.util.Date; /** @@ -24,20 +25,24 @@ public class RetryableException extends FeignException { private static final long serialVersionUID = 1L; private final Long retryAfter; + private final HttpMethod httpMethod; /** * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} header. */ - public RetryableException(String message, Throwable cause, Date retryAfter) { + public RetryableException( + String message, HttpMethod httpMethod, Throwable cause, Date retryAfter) { super(message, cause); + this.httpMethod = httpMethod; this.retryAfter = retryAfter != null ? retryAfter.getTime() : null; } /** * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} header. */ - public RetryableException(String message, Date retryAfter) { + public RetryableException(String message, HttpMethod httpMethod, Date retryAfter) { super(message); + this.httpMethod = httpMethod; this.retryAfter = retryAfter != null ? retryAfter.getTime() : null; } @@ -48,4 +53,8 @@ public RetryableException(String message, Date retryAfter) { public Date retryAfter() { return retryAfter != null ? new Date(retryAfter) : null; } + + public HttpMethod method() { + return this.httpMethod; + } } diff --git a/core/src/main/java/feign/codec/ErrorDecoder.java b/core/src/main/java/feign/codec/ErrorDecoder.java index 89791fd3a..a449544d5 100644 --- a/core/src/main/java/feign/codec/ErrorDecoder.java +++ b/core/src/main/java/feign/codec/ErrorDecoder.java @@ -89,7 +89,8 @@ public Exception decode(String methodKey, Response response) { FeignException exception = errorStatus(methodKey, response); Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER)); if (retryAfter != null) { - return new RetryableException(exception.getMessage(), exception, retryAfter); + return new RetryableException( + exception.getMessage(), response.request().httpMethod(), exception, retryAfter); } return exception; } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index ccd3fc925..a6a60cae4 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -22,6 +22,7 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import feign.Feign.ResponseMappingDecoder; +import feign.Request.HttpMethod; import feign.Target.HardCodedTarget; import feign.codec.DecodeException; import feign.codec.Decoder; @@ -476,7 +477,7 @@ public void retryableExceptionInDecoder() throws Exception { public Object decode(Response response, Type type) throws IOException { String string = super.decode(response, type).toString(); if ("retry!".equals(string)) { - throw new RetryableException(string, null); + throw new RetryableException(string, HttpMethod.POST, null); } return string; } @@ -523,7 +524,7 @@ public void ensureRetryerClonesItself() { new ErrorDecoder() { @Override public Exception decode(String methodKey, Response response) { - return new RetryableException("play it again sam!", null); + return new RetryableException("play it again sam!", HttpMethod.POST, null); } }) .target(TestInterface.class, "http://localhost:" + server.getPort()); @@ -538,7 +539,13 @@ public void whenReturnTypeIsResponseNoErrorHandling() { Map> headers = new LinkedHashMap>(); headers.put("Location", Arrays.asList("http://bar.com")); final Response response = - Response.builder().status(302).reason("Found").headers(headers).body(new byte[0]).build(); + Response.builder() + .status(302) + .reason("Found") + .headers(headers) + .request(Request.create("GET", "/", Collections.emptyMap(), null, Util.UTF_8)) + .body(new byte[0]) + .build(); TestInterface api = Feign.builder() @@ -739,6 +746,7 @@ private Response responseWithText(String text) { return Response.builder() .body(text, Util.UTF_8) .status(200) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(new HashMap>()) .build(); } diff --git a/core/src/test/java/feign/ResponseTest.java b/core/src/test/java/feign/ResponseTest.java index 03440f1e8..19ee6bf5b 100644 --- a/core/src/test/java/feign/ResponseTest.java +++ b/core/src/test/java/feign/ResponseTest.java @@ -32,6 +32,7 @@ public void reasonPhraseIsOptional() { Response.builder() .status(200) .headers(Collections.>emptyMap()) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -45,7 +46,12 @@ public void canAccessHeadersCaseInsensitively() { List valueList = Collections.singletonList("application/json"); headersMap.put("Content-Type", valueList); Response response = - Response.builder().status(200).headers(headersMap).body(new byte[0]).build(); + Response.builder() + .status(200) + .headers(headersMap) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) + .body(new byte[0]) + .build(); assertThat(response.headers().get("content-type")).isEqualTo(valueList); assertThat(response.headers().get("Content-Type")).isEqualTo(valueList); } @@ -57,7 +63,12 @@ public void headerValuesWithSameNameOnlyVaryingInCaseAreMerged() { headersMap.put("set-cookie", Arrays.asList("Cookie-C=Value")); Response response = - Response.builder().status(200).headers(headersMap).body(new byte[0]).build(); + Response.builder() + .status(200) + .headers(headersMap) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) + .body(new byte[0]) + .build(); List expectedHeaderValue = Arrays.asList("Cookie-A=Value", "Cookie-B=Value", "Cookie-C=Value"); diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java index 74669bb76..a4f3ed692 100644 --- a/core/src/test/java/feign/codec/DefaultDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -17,7 +17,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import feign.Request; import feign.Response; +import feign.Util; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Collection; @@ -73,6 +75,7 @@ private Response knownResponse() { .status(200) .reason("OK") .headers(headers) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(inputStream, content.length()) .build(); } @@ -82,6 +85,7 @@ private Response nullBodyResponse() { .status(200) .reason("OK") .headers(Collections.>emptyMap()) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .build(); } } diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index 00dce835f..bc80324de 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -18,9 +18,12 @@ import static org.assertj.core.api.Assertions.assertThat; import feign.FeignException; +import feign.Request; import feign.Response; +import feign.Util; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import org.junit.Rule; @@ -41,7 +44,12 @@ public void throwsFeignException() throws Throwable { thrown.expectMessage("status 500 reading Service#foo()"); Response response = - Response.builder().status(500).reason("Internal server error").headers(headers).build(); + Response.builder() + .status(500) + .reason("Internal server error") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(headers) + .build(); throw errorDecoder.decode("Service#foo()", response); } @@ -55,6 +63,7 @@ public void throwsFeignExceptionIncludingBody() throws Throwable { Response.builder() .status(500) .reason("Internal server error") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .body("hello world", UTF_8) .build(); @@ -65,7 +74,12 @@ public void throwsFeignExceptionIncludingBody() throws Throwable { @Test public void testFeignExceptionIncludesStatus() throws Throwable { Response response = - Response.builder().status(400).reason("Bad request").headers(headers).build(); + Response.builder() + .status(400) + .reason("Bad request") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(headers) + .build(); Exception exception = errorDecoder.decode("Service#foo()", response); @@ -80,7 +94,12 @@ public void retryAfterHeaderThrowsRetryableException() throws Throwable { headers.put(RETRY_AFTER, Arrays.asList("Sat, 1 Jan 2000 00:00:00 GMT")); Response response = - Response.builder().status(503).reason("Service Unavailable").headers(headers).build(); + Response.builder() + .status(503) + .reason("Service Unavailable") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(headers) + .build(); throw errorDecoder.decode("Service#foo()", response); } diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 8e86e9bf7..f95278bfa 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -18,8 +18,10 @@ import com.fasterxml.jackson.core.type.TypeReference; import feign.Feign; +import feign.Request; import feign.RequestLine; import feign.Response; +import feign.Util; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; @@ -86,6 +88,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException { .status(200) .reason("OK") .headers(Collections.emptyMap()) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .body("", UTF_8) .build(); diff --git a/gson/src/test/java/feign/gson/GsonCodecTest.java b/gson/src/test/java/feign/gson/GsonCodecTest.java index 043b85c1e..b1930bbe6 100644 --- a/gson/src/test/java/feign/gson/GsonCodecTest.java +++ b/gson/src/test/java/feign/gson/GsonCodecTest.java @@ -22,8 +22,10 @@ import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import feign.Request; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.io.IOException; import java.util.Arrays; import java.util.Collection; @@ -61,6 +63,7 @@ public void decodesMapObjectNumericalValuesAsInteger() throws Exception { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("{\"foo\": 1}", UTF_8) .build(); @@ -122,6 +125,7 @@ public void decodes() throws Exception { .status(200) .reason("OK") .headers(Collections.>emptyMap()) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(zonesJson, UTF_8) .build(); assertEquals( @@ -135,6 +139,7 @@ public void nullBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .headers(Collections.>emptyMap()) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .build(); assertNull(new GsonDecoder().decode(response, String.class)); } @@ -146,6 +151,7 @@ public void emptyBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .headers(Collections.>emptyMap()) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); assertNull(new GsonDecoder().decode(response, String.class)); @@ -200,6 +206,7 @@ public void customDecoder() throws Exception { .status(200) .reason("OK") .headers(Collections.>emptyMap()) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(zonesJson, UTF_8) .build(); assertEquals(zones, decoder.decode(response, new TypeToken>() {}.getType())); @@ -238,6 +245,7 @@ public void notFoundDecodesToEmpty() throws Exception { .status(404) .reason("NOT FOUND") .headers(Collections.>emptyMap()) + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .build(); assertThat((byte[]) new GsonDecoder().decode(response, byte[].class)).isEmpty(); } diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 0d159b67d..79e8f49f9 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -83,7 +83,7 @@ public Response execute(Request request, Request.Options options) throws IOExcep throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); } HttpResponse httpResponse = client.execute(httpUriRequest); - return toFeignResponse(httpResponse).toBuilder().request(request).build(); + return toFeignResponse(httpResponse, request); } HttpUriRequest toHttpUriRequest(Request request, Request.Options options) @@ -169,7 +169,7 @@ private ContentType getContentType(Request request) { return contentType; } - Response toFeignResponse(HttpResponse httpResponse) throws IOException { + Response toFeignResponse(HttpResponse httpResponse, Request request) throws IOException { StatusLine statusLine = httpResponse.getStatusLine(); int statusCode = statusLine.getStatusCode(); @@ -192,6 +192,7 @@ Response toFeignResponse(HttpResponse httpResponse) throws IOException { .status(statusCode) .reason(reason) .headers(headers) + .request(request) .body(toFeignBody(httpResponse)) .build(); } diff --git a/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java b/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java index 221e16df6..dfa8c9987 100644 --- a/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java +++ b/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java @@ -16,8 +16,10 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; +import feign.Request; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.util.Collection; import java.util.Collections; import javax.xml.bind.annotation.XmlAccessType; @@ -44,6 +46,7 @@ public void decodeTest() throws Exception { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("{\"value\":\"Test\"}", UTF_8) .build(); @@ -59,6 +62,7 @@ public void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat((byte[]) new JacksonJaxbJsonDecoder().decode(response, byte[].class)).isEmpty(); diff --git a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java index 62ec307a6..c73378972 100644 --- a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java @@ -29,8 +29,10 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import feign.Request; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; @@ -116,6 +118,7 @@ public void decodes() throws Exception { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -129,6 +132,7 @@ public void nullBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertNull(new JacksonDecoder().decode(response, String.class)); @@ -140,6 +144,7 @@ public void emptyBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(new byte[0]) .build(); @@ -161,6 +166,7 @@ public void customDecoder() throws Exception { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -207,6 +213,7 @@ public void decodesIterator() throws Exception { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -230,6 +237,7 @@ public void nullBodyDecodesToNullIterator() throws Exception { Response.builder() .status(204) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertNull(JacksonIteratorDecoder.create().decode(response, Iterator.class)); @@ -241,6 +249,7 @@ public void emptyBodyDecodesToNullIterator() throws Exception { Response.builder() .status(204) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(new byte[0]) .build(); @@ -313,6 +322,7 @@ public void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat((byte[]) new JacksonDecoder().decode(response, byte[].class)).isEmpty(); @@ -325,6 +335,7 @@ public void notFoundDecodesToEmptyIterator() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat((byte[]) JacksonIteratorDecoder.create().decode(response, byte[].class)).isEmpty(); diff --git a/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java b/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java index 4a75f03e2..fadfda1ce 100644 --- a/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java @@ -18,7 +18,9 @@ import static org.hamcrest.core.Is.isA; import com.fasterxml.jackson.databind.ObjectMapper; +import feign.Request; import feign.Response; +import feign.Util; import feign.codec.DecodeException; import feign.jackson.JacksonIteratorDecoder.JacksonIterator; import java.io.ByteArrayInputStream; @@ -88,6 +90,7 @@ public void close() throws IOException { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(inputStream, jsonBytes.length) .build(); @@ -113,6 +116,7 @@ public void close() throws IOException { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(inputStream, jsonBytes.length) .build(); @@ -143,6 +147,7 @@ JacksonIterator iterator(Class type, String json) throws IOException { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(json, UTF_8) .build(); diff --git a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java index 00ff70adc..de091d7f6 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -17,8 +17,10 @@ import static feign.assertj.FeignAssertions.assertThat; import static org.junit.Assert.assertEquals; +import feign.Request; import feign.RequestTemplate; import feign.Response; +import feign.Util; import feign.codec.Encoder; import java.lang.reflect.Type; import java.util.Collection; @@ -174,6 +176,7 @@ public void decodesXml() throws Exception { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(mockXml, UTF_8) .build(); @@ -199,6 +202,7 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("", UTF_8) .build(); @@ -213,6 +217,7 @@ public void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat( diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java index 38dd798be..03700847b 100644 --- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java @@ -90,12 +90,14 @@ static Request toOkHttpRequest(feign.Request input) { return requestBuilder.build(); } - private static feign.Response toFeignResponse(Response input) throws IOException { + private static feign.Response toFeignResponse(Response response, feign.Request request) + throws IOException { return feign.Response.builder() - .status(input.code()) - .reason(input.message()) - .headers(toMap(input.headers())) - .body(toBody(input.body())) + .status(response.code()) + .reason(response.message()) + .request(request) + .headers(toMap(response.headers())) + .body(toBody(response.body())) .build(); } @@ -162,6 +164,6 @@ public feign.Response execute(feign.Request input, feign.Request.Options options } Request request = toOkHttpRequest(input); Response response = requestScoped.newCall(request).execute(); - return toFeignResponse(response).toBuilder().request(input).build(); + return toFeignResponse(response, input).toBuilder().request(input).build(); } } diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java index 72409aac4..36e020545 100644 --- a/sax/src/test/java/feign/sax/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -18,7 +18,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import feign.Request; import feign.Response; +import feign.Util; import feign.codec.Decoder; import java.io.IOException; import java.text.ParseException; @@ -77,6 +79,7 @@ private Response statusFailedResponse() { return Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(statusFailed, UTF_8) .build(); @@ -88,6 +91,7 @@ public void nullBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertNull(decoder.decode(response, String.class)); @@ -100,6 +104,7 @@ public void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat((byte[]) decoder.decode(response, byte[].class)).isEmpty(); diff --git a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java index f3a054e4d..b5e1a9933 100644 --- a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java +++ b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java @@ -18,6 +18,7 @@ import feign.Request; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.util.Collection; import java.util.Collections; import org.junit.Rule; @@ -33,6 +34,7 @@ public class Slf4jLoggerTest { Response.builder() .status(200) .reason("OK") + .request(Request.create("GET", "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(new byte[0]) .build();