From 74c963e9875a5be58fc8640486c24f63aab76009 Mon Sep 17 00:00:00 2001 From: UzimakiNaruto Date: Sat, 19 Dec 2020 13:29:23 +0800 Subject: [PATCH 1/9] java11 async http client for AsyncFeign --- .../feign/http2client/AbstractHttpClient.java | 108 ++++++++++++++++++ .../feign/http2client/AsyncHttpClient.java | 58 ++++++++++ .../java/feign/http2client/Http2Client.java | 84 ++------------ 3 files changed, 178 insertions(+), 72 deletions(-) create mode 100644 java11/src/main/java/feign/http2client/AbstractHttpClient.java create mode 100644 java11/src/main/java/feign/http2client/AsyncHttpClient.java diff --git a/java11/src/main/java/feign/http2client/AbstractHttpClient.java b/java11/src/main/java/feign/http2client/AbstractHttpClient.java new file mode 100644 index 000000000..8de78d3f8 --- /dev/null +++ b/java11/src/main/java/feign/http2client/AbstractHttpClient.java @@ -0,0 +1,108 @@ +/** + * Copyright 2012-2020 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.http2client; + +import feign.Request; +import feign.Request.Options; +import feign.Response; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.OptionalLong; + +public abstract class AbstractHttpClient { + + protected Response toFeignResponse(Request request, HttpResponse httpResponse) { + final OptionalLong length = httpResponse.headers().firstValueAsLong("Content-Length"); + + return Response.builder() + .body(new ByteArrayInputStream(httpResponse.body()), + length.isPresent() ? (int) length.getAsLong() : null) + .reason(httpResponse.headers().firstValue("Reason-Phrase").orElse("OK")) + .request(request) + .status(httpResponse.statusCode()) + .headers(castMapCollectType(httpResponse.headers().map())) + .build(); + } + + protected Builder newRequestBuilder(Request request, Options options) throws URISyntaxException { + URI uri = new URI(request.url()); + + final BodyPublisher body; + final byte[] data = request.body(); + if (data == null) { + body = BodyPublishers.noBody(); + } else { + body = BodyPublishers.ofByteArray(data); + } + + final Builder requestBuilder = HttpRequest.newBuilder() + .uri(uri) + .timeout(Duration.ofMillis(options.readTimeoutMillis())); + + final Map> headers = filterRestrictedHeaders(request.headers()); + if (!headers.isEmpty()) { + requestBuilder.headers(asString(headers)); + } + + switch (request.httpMethod()) { + case GET: + return requestBuilder.GET(); + case POST: + return requestBuilder.POST(body); + case PUT: + return requestBuilder.PUT(body); + case DELETE: + return requestBuilder.DELETE(); + default: + // fall back scenario, http implementations may restrict some methods + return requestBuilder.method(request.httpMethod().toString(), body); + } + } + + protected Map> filterRestrictedHeaders(Map> headers) { + return headers; + } + + private Map> castMapCollectType(Map> map) { + final Map> result = new HashMap<>(); + map.forEach((key, value) -> result.put(key, new HashSet<>(value))); + return result; + } + + private String[] asString(Map> headers) { + return headers.entrySet().stream() + .flatMap(entry -> entry.getValue() + .stream() + .map(value -> Arrays.asList(entry.getKey(), value)) + .flatMap(List::stream)) + .toArray(String[]::new); + } + +} diff --git a/java11/src/main/java/feign/http2client/AsyncHttpClient.java b/java11/src/main/java/feign/http2client/AsyncHttpClient.java new file mode 100644 index 000000000..63e6135c8 --- /dev/null +++ b/java11/src/main/java/feign/http2client/AsyncHttpClient.java @@ -0,0 +1,58 @@ +/** + * Copyright 2012-2020 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.http2client; + +import feign.AsyncClient; +import feign.Request; +import feign.Request.Options; +import feign.Response; +import feign.Util; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public class AsyncHttpClient extends AbstractHttpClient implements AsyncClient { + + private final HttpClient client; + + public AsyncHttpClient() { + this(HttpClient.newBuilder() + .followRedirects(Redirect.ALWAYS) + .build()); + } + + public AsyncHttpClient(HttpClient client) { + this.client = Util.checkNotNull(client, "HttpClient must not be null"); + } + + @Override + public CompletableFuture execute(Request request, Options options, Optional requestContext) { + HttpRequest httpRequest; + try { + httpRequest = newRequestBuilder(request, options).build(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid uri " + request.url(), e); + } + + CompletableFuture> future = client.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + return future.thenApply(httpResponse -> toFeignResponse(request, httpResponse)); + } +} diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index 18488c19b..fcd07cd4c 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -36,7 +36,7 @@ import java.util.function.Function; import java.util.stream.Collectors; -public class Http2Client implements Client { +public class Http2Client extends AbstractHttpClient implements Client { private final HttpClient client; @@ -53,7 +53,14 @@ public Http2Client(HttpClient client) { @Override public Response execute(Request request, Options options) throws IOException { - final HttpRequest httpRequest = newRequestBuilder(request, options).build(); + final HttpRequest httpRequest; + try { + httpRequest = newRequestBuilder(request, options) + .version(Version.HTTP_2) + .build(); + } catch (URISyntaxException e) { + throw new IOException("Invalid uri " + request.url(), e); + } HttpResponse httpResponse; try { @@ -63,59 +70,7 @@ public Response execute(Request request, Options options) throws IOException { throw new IOException("Invalid uri " + request.url(), e); } - final OptionalLong length = httpResponse.headers().firstValueAsLong("Content-Length"); - - final Response response = Response.builder() - .body(new ByteArrayInputStream(httpResponse.body()), - length.isPresent() ? (int) length.getAsLong() : null) - .reason(httpResponse.headers().firstValue("Reason-Phrase").orElse("OK")) - .request(request) - .status(httpResponse.statusCode()) - .headers(castMapCollectType(httpResponse.headers().map())) - .build(); - return response; - } - - private Builder newRequestBuilder(Request request, Options options) throws IOException { - URI uri; - try { - uri = new URI(request.url()); - } catch (final URISyntaxException e) { - throw new IOException("Invalid uri " + request.url(), e); - } - - final BodyPublisher body; - final byte[] data = request.body(); - if (data == null) { - body = BodyPublishers.noBody(); - } else { - body = BodyPublishers.ofByteArray(data); - } - - final Builder requestBuilder = HttpRequest.newBuilder() - .uri(uri) - .timeout(Duration.ofMillis(options.readTimeoutMillis())) - .version(Version.HTTP_2); - - final Map> headers = filterRestrictedHeaders(request.headers()); - if (!headers.isEmpty()) { - requestBuilder.headers(asString(headers)); - } - - switch (request.httpMethod()) { - case GET: - return requestBuilder.GET(); - case POST: - return requestBuilder.POST(body); - case PUT: - return requestBuilder.PUT(body); - case DELETE: - return requestBuilder.DELETE(); - default: - // fall back scenario, http implementations may restrict some methods - return requestBuilder.method(request.httpMethod().toString(), body); - } - + return toFeignResponse(request, httpResponse); } /** @@ -133,7 +88,8 @@ private Builder newRequestBuilder(Request request, Options options) throws IOExc DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); } - private Map> filterRestrictedHeaders(Map> headers) { + @Override + protected Map> filterRestrictedHeaders(Map> headers) { final Map> filteredHeaders = headers.keySet() .stream() .filter(headerName -> !DISALLOWED_HEADERS_SET.contains(headerName)) @@ -145,20 +101,4 @@ private Map> filterRestrictedHeaders(Map> castMapCollectType(Map> map) { - final Map> result = new HashMap<>(); - map.forEach((key, value) -> result.put(key, new HashSet<>(value))); - return result; - } - - private String[] asString(Map> headers) { - return headers.entrySet().stream() - .flatMap(entry -> entry.getValue() - .stream() - .map(value -> Arrays.asList(entry.getKey(), value)) - .flatMap(List::stream)) - .toArray(String[]::new); - } - } From 5a77d5e8e4a7240d57dbf4f06b2d2050b5f0f456 Mon Sep 17 00:00:00 2001 From: UzimakiNaruto Date: Sat, 19 Dec 2020 16:53:01 +0800 Subject: [PATCH 2/9] java 11 httpclient not allow some header --- .../feign/http2client/AbstractHttpClient.java | 38 +++++++++++++++--- .../feign/http2client/AsyncHttpClient.java | 6 +-- .../java/feign/http2client/Http2Client.java | 39 +------------------ 3 files changed, 36 insertions(+), 47 deletions(-) diff --git a/java11/src/main/java/feign/http2client/AbstractHttpClient.java b/java11/src/main/java/feign/http2client/AbstractHttpClient.java index 8de78d3f8..e16843edd 100644 --- a/java11/src/main/java/feign/http2client/AbstractHttpClient.java +++ b/java11/src/main/java/feign/http2client/AbstractHttpClient.java @@ -30,11 +30,16 @@ import java.time.Duration; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.OptionalLong; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.stream.Collectors; public abstract class AbstractHttpClient { @@ -86,11 +91,7 @@ protected Builder newRequestBuilder(Request request, Options options) throws URI } } - protected Map> filterRestrictedHeaders(Map> headers) { - return headers; - } - - private Map> castMapCollectType(Map> map) { + private Map> castMapCollectType(Map> map) { final Map> result = new HashMap<>(); map.forEach((key, value) -> result.put(key, new HashSet<>(value))); return result; @@ -105,4 +106,31 @@ private String[] asString(Map> headers) { .toArray(String[]::new); } + /** + * There is a bunch o headers that the http2 client do not allow to be set. + * + * @see jdk.internal.net.http.common.Utils.DISALLOWED_HEADERS_SET + */ + private static final Set DISALLOWED_HEADERS_SET; + + static { + // A case insensitive TreeSet of strings. + final TreeSet treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + treeSet.addAll(Set.of("connection", "content-length", "date", "expect", "from", "host", + "origin", "referer", "upgrade", "via", "warning")); + DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); + } + + protected Map> filterRestrictedHeaders(Map> headers) { + final Map> filteredHeaders = headers.keySet() + .stream() + .filter(headerName -> !DISALLOWED_HEADERS_SET.contains(headerName)) + .collect(Collectors.toMap( + Function.identity(), + headers::get)); + + filteredHeaders.computeIfAbsent("Accept", key -> List.of("*/*")); + + return filteredHeaders; + } } diff --git a/java11/src/main/java/feign/http2client/AsyncHttpClient.java b/java11/src/main/java/feign/http2client/AsyncHttpClient.java index 63e6135c8..27690e75e 100644 --- a/java11/src/main/java/feign/http2client/AsyncHttpClient.java +++ b/java11/src/main/java/feign/http2client/AsyncHttpClient.java @@ -19,7 +19,6 @@ import feign.Response; import feign.Util; -import java.io.IOException; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; @@ -27,9 +26,8 @@ import java.net.http.HttpResponse; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -public class AsyncHttpClient extends AbstractHttpClient implements AsyncClient { +public class AsyncHttpClient extends AbstractHttpClient implements AsyncClient { private final HttpClient client; @@ -44,7 +42,7 @@ public AsyncHttpClient(HttpClient client) { } @Override - public CompletableFuture execute(Request request, Options options, Optional requestContext) { + public CompletableFuture execute(Request request, Options options, Optional requestContext) { HttpRequest httpRequest; try { httpRequest = newRequestBuilder(request, options).build(); diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index fcd07cd4c..3d2c37303 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -18,23 +18,15 @@ import feign.Request.Options; import feign.Response; import feign.Util; -import java.io.ByteArrayInputStream; + import java.io.IOException; -import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublisher; -import java.net.http.HttpRequest.BodyPublishers; -import java.net.http.HttpRequest.Builder; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import java.time.Duration; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; public class Http2Client extends AbstractHttpClient implements Client { @@ -72,33 +64,4 @@ public Response execute(Request request, Options options) throws IOException { return toFeignResponse(request, httpResponse); } - - /** - * There is a bunch o headers that the http2 client do not allow to be set. - * - * @see jdk.internal.net.http.common.Utils.DISALLOWED_HEADERS_SET - */ - private static final Set DISALLOWED_HEADERS_SET; - - static { - // A case insensitive TreeSet of strings. - final TreeSet treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - treeSet.addAll(Set.of("connection", "content-length", "date", "expect", "from", "host", - "origin", "referer", "upgrade", "via", "warning")); - DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); - } - - @Override - protected Map> filterRestrictedHeaders(Map> headers) { - final Map> filteredHeaders = headers.keySet() - .stream() - .filter(headerName -> !DISALLOWED_HEADERS_SET.contains(headerName)) - .collect(Collectors.toMap( - Function.identity(), - headers::get)); - - filteredHeaders.computeIfAbsent("Accept", key -> List.of("*/*")); - - return filteredHeaders; - } } From 35fdd8373b204d6733e4e139e5c1ad62925b3a85 Mon Sep 17 00:00:00 2001 From: UzimakiNaruto Date: Sat, 19 Dec 2020 16:53:16 +0800 Subject: [PATCH 3/9] add unit test --- java11/pom.xml | 5 + .../http2client/test/AsyncHttpClientTest.java | 1056 +++++++++++++++++ .../feign/http2client/test/CustomPojo.java | 25 + 3 files changed, 1086 insertions(+) create mode 100644 java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java create mode 100644 java11/src/test/java/feign/http2client/test/CustomPojo.java diff --git a/java11/pom.xml b/java11/pom.xml index f9e038df4..00cd002ea 100644 --- a/java11/pom.xml +++ b/java11/pom.xml @@ -69,6 +69,11 @@ jar test + + com.google.code.gson + gson + test + diff --git a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java b/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java new file mode 100644 index 000000000..d81785764 --- /dev/null +++ b/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java @@ -0,0 +1,1056 @@ +package feign.http2client.test; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import feign.AsyncClient; +import feign.AsyncFeign; +import feign.Body; +import feign.ChildPojo; +import feign.Feign; +import feign.Feign.ResponseMappingDecoder; +import feign.FeignException; +import feign.HeaderMap; +import feign.Headers; +import feign.Param; +import feign.PropertyPojo; +import feign.QueryMap; +import feign.QueryMapEncoder; +import feign.Request; +import feign.Request.HttpMethod; +import feign.RequestInterceptor; +import feign.RequestLine; +import feign.RequestTemplate; +import feign.Response; +import feign.ResponseMapper; +import feign.Target; +import feign.Target.HardCodedTarget; +import feign.Util; +import feign.codec.DecodeException; +import feign.codec.Decoder; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import feign.codec.StringDecoder; +import feign.http2client.AsyncHttpClient; +import feign.querymap.BeanQueryMapEncoder; +import feign.querymap.FieldQueryMapEncoder; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okio.Buffer; +import org.assertj.core.api.Assertions; +import org.jetbrains.annotations.NotNull; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; +import static org.hamcrest.CoreMatchers.isA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AsyncHttpClientTest { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + @Rule + public final MockWebServer server = new MockWebServer(); + + @Test + public void iterableQueryParams() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + api.queryParams("user", Arrays.asList("apple", "pear")); + + assertThat(server.takeRequest()).hasPath("/?1=user&2=apple&2=pear"); + } + + @Test + public void postTemplateParamsResolve() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + api.login("netflix", "denominator", "password"); + + assertThat(server.takeRequest()).hasBody( + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); + } + + @Test + public void responseCoercesToStringBody() throws Throwable { + server.enqueue(new MockResponse().setBody("foo")); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final Response response = unwrap(api.response()); + assertTrue(response.body().isRepeatable()); + assertEquals("foo", response.body().toString()); + } + + @Test + public void postFormParams() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.form("netflix", "denominator", "password"); + + assertThat(server.takeRequest()) + .hasBody( + "{\"customer_name\":\"netflix\",\"user_name\":\"denominator\",\"password\":\"password\"}"); + + checkCFCompletedSoon(cf); + } + + @Test + public void postBodyParam() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.body(Arrays.asList("netflix", "denominator", "password")); + + assertThat(server.takeRequest()) + .hasHeaders(entry("Content-Length", Collections.singletonList("32"))) + .hasBody("[netflix, denominator, password]"); + + checkCFCompletedSoon(cf); + } + + /** + * The type of a parameter value may not be the desired type to encode as. Prefer the interface + * type. + */ + @Test + public void bodyTypeCorrespondsWithParameterType() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + final AtomicReference encodedType = new AtomicReference(); + final TestInterfaceAsync api = newAsyncBuilder().encoder(new Encoder.Default() { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { + encodedType.set(bodyType); + } + }).target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.body(Arrays.asList("netflix", "denominator", "password")); + + server.takeRequest(); + + Assertions.assertThat(encodedType.get()).isEqualTo(new TypeToken>() { + }.getType()); + + checkCFCompletedSoon(cf); + } + + @Test + public void singleInterceptor() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + final TestInterfaceAsync api = + newAsyncBuilder().requestInterceptor(new ForwardedForInterceptor()) + .target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.post(); + + assertThat(server.takeRequest()) + .hasHeaders(entry("X-Forwarded-For", Collections.singletonList("origin.host.com"))); + + checkCFCompletedSoon(cf); + } + + @Test + public void multipleInterceptor() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + final TestInterfaceAsync api = + newAsyncBuilder().requestInterceptor(new ForwardedForInterceptor()) + .requestInterceptor(new UserAgentInterceptor()) + .target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.post(); + + assertThat(server.takeRequest()).hasHeaders( + entry("X-Forwarded-For", Collections.singletonList("origin.host.com")), + entry("User-Agent", Collections.singletonList("Feign"))); + + checkCFCompletedSoon(cf); + } + + @Test + public void customExpander() throws Exception { + server.enqueue(new MockResponse()); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.expand(new Date(1234l)); + + assertThat(server.takeRequest()).hasPath("/?date=1234"); + + checkCFCompletedSoon(cf); + } + + @Test + public void customExpanderListParam() throws Exception { + server.enqueue(new MockResponse()); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = + api.expandList(Arrays.asList(new Date(1234l), new Date(12345l))); + + assertThat(server.takeRequest()).hasPath("/?date=1234&date=12345"); + + checkCFCompletedSoon(cf); + } + + @Test + public void customExpanderNullParam() throws Exception { + server.enqueue(new MockResponse()); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.expandList(Arrays.asList(new Date(1234l), null)); + + assertThat(server.takeRequest()).hasPath("/?date=1234"); + + checkCFCompletedSoon(cf); + } + + @Test + public void headerMap() throws Exception { + server.enqueue(new MockResponse()); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final Map headerMap = new LinkedHashMap(); + headerMap.put("Content-Type", "myContent"); + headerMap.put("Custom-Header", "fooValue"); + final CompletableFuture cf = api.headerMap(headerMap); + + assertThat(server.takeRequest()).hasHeaders(entry("Content-Type", Arrays.asList("myContent")), + entry("Custom-Header", Arrays.asList("fooValue"))); + + checkCFCompletedSoon(cf); + } + + @Test + public void headerMapWithHeaderAnnotations() throws Exception { + server.enqueue(new MockResponse()); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final Map headerMap = new LinkedHashMap(); + headerMap.put("Custom-Header", "fooValue"); + api.headerMapWithHeaderAnnotations(headerMap); + + // header map should be additive for headers provided by annotations + assertThat(server.takeRequest()).hasHeaders(entry("Content-Encoding", Arrays.asList("deflate")), + entry("Custom-Header", Arrays.asList("fooValue"))); + + server.enqueue(new MockResponse()); + headerMap.put("Content-Encoding", "overrideFromMap"); + + final CompletableFuture cf = api.headerMapWithHeaderAnnotations(headerMap); + + /* + * @HeaderMap map values no longer override @Header parameters. This caused confusion as it is + * valid to have more than one value for a header. + */ + assertThat(server.takeRequest()).hasHeaders( + entry("Content-Encoding", Arrays.asList("deflate", "overrideFromMap")), + entry("Custom-Header", Arrays.asList("fooValue"))); + + checkCFCompletedSoon(cf); + } + + @Test + public void queryMap() throws Exception { + server.enqueue(new MockResponse()); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final Map queryMap = new LinkedHashMap(); + queryMap.put("name", "alice"); + queryMap.put("fooKey", "fooValue"); + final CompletableFuture cf = api.queryMap(queryMap); + + assertThat(server.takeRequest()).hasPath("/?name=alice&fooKey=fooValue"); + + checkCFCompletedSoon(cf); + } + + @Test + public void queryMapIterableValuesExpanded() throws Exception { + server.enqueue(new MockResponse()); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final Map queryMap = new LinkedHashMap(); + queryMap.put("name", Arrays.asList("Alice", "Bob")); + queryMap.put("fooKey", "fooValue"); + queryMap.put("emptyListKey", new ArrayList()); + queryMap.put("emptyStringKey", ""); // empty values are ignored. + final CompletableFuture cf = api.queryMap(queryMap); + + assertThat(server.takeRequest()) + .hasPath("/?name=Alice&name=Bob&fooKey=fooValue&emptyStringKey"); + + checkCFCompletedSoon(cf); + } + + @Test + public void queryMapWithQueryParams() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + server.enqueue(new MockResponse()); + Map queryMap = new LinkedHashMap(); + queryMap.put("fooKey", "fooValue"); + api.queryMapWithQueryParams("alice", queryMap); + // query map should be expanded after built-in parameters + assertThat(server.takeRequest()).hasPath("/?name=alice&fooKey=fooValue"); + + server.enqueue(new MockResponse()); + queryMap = new LinkedHashMap(); + queryMap.put("name", "bob"); + api.queryMapWithQueryParams("alice", queryMap); + // queries are additive + assertThat(server.takeRequest()).hasPath("/?name=alice&name=bob"); + + server.enqueue(new MockResponse()); + queryMap = new LinkedHashMap(); + queryMap.put("name", null); + api.queryMapWithQueryParams("alice", queryMap); + // null value for a query map key removes query parameter + assertThat(server.takeRequest()).hasPath("/?name=alice"); + } + + @Test + public void queryMapValueStartingWithBrace() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + server.enqueue(new MockResponse()); + Map queryMap = new LinkedHashMap(); + queryMap.put("name", "{alice"); + api.queryMap(queryMap); + assertThat(server.takeRequest()).hasPath("/?name=%7Balice"); + + server.enqueue(new MockResponse()); + queryMap = new LinkedHashMap(); + queryMap.put("{name", "alice"); + api.queryMap(queryMap); + assertThat(server.takeRequest()).hasPath("/?%7Bname=alice"); + + server.enqueue(new MockResponse()); + queryMap = new LinkedHashMap(); + queryMap.put("name", "%7Balice"); + api.queryMapEncoded(queryMap); + assertThat(server.takeRequest()).hasPath("/?name=%7Balice"); + + server.enqueue(new MockResponse()); + queryMap = new LinkedHashMap(); + queryMap.put("%7Bname", "%7Balice"); + api.queryMapEncoded(queryMap); + assertThat(server.takeRequest()).hasPath("/?%7Bname=%7Balice"); + } + + @Test + public void queryMapPojoWithFullParams() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CustomPojo customPojo = new CustomPojo("Name", 3); + + server.enqueue(new MockResponse()); + final CompletableFuture cf = api.queryMapPojo(customPojo); + assertThat(server.takeRequest()).hasQueryParams(Arrays.asList("name=Name", "number=3")); + checkCFCompletedSoon(cf); + } + + @Test + public void queryMapPojoWithPartialParams() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CustomPojo customPojo = new CustomPojo("Name", null); + + server.enqueue(new MockResponse()); + final CompletableFuture cf = api.queryMapPojo(customPojo); + assertThat(server.takeRequest()).hasPath("/?name=Name"); + + checkCFCompletedSoon(cf); + } + + @Test + public void queryMapPojoWithEmptyParams() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CustomPojo customPojo = new CustomPojo(null, null); + + server.enqueue(new MockResponse()); + api.queryMapPojo(customPojo); + assertThat(server.takeRequest()).hasPath("/"); + } + + @Test + public void configKeyFormatsAsExpected() throws Exception { + assertEquals("TestInterfaceAsync#post()", + Feign.configKey(TestInterfaceAsync.class, + TestInterfaceAsync.class.getDeclaredMethod("post"))); + assertEquals("TestInterfaceAsync#uriParam(String,URI,String)", + Feign.configKey(TestInterfaceAsync.class, + TestInterfaceAsync.class.getDeclaredMethod("uriParam", String.class, URI.class, + String.class))); + } + + @Test + public void configKeyUsesChildType() throws Exception { + assertEquals("List#iterator()", + Feign.configKey(List.class, Iterable.class.getDeclaredMethod("iterator"))); + } + + private T unwrap(CompletableFuture cf) throws Throwable { + try { + return cf.get(1, TimeUnit.SECONDS); + } catch (final ExecutionException e) { + throw e.getCause(); + } + } + + @Test + public void canOverrideErrorDecoder() throws Throwable { + server.enqueue(new MockResponse().setResponseCode(400).setBody("foo")); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("bad zone name"); + + final TestInterfaceAsync api = + newAsyncBuilder().errorDecoder(new IllegalArgumentExceptionOn400()) + .target("http://localhost:" + server.getPort()); + + unwrap(api.post()); + } + + @Test + public void overrideTypeSpecificDecoder() throws Throwable { + server.enqueue(new MockResponse().setBody("success!")); + + final TestInterfaceAsync api = newAsyncBuilder() + .decoder((response, type) -> "fail").target("http://localhost:" + server.getPort()); + + assertEquals("fail", unwrap(api.post())); + } + + @Test + public void doesntRetryAfterResponseIsSent() throws Throwable { + server.enqueue(new MockResponse().setBody("success!")); + thrown.expect(FeignException.class); + thrown.expectMessage("timeout reading POST http://"); + + final TestInterfaceAsync api = newAsyncBuilder().decoder((response, type) -> { + throw new IOException("timeout"); + }).target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.post(); + server.takeRequest(); + unwrap(cf); + } + + @Test + public void throwsFeignExceptionIncludingBody() throws Throwable { + server.enqueue(new MockResponse().setBody("success!")); + + final TestInterfaceAsync api = newAsyncBuilder().decoder((response, type) -> { + throw new IOException("timeout"); + }).target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.body("Request body"); + server.takeRequest(); + try { + unwrap(cf); + } catch (final FeignException e) { + Assertions.assertThat(e.getMessage()) + .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); + Assertions.assertThat(e.contentUTF8()).isEqualTo("Request body"); + return; + } + fail(); + } + + @Test + public void throwsFeignExceptionWithoutBody() { + server.enqueue(new MockResponse().setBody("success!")); + + final TestInterfaceAsync api = newAsyncBuilder().decoder((response, type) -> { + throw new IOException("timeout"); + }).target("http://localhost:" + server.getPort()); + + try { + api.noContent(); + } catch (final FeignException e) { + Assertions.assertThat(e.getMessage()) + .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); + Assertions.assertThat(e.contentUTF8()).isEqualTo(""); + } + } + + @SuppressWarnings("deprecation") + @Test + public void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { + final Map> headers = new LinkedHashMap>(); + headers.put("Location", Arrays.asList("http://bar.com")); + final Response response = Response.builder().status(302).reason("Found").headers(headers) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) + .body(new byte[0]).build(); + + final ExecutorService execs = Executors.newSingleThreadExecutor(); + + // fake client as Client.Default follows redirects. + final TestInterfaceAsync api = AsyncFeign.asyncBuilder() + .client(new AsyncClient.Default<>((request, options) -> response, execs)) + .target(TestInterfaceAsync.class, "http://localhost:" + server.getPort()); + + assertEquals(Collections.singletonList("http://bar.com"), + unwrap(api.response()).headers().get("Location")); + + execs.shutdown(); + } + + @Test + public void okIfDecodeRootCauseHasNoMessage() throws Throwable { + server.enqueue(new MockResponse().setBody("success!")); + thrown.expect(DecodeException.class); + + final TestInterfaceAsync api = newAsyncBuilder().decoder((response, type) -> { + throw new RuntimeException(); + }).target("http://localhost:" + server.getPort()); + + unwrap(api.post()); + } + + @Test + public void decodingExceptionGetWrappedInDecode404Mode() throws Throwable { + server.enqueue(new MockResponse().setResponseCode(404)); + thrown.expect(DecodeException.class); + thrown.expectCause(isA(NoSuchElementException.class)); + + final TestInterfaceAsync api = + newAsyncBuilder().decode404().decoder((response, type) -> { + assertEquals(404, response.status()); + throw new NoSuchElementException(); + }).target("http://localhost:" + server.getPort()); + + unwrap(api.post()); + } + + @Test + public void decodingDoesNotSwallow404ErrorsInDecode404Mode() throws Throwable { + server.enqueue(new MockResponse().setResponseCode(404)); + thrown.expect(IllegalArgumentException.class); + + final TestInterfaceAsync api = newAsyncBuilder().decode404() + .errorDecoder(new IllegalArgumentExceptionOn404()) + .target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.queryMap(Collections.emptyMap()); + server.takeRequest(); + unwrap(cf); + } + + @Test + public void okIfEncodeRootCauseHasNoMessage() throws Throwable { + server.enqueue(new MockResponse().setBody("success!")); + thrown.expect(EncodeException.class); + + final TestInterfaceAsync api = + newAsyncBuilder().encoder((object, bodyType, template) -> { + throw new RuntimeException(); + }).target("http://localhost:" + server.getPort()); + + unwrap(api.body(Arrays.asList("foo"))); + } + + @Test + public void equalsHashCodeAndToStringWork() { + final Target t1 = + new HardCodedTarget(TestInterfaceAsync.class, + "http://localhost:8080"); + final Target t2 = + new HardCodedTarget(TestInterfaceAsync.class, + "http://localhost:8888"); + final Target t3 = + new HardCodedTarget(OtherTestInterfaceAsync.class, + "http://localhost:8080"); + final TestInterfaceAsync i1 = newAsyncBuilder().target(t1); + final TestInterfaceAsync i2 = newAsyncBuilder().target(t1); + final TestInterfaceAsync i3 = newAsyncBuilder().target(t2); + final OtherTestInterfaceAsync i4 = newAsyncBuilder().target(t3); + + Assertions.assertThat(i1).isEqualTo(i2).isNotEqualTo(i3).isNotEqualTo(i4); + + Assertions.assertThat(i1.hashCode()).isEqualTo(i2.hashCode()).isNotEqualTo(i3.hashCode()) + .isNotEqualTo(i4.hashCode()); + + Assertions.assertThat(i1.toString()).isEqualTo(i2.toString()).isNotEqualTo(i3.toString()) + .isNotEqualTo(i4.toString()); + + Assertions.assertThat(t1).isNotEqualTo(i1); + + Assertions.assertThat(t1.hashCode()).isEqualTo(i1.hashCode()); + + Assertions.assertThat(t1.toString()).isEqualTo(i1.toString()); + } + + @SuppressWarnings("resource") + @Test + public void decodeLogicSupportsByteArray() throws Throwable { + final byte[] expectedResponse = {12, 34, 56}; + server.enqueue(new MockResponse().setBody(new Buffer().write(expectedResponse))); + + final OtherTestInterfaceAsync api = + newAsyncBuilder().target(new HardCodedTarget<>( + OtherTestInterfaceAsync.class, + "http://localhost:" + server.getPort() + )); + + Assertions.assertThat(unwrap(api.binaryResponseBody())).containsExactly(expectedResponse); + } + + @Test + public void encodeLogicSupportsByteArray() throws Exception { + final byte[] expectedRequest = {12, 34, 56}; + server.enqueue(new MockResponse()); + + final OtherTestInterfaceAsync api = + newAsyncBuilder().encoder(new Encoder.Default()).target(new HardCodedTarget<>( + OtherTestInterfaceAsync.class, + "http://localhost:" + server.getPort() + )); + + final CompletableFuture cf = api.binaryRequestBody(expectedRequest); + + assertThat(server.takeRequest()).hasBody(expectedRequest); + + checkCFCompletedSoon(cf); + } + + @Test + public void encodedQueryParam() throws Exception { + server.enqueue(new MockResponse()); + + final TestInterfaceAsync api = + newAsyncBuilder().target("http://localhost:" + server.getPort()); + + final CompletableFuture cf = api.encodedQueryParam("5.2FSi+"); + + assertThat(server.takeRequest()).hasPath("/?trim=5.2FSi%2B"); + + checkCFCompletedSoon(cf); + } + + private void checkCFCompletedSoon(CompletableFuture cf) { + try { + unwrap(cf); + } catch (final RuntimeException e) { + throw e; + } catch (final Throwable t) { + throw new RuntimeException(t); + } + } + + @Test + public void responseMapperIsAppliedBeforeDelegate() throws IOException { + final ResponseMappingDecoder decoder = + new ResponseMappingDecoder(upperCaseResponseMapper(), new StringDecoder()); + final String output = (String) decoder.decode(responseWithText("response"), String.class); + + Assertions.assertThat(output).isEqualTo("RESPONSE"); + } + + private static TestInterfaceAsyncBuilder newAsyncBuilder() { + return new TestInterfaceAsyncBuilder(); + } + + private ResponseMapper upperCaseResponseMapper() { + return new ResponseMapper() { + @SuppressWarnings("deprecation") + @Override + public Response map(Response response, Type type) { + try { + return response.toBuilder() + .body(Util.toString(response.body().asReader()).toUpperCase().getBytes()) + .build(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + @SuppressWarnings("deprecation") + private Response responseWithText(String text) { + return Response.builder().body(text, Util.UTF_8).status(200) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(new HashMap<>()).build(); + } + + @Test + public void mapAndDecodeExecutesMapFunction() throws Throwable { + server.enqueue(new MockResponse().setBody("response!")); + + final TestInterfaceAsync api = + AsyncFeign.asyncBuilder().mapAndDecode(upperCaseResponseMapper(), new StringDecoder()) + .target(TestInterfaceAsync.class, "http://localhost:" + server.getPort()); + + assertEquals("RESPONSE!", unwrap(api.post())); + } + + @Test + public void beanQueryMapEncoderWithPrivateGetterIgnored() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) + .target("http://localhost:" + server.getPort()); + + final PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); + propertyPojo.setPrivateGetterProperty("privateGetterProperty"); + propertyPojo.setName("Name"); + propertyPojo.setNumber(1); + + server.enqueue(new MockResponse()); + final CompletableFuture cf = api.queryMapPropertyPojo(propertyPojo); + assertThat(server.takeRequest()).hasQueryParams(Arrays.asList("name=Name", "number=1")); + checkCFCompletedSoon(cf); + } + + @Test + public void queryMap_with_child_pojo() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().queryMapEndcoder(new FieldQueryMapEncoder()) + .target("http://localhost:" + server.getPort()); + + final ChildPojo childPojo = new ChildPojo(); + childPojo.setChildPrivateProperty("first"); + childPojo.setParentProtectedProperty("second"); + childPojo.setParentPublicProperty("third"); + + server.enqueue(new MockResponse()); + final CompletableFuture cf = api.queryMapPropertyInheritence(childPojo); + assertThat(server.takeRequest()).hasQueryParams("parentPublicProperty=third", + "parentProtectedProperty=second", + "childPrivateProperty=first"); + checkCFCompletedSoon(cf); + } + + @Test + public void beanQueryMapEncoderWithNullValueIgnored() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) + .target("http://localhost:" + server.getPort()); + + final PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); + propertyPojo.setName(null); + propertyPojo.setNumber(1); + + server.enqueue(new MockResponse()); + final CompletableFuture cf = api.queryMapPropertyPojo(propertyPojo); + + assertThat(server.takeRequest()).hasQueryParams("number=1"); + + checkCFCompletedSoon(cf); + } + + @Test + public void beanQueryMapEncoderWithEmptyParams() throws Exception { + final TestInterfaceAsync api = + newAsyncBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) + .target("http://localhost:" + server.getPort()); + + final PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); + + server.enqueue(new MockResponse()); + final CompletableFuture cf = api.queryMapPropertyPojo(propertyPojo); + assertThat(server.takeRequest()).hasQueryParams("/"); + + checkCFCompletedSoon(cf); + } + + public interface TestInterfaceAsync { + + @RequestLine("POST /") + CompletableFuture response(); + + @RequestLine("POST /") + CompletableFuture post() throws TestInterfaceException; + + @RequestLine("POST /") + @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") + CompletableFuture login(@Param("customer_name") String customer, + @Param("user_name") String user, + @Param("password") String password); + + @RequestLine("POST /") + CompletableFuture body(List contents); + + @RequestLine("POST /") + CompletableFuture body(String content); + + @RequestLine("POST /") + CompletableFuture noContent(); + + @RequestLine("POST /") + @Headers("Content-Encoding: gzip") + CompletableFuture gzipBody(List contents); + + @RequestLine("POST /") + @Headers("Content-Encoding: deflate") + CompletableFuture deflateBody(List contents); + + @RequestLine("POST /") + CompletableFuture form(@Param("customer_name") String customer, + @Param("user_name") String user, + @Param("password") String password); + + @RequestLine("GET /{1}/{2}") + CompletableFuture uriParam(@Param("1") String one, + URI endpoint, + @Param("2") String two); + + @RequestLine("GET /?1={1}&2={2}") + CompletableFuture queryParams(@Param("1") String one, + @Param("2") Iterable twos); + + @RequestLine("POST /?date={date}") + CompletableFuture expand(@Param(value = "date", expander = DateToMillis.class) Date date); + + @RequestLine("GET /?date={date}") + CompletableFuture expandList(@Param(value = "date", + expander = DateToMillis.class) List dates); + + @RequestLine("GET /?date={date}") + CompletableFuture expandArray(@Param(value = "date", + expander = DateToMillis.class) Date[] dates); + + @RequestLine("GET /") + CompletableFuture headerMap(@HeaderMap Map headerMap); + + @RequestLine("GET /") + @Headers("Content-Encoding: deflate") + CompletableFuture headerMapWithHeaderAnnotations(@HeaderMap Map headerMap); + + @RequestLine("GET /") + CompletableFuture queryMap(@QueryMap Map queryMap); + + @RequestLine("GET /") + CompletableFuture queryMapEncoded(@QueryMap(encoded = true) Map queryMap); + + @RequestLine("GET /?name={name}") + CompletableFuture queryMapWithQueryParams(@Param("name") String name, + @QueryMap Map queryMap); + + @RequestLine("GET /?trim={trim}") + CompletableFuture encodedQueryParam(@Param(value = "trim", encoded = true) String trim); + + @RequestLine("GET /") + CompletableFuture queryMapPojo(@QueryMap CustomPojo object); + + @RequestLine("GET /") + CompletableFuture queryMapPropertyPojo(@QueryMap PropertyPojo object); + + @RequestLine("GET /") + CompletableFuture queryMapPropertyInheritence(@QueryMap ChildPojo object); + + class DateToMillis implements Param.Expander { + + @Override + public String expand(Object value) { + return String.valueOf(((Date) value).getTime()); + } + } + } + + class TestInterfaceException extends Exception { + private static final long serialVersionUID = 1L; + + TestInterfaceException(String message) { + super(message); + } + } + + public interface OtherTestInterfaceAsync { + + @RequestLine("POST /") + CompletableFuture post(); + + @RequestLine("POST /") + CompletableFuture binaryResponseBody(); + + @RequestLine("POST /") + CompletableFuture binaryRequestBody(byte[] contents); + } + + static class ForwardedForInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + template.header("X-Forwarded-For", "origin.host.com"); + } + } + + static class UserAgentInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + template.header("User-Agent", "Feign"); + } + } + + static class IllegalArgumentExceptionOn400 extends ErrorDecoder.Default { + + @Override + public Exception decode(String methodKey, Response response) { + if (response.status() == 400) { + return new IllegalArgumentException("bad zone name"); + } + return super.decode(methodKey, response); + } + } + + static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default { + + @Override + public Exception decode(String methodKey, Response response) { + if (response.status() == 404) { + return new IllegalArgumentException("bad zone name"); + } + return super.decode(methodKey, response); + } + } + + static final class TestInterfaceAsyncBuilder { + + private final AsyncFeign.AsyncBuilder delegate = + AsyncFeign.asyncBuilder() + .client(new AsyncHttpClient()) + .decoder(new Decoder.Default()).encoder(new Encoder() { + + @SuppressWarnings("deprecation") + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { + if (object instanceof Map) { + template.body(new Gson().toJson(object)); + } else { + template.body(object.toString()); + } + } + }); + + TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { + delegate.requestInterceptor(requestInterceptor); + return this; + } + + TestInterfaceAsyncBuilder encoder(Encoder encoder) { + delegate.encoder(encoder); + return this; + } + + TestInterfaceAsyncBuilder decoder(Decoder decoder) { + delegate.decoder(decoder); + return this; + } + + TestInterfaceAsyncBuilder errorDecoder(ErrorDecoder errorDecoder) { + delegate.errorDecoder(errorDecoder); + return this; + } + + TestInterfaceAsyncBuilder decode404() { + delegate.decode404(); + return this; + } + + TestInterfaceAsyncBuilder queryMapEndcoder(QueryMapEncoder queryMapEncoder) { + delegate.queryMapEncoder(queryMapEncoder); + return this; + } + + TestInterfaceAsync target(String url) { + return delegate.target(TestInterfaceAsync.class, url); + } + + T target(Target target) { + return delegate.target(target); + } + } + + static final class ExtendedCF extends CompletableFuture { + + } + + static abstract class NonInterface { + @RequestLine("GET /") + abstract CompletableFuture x(); + } + + interface NonCFApi { + @RequestLine("GET /") + void x(); + } + + interface ExtendedCFApi { + @RequestLine("GET /") + ExtendedCF x(); + } + + interface LowerWildApi { + @RequestLine("GET /") + CompletableFuture x(); + } + + interface UpperWildApi { + @RequestLine("GET /") + CompletableFuture x(); + } + + interface WildApi { + @RequestLine("GET /") + CompletableFuture x(); + } +} diff --git a/java11/src/test/java/feign/http2client/test/CustomPojo.java b/java11/src/test/java/feign/http2client/test/CustomPojo.java new file mode 100644 index 000000000..0d21ba1a5 --- /dev/null +++ b/java11/src/test/java/feign/http2client/test/CustomPojo.java @@ -0,0 +1,25 @@ +/** + * Copyright 2012-2020 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.http2client.test; + +public class CustomPojo { + + private final String name; + private final Integer number; + + CustomPojo(String name, Integer number) { + this.name = name; + this.number = number; + } +} From 1e01e624e0dd9fd6c209ea40529b0f6296685f2e Mon Sep 17 00:00:00 2001 From: UzimakiNaruto Date: Sat, 19 Dec 2020 17:23:13 +0800 Subject: [PATCH 4/9] add license --- .../feign/http2client/test/AsyncHttpClientTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java b/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java index d81785764..403fe66c1 100644 --- a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java +++ b/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java @@ -1,3 +1,16 @@ +/** + * Copyright 2012-2020 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ package feign.http2client.test; import com.google.gson.Gson; From 83604025a9fc8db82708ae103981318a951550a3 Mon Sep 17 00:00:00 2001 From: UzimakiNaruto Date: Tue, 22 Dec 2020 09:41:34 +0800 Subject: [PATCH 5/9] remove unused annotation --- .../test/java/feign/http2client/test/AsyncHttpClientTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java b/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java index 403fe66c1..fb2bf7dae 100644 --- a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java +++ b/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java @@ -51,7 +51,6 @@ import okhttp3.mockwebserver.MockWebServer; import okio.Buffer; import org.assertj.core.api.Assertions; -import org.jetbrains.annotations.NotNull; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; From 5e8e0f9a62b227e9397333df418e05ffa623beea Mon Sep 17 00:00:00 2001 From: UzimakiNaruto Date: Fri, 25 Dec 2020 09:43:18 +0800 Subject: [PATCH 6/9] Merge branch 'master' of https://github.com/OpenFeign/feign into asyncfeign-java11-impl  Conflicts:  java11/src/main/java/feign/http2client/Http2Client.java --- .../java/feign/http2client/Http2Client.java | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index 3d2c37303..091908c34 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -27,6 +27,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; public class Http2Client extends AbstractHttpClient implements Client { @@ -39,6 +40,12 @@ public Http2Client() { .build()); } + public Http2Client(Options options) { + this(newClientBuilder(options) + .version(Version.HTTP_2) + .build()); + } + public Http2Client(HttpClient client) { this.client = Util.checkNotNull(client, "HttpClient must not be null"); } @@ -54,9 +61,10 @@ public Response execute(Request request, Options options) throws IOException { throw new IOException("Invalid uri " + request.url(), e); } + HttpClient clientForRequest = getOrCreateClient(options); HttpResponse httpResponse; try { - httpResponse = client.send(httpRequest, BodyHandlers.ofByteArray()); + httpResponse = clientForRequest.send(httpRequest, BodyHandlers.ofByteArray()); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Invalid uri " + request.url(), e); @@ -64,4 +72,37 @@ public Response execute(Request request, Options options) throws IOException { return toFeignResponse(request, httpResponse); } + + private HttpClient getOrCreateClient(Options options) { + if (doesClientConfigurationDiffer(options)) { + // create a new client from the existing one - but with connectTimeout and followRedirect + // settings from options + java.net.http.HttpClient.Builder builder = newClientBuilder(options) + .sslContext(client.sslContext()) + .sslParameters(client.sslParameters()) + .version(client.version()); + client.authenticator().ifPresent(builder::authenticator); + client.cookieHandler().ifPresent(builder::cookieHandler); + client.executor().ifPresent(builder::executor); + client.proxy().ifPresent(builder::proxy); + return builder.build(); + } + return client; + } + + private boolean doesClientConfigurationDiffer(Options options) { + if ((client.followRedirects() == Redirect.ALWAYS) != options.isFollowRedirects()) { + return true; + } + return client.connectTimeout() + .map(timeout -> timeout.toMillis() != options.connectTimeoutMillis()) + .orElse(true); + } + + private static java.net.http.HttpClient.Builder newClientBuilder(Options options) { + return HttpClient + .newBuilder() + .followRedirects(options.isFollowRedirects() ? Redirect.ALWAYS : Redirect.NEVER) + .connectTimeout(Duration.ofMillis(options.connectTimeoutMillis())); + } } From 661f195e1082e75e08176444a8a1430ff50b7048 Mon Sep 17 00:00:00 2001 From: UzimakiNaruto Date: Fri, 25 Dec 2020 10:08:02 +0800 Subject: [PATCH 7/9] port connectionTimeout feature of Http2Client to AsyncHttpClient --- .../feign/http2client/AbstractHttpClient.java | 37 ++++++++++++++++++- .../feign/http2client/AsyncHttpClient.java | 3 +- .../java/feign/http2client/Http2Client.java | 36 +----------------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/java11/src/main/java/feign/http2client/AbstractHttpClient.java b/java11/src/main/java/feign/http2client/AbstractHttpClient.java index e16843edd..786a2943b 100644 --- a/java11/src/main/java/feign/http2client/AbstractHttpClient.java +++ b/java11/src/main/java/feign/http2client/AbstractHttpClient.java @@ -18,10 +18,10 @@ import feign.Response; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.net.http.HttpClient.Version; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; @@ -106,6 +106,39 @@ private String[] asString(Map> headers) { .toArray(String[]::new); } + protected HttpClient getOrCreateClient(HttpClient client, Options options) { + if (doesClientConfigurationDiffer(client, options)) { + // create a new client from the existing one - but with connectTimeout and followRedirect + // settings from options + java.net.http.HttpClient.Builder builder = newClientBuilder(options) + .sslContext(client.sslContext()) + .sslParameters(client.sslParameters()) + .version(client.version()); + client.authenticator().ifPresent(builder::authenticator); + client.cookieHandler().ifPresent(builder::cookieHandler); + client.executor().ifPresent(builder::executor); + client.proxy().ifPresent(builder::proxy); + return builder.build(); + } + return client; + } + + protected boolean doesClientConfigurationDiffer(HttpClient client, Options options) { + if ((client.followRedirects() == Redirect.ALWAYS) != options.isFollowRedirects()) { + return true; + } + return client.connectTimeout() + .map(timeout -> timeout.toMillis() != options.connectTimeoutMillis()) + .orElse(true); + } + + protected static java.net.http.HttpClient.Builder newClientBuilder(Options options) { + return HttpClient + .newBuilder() + .followRedirects(options.isFollowRedirects() ? Redirect.ALWAYS : Redirect.NEVER) + .connectTimeout(Duration.ofMillis(options.connectTimeoutMillis())); + } + /** * There is a bunch o headers that the http2 client do not allow to be set. * diff --git a/java11/src/main/java/feign/http2client/AsyncHttpClient.java b/java11/src/main/java/feign/http2client/AsyncHttpClient.java index 27690e75e..274212e17 100644 --- a/java11/src/main/java/feign/http2client/AsyncHttpClient.java +++ b/java11/src/main/java/feign/http2client/AsyncHttpClient.java @@ -50,7 +50,8 @@ public CompletableFuture execute(Request request, Options options, Opt throw new IllegalArgumentException("Invalid uri " + request.url(), e); } - CompletableFuture> future = client.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + HttpClient clientForRequest = getOrCreateClient(client, options); + CompletableFuture> future = clientForRequest.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); return future.thenApply(httpResponse -> toFeignResponse(request, httpResponse)); } } diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index 091908c34..a46015d30 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -27,7 +27,6 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import java.time.Duration; public class Http2Client extends AbstractHttpClient implements Client { @@ -61,7 +60,7 @@ public Response execute(Request request, Options options) throws IOException { throw new IOException("Invalid uri " + request.url(), e); } - HttpClient clientForRequest = getOrCreateClient(options); + HttpClient clientForRequest = getOrCreateClient(client, options); HttpResponse httpResponse; try { httpResponse = clientForRequest.send(httpRequest, BodyHandlers.ofByteArray()); @@ -72,37 +71,4 @@ public Response execute(Request request, Options options) throws IOException { return toFeignResponse(request, httpResponse); } - - private HttpClient getOrCreateClient(Options options) { - if (doesClientConfigurationDiffer(options)) { - // create a new client from the existing one - but with connectTimeout and followRedirect - // settings from options - java.net.http.HttpClient.Builder builder = newClientBuilder(options) - .sslContext(client.sslContext()) - .sslParameters(client.sslParameters()) - .version(client.version()); - client.authenticator().ifPresent(builder::authenticator); - client.cookieHandler().ifPresent(builder::cookieHandler); - client.executor().ifPresent(builder::executor); - client.proxy().ifPresent(builder::proxy); - return builder.build(); - } - return client; - } - - private boolean doesClientConfigurationDiffer(Options options) { - if ((client.followRedirects() == Redirect.ALWAYS) != options.isFollowRedirects()) { - return true; - } - return client.connectTimeout() - .map(timeout -> timeout.toMillis() != options.connectTimeoutMillis()) - .orElse(true); - } - - private static java.net.http.HttpClient.Builder newClientBuilder(Options options) { - return HttpClient - .newBuilder() - .followRedirects(options.isFollowRedirects() ? Redirect.ALWAYS : Redirect.NEVER) - .connectTimeout(Duration.ofMillis(options.connectTimeoutMillis())); - } } From 0ef4c877730c931a1b86558c1d49f4554538835d Mon Sep 17 00:00:00 2001 From: UzimakiNaruto Date: Wed, 30 Dec 2020 18:00:46 +0800 Subject: [PATCH 8/9] fix format --- .../feign/http2client/AbstractHttpClient.java | 13 ++++---- .../feign/http2client/AsyncHttpClient.java | 8 +++-- .../java/feign/http2client/Http2Client.java | 5 ++- .../http2client/test/AsyncHttpClientTest.java | 31 ++++++++----------- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/java11/src/main/java/feign/http2client/AbstractHttpClient.java b/java11/src/main/java/feign/http2client/AbstractHttpClient.java index 786a2943b..9cf24fc18 100644 --- a/java11/src/main/java/feign/http2client/AbstractHttpClient.java +++ b/java11/src/main/java/feign/http2client/AbstractHttpClient.java @@ -16,7 +16,6 @@ import feign.Request; import feign.Request.Options; import feign.Response; - import java.io.ByteArrayInputStream; import java.net.URI; import java.net.URISyntaxException; @@ -150,17 +149,17 @@ protected static java.net.http.HttpClient.Builder newClientBuilder(Options optio // A case insensitive TreeSet of strings. final TreeSet treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); treeSet.addAll(Set.of("connection", "content-length", "date", "expect", "from", "host", - "origin", "referer", "upgrade", "via", "warning")); + "origin", "referer", "upgrade", "via", "warning")); DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); } protected Map> filterRestrictedHeaders(Map> headers) { final Map> filteredHeaders = headers.keySet() - .stream() - .filter(headerName -> !DISALLOWED_HEADERS_SET.contains(headerName)) - .collect(Collectors.toMap( - Function.identity(), - headers::get)); + .stream() + .filter(headerName -> !DISALLOWED_HEADERS_SET.contains(headerName)) + .collect(Collectors.toMap( + Function.identity(), + headers::get)); filteredHeaders.computeIfAbsent("Accept", key -> List.of("*/*")); diff --git a/java11/src/main/java/feign/http2client/AsyncHttpClient.java b/java11/src/main/java/feign/http2client/AsyncHttpClient.java index 274212e17..d040160fc 100644 --- a/java11/src/main/java/feign/http2client/AsyncHttpClient.java +++ b/java11/src/main/java/feign/http2client/AsyncHttpClient.java @@ -18,7 +18,6 @@ import feign.Request.Options; import feign.Response; import feign.Util; - import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; @@ -42,7 +41,9 @@ public AsyncHttpClient(HttpClient client) { } @Override - public CompletableFuture execute(Request request, Options options, Optional requestContext) { + public CompletableFuture execute(Request request, + Options options, + Optional requestContext) { HttpRequest httpRequest; try { httpRequest = newRequestBuilder(request, options).build(); @@ -51,7 +52,8 @@ public CompletableFuture execute(Request request, Options options, Opt } HttpClient clientForRequest = getOrCreateClient(client, options); - CompletableFuture> future = clientForRequest.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + CompletableFuture> future = + clientForRequest.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); return future.thenApply(httpResponse -> toFeignResponse(request, httpResponse)); } } diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index a46015d30..df346290f 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -18,7 +18,6 @@ import feign.Request.Options; import feign.Response; import feign.Util; - import java.io.IOException; import java.net.URISyntaxException; import java.net.http.HttpClient; @@ -54,8 +53,8 @@ public Response execute(Request request, Options options) throws IOException { final HttpRequest httpRequest; try { httpRequest = newRequestBuilder(request, options) - .version(Version.HTTP_2) - .build(); + .version(Version.HTTP_2) + .build(); } catch (URISyntaxException e) { throw new IOException("Invalid uri " + request.url(), e); } diff --git a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java b/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java index fb2bf7dae..559a220a3 100644 --- a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java +++ b/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java @@ -54,7 +54,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; - import java.io.IOException; import java.lang.reflect.Type; import java.net.URI; @@ -74,7 +73,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; - import static feign.assertj.MockWebServerAssertions.assertThat; import static org.assertj.core.data.MapEntry.entry; import static org.hamcrest.CoreMatchers.isA; @@ -177,8 +175,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { server.takeRequest(); - Assertions.assertThat(encodedType.get()).isEqualTo(new TypeToken>() { - }.getType()); + Assertions.assertThat(encodedType.get()).isEqualTo(new TypeToken>() {}.getType()); checkCFCompletedSoon(cf); } @@ -659,8 +656,7 @@ public void decodeLogicSupportsByteArray() throws Throwable { final OtherTestInterfaceAsync api = newAsyncBuilder().target(new HardCodedTarget<>( OtherTestInterfaceAsync.class, - "http://localhost:" + server.getPort() - )); + "http://localhost:" + server.getPort())); Assertions.assertThat(unwrap(api.binaryResponseBody())).containsExactly(expectedResponse); } @@ -673,8 +669,7 @@ public void encodeLogicSupportsByteArray() throws Exception { final OtherTestInterfaceAsync api = newAsyncBuilder().encoder(new Encoder.Default()).target(new HardCodedTarget<>( OtherTestInterfaceAsync.class, - "http://localhost:" + server.getPort() - )); + "http://localhost:" + server.getPort())); final CompletableFuture cf = api.binaryRequestBody(expectedRequest); @@ -982,16 +977,16 @@ static final class TestInterfaceAsyncBuilder { .client(new AsyncHttpClient()) .decoder(new Decoder.Default()).encoder(new Encoder() { - @SuppressWarnings("deprecation") - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { - if (object instanceof Map) { - template.body(new Gson().toJson(object)); - } else { - template.body(object.toString()); - } - } - }); + @SuppressWarnings("deprecation") + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { + if (object instanceof Map) { + template.body(new Gson().toJson(object)); + } else { + template.body(object.toString()); + } + } + }); TestInterfaceAsyncBuilder requestInterceptor(RequestInterceptor requestInterceptor) { delegate.requestInterceptor(requestInterceptor); From 7d66dd9cd913cf5a7fff5b75cab75573b193976e Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Fri, 23 Jul 2021 14:22:47 +1200 Subject: [PATCH 9/9] Combined Sync and Async clients into a single class --- .../feign/http2client/AbstractHttpClient.java | 168 ---------------- .../feign/http2client/AsyncHttpClient.java | 59 ------ .../java/feign/http2client/Http2Client.java | 183 +++++++++++++++++- ...entTest.java => Http2ClientAsyncTest.java} | 74 +++---- 4 files changed, 212 insertions(+), 272 deletions(-) delete mode 100644 java11/src/main/java/feign/http2client/AbstractHttpClient.java delete mode 100644 java11/src/main/java/feign/http2client/AsyncHttpClient.java rename java11/src/test/java/feign/http2client/test/{AsyncHttpClientTest.java => Http2ClientAsyncTest.java} (98%) diff --git a/java11/src/main/java/feign/http2client/AbstractHttpClient.java b/java11/src/main/java/feign/http2client/AbstractHttpClient.java deleted file mode 100644 index 9cf24fc18..000000000 --- a/java11/src/main/java/feign/http2client/AbstractHttpClient.java +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Copyright 2012-2020 The Feign Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ -package feign.http2client; - -import feign.Request; -import feign.Request.Options; -import feign.Response; -import java.io.ByteArrayInputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.http.HttpClient; -import java.net.http.HttpClient.Redirect; -import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublisher; -import java.net.http.HttpRequest.BodyPublishers; -import java.net.http.HttpRequest.Builder; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.OptionalLong; -import java.util.Set; -import java.util.TreeSet; -import java.util.function.Function; -import java.util.stream.Collectors; - -public abstract class AbstractHttpClient { - - protected Response toFeignResponse(Request request, HttpResponse httpResponse) { - final OptionalLong length = httpResponse.headers().firstValueAsLong("Content-Length"); - - return Response.builder() - .body(new ByteArrayInputStream(httpResponse.body()), - length.isPresent() ? (int) length.getAsLong() : null) - .reason(httpResponse.headers().firstValue("Reason-Phrase").orElse("OK")) - .request(request) - .status(httpResponse.statusCode()) - .headers(castMapCollectType(httpResponse.headers().map())) - .build(); - } - - protected Builder newRequestBuilder(Request request, Options options) throws URISyntaxException { - URI uri = new URI(request.url()); - - final BodyPublisher body; - final byte[] data = request.body(); - if (data == null) { - body = BodyPublishers.noBody(); - } else { - body = BodyPublishers.ofByteArray(data); - } - - final Builder requestBuilder = HttpRequest.newBuilder() - .uri(uri) - .timeout(Duration.ofMillis(options.readTimeoutMillis())); - - final Map> headers = filterRestrictedHeaders(request.headers()); - if (!headers.isEmpty()) { - requestBuilder.headers(asString(headers)); - } - - switch (request.httpMethod()) { - case GET: - return requestBuilder.GET(); - case POST: - return requestBuilder.POST(body); - case PUT: - return requestBuilder.PUT(body); - case DELETE: - return requestBuilder.DELETE(); - default: - // fall back scenario, http implementations may restrict some methods - return requestBuilder.method(request.httpMethod().toString(), body); - } - } - - private Map> castMapCollectType(Map> map) { - final Map> result = new HashMap<>(); - map.forEach((key, value) -> result.put(key, new HashSet<>(value))); - return result; - } - - private String[] asString(Map> headers) { - return headers.entrySet().stream() - .flatMap(entry -> entry.getValue() - .stream() - .map(value -> Arrays.asList(entry.getKey(), value)) - .flatMap(List::stream)) - .toArray(String[]::new); - } - - protected HttpClient getOrCreateClient(HttpClient client, Options options) { - if (doesClientConfigurationDiffer(client, options)) { - // create a new client from the existing one - but with connectTimeout and followRedirect - // settings from options - java.net.http.HttpClient.Builder builder = newClientBuilder(options) - .sslContext(client.sslContext()) - .sslParameters(client.sslParameters()) - .version(client.version()); - client.authenticator().ifPresent(builder::authenticator); - client.cookieHandler().ifPresent(builder::cookieHandler); - client.executor().ifPresent(builder::executor); - client.proxy().ifPresent(builder::proxy); - return builder.build(); - } - return client; - } - - protected boolean doesClientConfigurationDiffer(HttpClient client, Options options) { - if ((client.followRedirects() == Redirect.ALWAYS) != options.isFollowRedirects()) { - return true; - } - return client.connectTimeout() - .map(timeout -> timeout.toMillis() != options.connectTimeoutMillis()) - .orElse(true); - } - - protected static java.net.http.HttpClient.Builder newClientBuilder(Options options) { - return HttpClient - .newBuilder() - .followRedirects(options.isFollowRedirects() ? Redirect.ALWAYS : Redirect.NEVER) - .connectTimeout(Duration.ofMillis(options.connectTimeoutMillis())); - } - - /** - * There is a bunch o headers that the http2 client do not allow to be set. - * - * @see jdk.internal.net.http.common.Utils.DISALLOWED_HEADERS_SET - */ - private static final Set DISALLOWED_HEADERS_SET; - - static { - // A case insensitive TreeSet of strings. - final TreeSet treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - treeSet.addAll(Set.of("connection", "content-length", "date", "expect", "from", "host", - "origin", "referer", "upgrade", "via", "warning")); - DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); - } - - protected Map> filterRestrictedHeaders(Map> headers) { - final Map> filteredHeaders = headers.keySet() - .stream() - .filter(headerName -> !DISALLOWED_HEADERS_SET.contains(headerName)) - .collect(Collectors.toMap( - Function.identity(), - headers::get)); - - filteredHeaders.computeIfAbsent("Accept", key -> List.of("*/*")); - - return filteredHeaders; - } -} diff --git a/java11/src/main/java/feign/http2client/AsyncHttpClient.java b/java11/src/main/java/feign/http2client/AsyncHttpClient.java deleted file mode 100644 index d040160fc..000000000 --- a/java11/src/main/java/feign/http2client/AsyncHttpClient.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright 2012-2020 The Feign Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ -package feign.http2client; - -import feign.AsyncClient; -import feign.Request; -import feign.Request.Options; -import feign.Response; -import feign.Util; -import java.net.URISyntaxException; -import java.net.http.HttpClient; -import java.net.http.HttpClient.Redirect; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -public class AsyncHttpClient extends AbstractHttpClient implements AsyncClient { - - private final HttpClient client; - - public AsyncHttpClient() { - this(HttpClient.newBuilder() - .followRedirects(Redirect.ALWAYS) - .build()); - } - - public AsyncHttpClient(HttpClient client) { - this.client = Util.checkNotNull(client, "HttpClient must not be null"); - } - - @Override - public CompletableFuture execute(Request request, - Options options, - Optional requestContext) { - HttpRequest httpRequest; - try { - httpRequest = newRequestBuilder(request, options).build(); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid uri " + request.url(), e); - } - - HttpClient clientForRequest = getOrCreateClient(client, options); - CompletableFuture> future = - clientForRequest.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); - return future.thenApply(httpResponse -> toFeignResponse(request, httpResponse)); - } -} diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index df346290f..66fbdce05 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -1,5 +1,5 @@ /** - * Copyright 2012-2020 The Feign Authors + * Copyright 2012-2021 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -13,21 +13,42 @@ */ package feign.http2client; -import feign.Client; -import feign.Request; -import feign.Request.Options; -import feign.Response; -import feign.Util; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpRequest.Builder; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import feign.AsyncClient; +import feign.Client; +import feign.Request; +import feign.Request.Options; +import feign.Response; +import feign.Util; -public class Http2Client extends AbstractHttpClient implements Client { +public class Http2Client implements Client, AsyncClient { private final HttpClient client; @@ -53,13 +74,13 @@ public Response execute(Request request, Options options) throws IOException { final HttpRequest httpRequest; try { httpRequest = newRequestBuilder(request, options) - .version(Version.HTTP_2) + .version(client.version()) .build(); } catch (URISyntaxException e) { throw new IOException("Invalid uri " + request.url(), e); } - HttpClient clientForRequest = getOrCreateClient(client, options); + HttpClient clientForRequest = getOrCreateClient(options); HttpResponse httpResponse; try { httpResponse = clientForRequest.send(httpRequest, BodyHandlers.ofByteArray()); @@ -70,4 +91,148 @@ public Response execute(Request request, Options options) throws IOException { return toFeignResponse(request, httpResponse); } + + @Override + public CompletableFuture execute(Request request, + Options options, + Optional requestContext) { + HttpRequest httpRequest; + try { + httpRequest = newRequestBuilder(request, options).build(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid uri " + request.url(), e); + } + + HttpClient clientForRequest = getOrCreateClient(options); + CompletableFuture> future = + clientForRequest.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + return future.thenApply(httpResponse -> toFeignResponse(request, httpResponse)); + } + + protected Response toFeignResponse(Request request, HttpResponse httpResponse) { + final OptionalLong length = httpResponse.headers().firstValueAsLong("Content-Length"); + + return Response.builder() + .body(new ByteArrayInputStream(httpResponse.body()), + length.isPresent() ? (int) length.getAsLong() : null) + .reason(httpResponse.headers().firstValue("Reason-Phrase").orElse("OK")) + .request(request) + .status(httpResponse.statusCode()) + .headers(castMapCollectType(httpResponse.headers().map())) + .build(); + } + + private HttpClient getOrCreateClient(Options options) { + if (doesClientConfigurationDiffer(options)) { + // create a new client from the existing one - but with connectTimeout and followRedirect + // settings from options + java.net.http.HttpClient.Builder builder = newClientBuilder(options) + .sslContext(client.sslContext()) + .sslParameters(client.sslParameters()) + .version(client.version()); + client.authenticator().ifPresent(builder::authenticator); + client.cookieHandler().ifPresent(builder::cookieHandler); + client.executor().ifPresent(builder::executor); + client.proxy().ifPresent(builder::proxy); + return builder.build(); + } + return client; + } + + private boolean doesClientConfigurationDiffer(Options options) { + if ((client.followRedirects() == Redirect.ALWAYS) != options.isFollowRedirects()) { + return true; + } + return client.connectTimeout() + .map(timeout -> timeout.toMillis() != options.connectTimeoutMillis()) + .orElse(true); + } + + private static java.net.http.HttpClient.Builder newClientBuilder(Options options) { + return HttpClient + .newBuilder() + .followRedirects(options.isFollowRedirects() ? Redirect.ALWAYS : Redirect.NEVER) + .connectTimeout(Duration.ofMillis(options.connectTimeoutMillis())); + } + + private Builder newRequestBuilder(Request request, Options options) throws URISyntaxException { + URI uri = new URI(request.url()); + + final BodyPublisher body; + final byte[] data = request.body(); + if (data == null) { + body = BodyPublishers.noBody(); + } else { + body = BodyPublishers.ofByteArray(data); + } + + final Builder requestBuilder = HttpRequest.newBuilder() + .uri(uri) + .timeout(Duration.ofMillis(options.readTimeoutMillis())) + .version(client.version()); + + final Map> headers = filterRestrictedHeaders(request.headers()); + if (!headers.isEmpty()) { + requestBuilder.headers(asString(headers)); + } + + switch (request.httpMethod()) { + case GET: + return requestBuilder.GET(); + case POST: + return requestBuilder.POST(body); + case PUT: + return requestBuilder.PUT(body); + case DELETE: + return requestBuilder.DELETE(); + default: + // fall back scenario, http implementations may restrict some methods + return requestBuilder.method(request.httpMethod().toString(), body); + } + + } + + /** + * There is a bunch o headers that the http2 client do not allow to be set. + * + * @see jdk.internal.net.http.common.Utils.DISALLOWED_HEADERS_SET + */ + private static final Set DISALLOWED_HEADERS_SET; + + static { + // A case insensitive TreeSet of strings. + final TreeSet treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + treeSet.addAll(Set.of("connection", "content-length", "date", "expect", "from", "host", + "origin", "referer", "upgrade", "via", "warning")); + DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); + } + + private Map> filterRestrictedHeaders(Map> headers) { + final Map> filteredHeaders = headers.keySet() + .stream() + .filter(headerName -> !DISALLOWED_HEADERS_SET.contains(headerName)) + .collect(Collectors.toMap( + Function.identity(), + headers::get)); + + filteredHeaders.computeIfAbsent("Accept", key -> List.of("*/*")); + + return filteredHeaders; + } + + private Map> castMapCollectType(Map> map) { + final Map> result = new HashMap<>(); + map.forEach((key, value) -> result.put(key, new HashSet<>(value))); + return result; + } + + private String[] asString(Map> headers) { + return headers.entrySet().stream() + .flatMap(entry -> entry.getValue() + .stream() + .map(value -> Arrays.asList(entry.getKey(), value)) + .flatMap(List::stream)) + .toArray(String[]::new); + } + } diff --git a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java similarity index 98% rename from java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java rename to java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java index 559a220a3..205450bd2 100644 --- a/java11/src/test/java/feign/http2client/test/AsyncHttpClientTest.java +++ b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012-2020 The Feign Authors + * Copyright 2012-2021 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -13,6 +13,37 @@ */ package feign.http2client.test; +import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; +import static org.hamcrest.CoreMatchers.isA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import feign.AsyncClient; @@ -44,43 +75,14 @@ import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.codec.StringDecoder; -import feign.http2client.AsyncHttpClient; +import feign.http2client.Http2Client; import feign.querymap.BeanQueryMapEncoder; import feign.querymap.FieldQueryMapEncoder; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okio.Buffer; -import org.assertj.core.api.Assertions; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import java.io.IOException; -import java.lang.reflect.Type; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import static feign.assertj.MockWebServerAssertions.assertThat; -import static org.assertj.core.data.MapEntry.entry; -import static org.hamcrest.CoreMatchers.isA; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -public class AsyncHttpClientTest { +public class Http2ClientAsyncTest { @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule @@ -120,7 +122,7 @@ public void responseCoercesToStringBody() throws Throwable { final Response response = unwrap(api.response()); assertTrue(response.body().isRepeatable()); - assertEquals("foo", response.body().toString()); + assertEquals("foo", Util.toString(response.body().asReader(StandardCharsets.UTF_8))); } @Test @@ -556,8 +558,8 @@ public void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .client(new AsyncClient.Default<>((request, options) -> response, execs)) .target(TestInterfaceAsync.class, "http://localhost:" + server.getPort()); - assertEquals(Collections.singletonList("http://bar.com"), - unwrap(api.response()).headers().get("Location")); + assertThat(unwrap(api.response()).headers().get("Location")) + .contains("http://bar.com"); execs.shutdown(); } @@ -974,7 +976,7 @@ static final class TestInterfaceAsyncBuilder { private final AsyncFeign.AsyncBuilder delegate = AsyncFeign.asyncBuilder() - .client(new AsyncHttpClient()) + .client(new Http2Client()) .decoder(new Decoder.Default()).encoder(new Encoder() { @SuppressWarnings("deprecation")