com.squareup.okhttp3
mockwebserver
diff --git a/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java
new file mode 100644
index 0000000000..f2e82bd672
--- /dev/null
+++ b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java
@@ -0,0 +1,183 @@
+/**
+ * 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.hc5;
+
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.config.Configurable;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.Header;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import feign.*;
+import feign.Request.Options;
+
+/**
+ * This module directs Feign's http requests to Apache's
+ * HttpClient 5. Ex.
+ *
+ *
+ * GitHub github = Feign.builder().client(new ApacheHttp5Client()).target(GitHub.class,
+ * "https://api.github.com");
+ */
+/*
+ */
+public final class AsyncApacheHttp5Client implements AsyncClient {
+
+ private static final String ACCEPT_HEADER_NAME = "Accept";
+
+ private final CloseableHttpAsyncClient client;
+
+ public AsyncApacheHttp5Client() {
+ this(HttpAsyncClients.custom().build());
+ }
+
+ public AsyncApacheHttp5Client(CloseableHttpAsyncClient client) {
+ this.client = client;
+ client.start();
+ }
+
+ @Override
+ public CompletableFuture execute(Request request,
+ Options options,
+ Optional requestContext) {
+ final SimpleHttpRequest httpUriRequest = toClassicHttpRequest(request, options);
+
+ final CompletableFuture result = new CompletableFuture<>();
+ final FutureCallback callback = new FutureCallback() {
+
+ @Override
+ public void completed(SimpleHttpResponse httpResponse) {
+ result.complete(toFeignResponse(httpResponse, request));
+ }
+
+ @Override
+ public void failed(Exception ex) {
+ result.completeExceptionally(ex);
+ }
+
+ @Override
+ public void cancelled() {
+ result.cancel(false);
+ }
+ };
+
+ client.execute(httpUriRequest, callback);
+
+ return result;
+ }
+
+ protected HttpClientContext configureTimeouts(Request.Options options) {
+ final HttpClientContext context = new HttpClientContext();
+ // per request timeouts
+ final RequestConfig requestConfig =
+ (client instanceof Configurable
+ ? RequestConfig.copy(((Configurable) client).getConfig())
+ : RequestConfig.custom())
+ .setConnectTimeout(options.connectTimeout(), options.connectTimeoutUnit())
+ .setResponseTimeout(options.readTimeout(), options.readTimeoutUnit())
+ .build();
+ context.setRequestConfig(requestConfig);
+ return context;
+ }
+
+ SimpleHttpRequest toClassicHttpRequest(Request request,
+ Request.Options options) {
+ final SimpleHttpRequest httpRequest =
+ new SimpleHttpRequest(request.httpMethod().name(), request.url());
+
+ // request headers
+ boolean hasAcceptHeader = false;
+ for (final Map.Entry> headerEntry : request.headers().entrySet()) {
+ final String headerName = headerEntry.getKey();
+ if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
+ hasAcceptHeader = true;
+ }
+
+ if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
+ // The 'Content-Length' header is always set by the Apache client and it
+ // doesn't like us to set it as well.
+ continue;
+ }
+
+ for (final String headerValue : headerEntry.getValue()) {
+ httpRequest.addHeader(headerName, headerValue);
+ }
+ }
+ // some servers choke on the default accept string, so we'll set it to anything
+ if (!hasAcceptHeader) {
+ httpRequest.addHeader(ACCEPT_HEADER_NAME, "*/*");
+ }
+
+ // request body
+ // final Body requestBody = request.requestBody();
+ final byte[] data = request.body();
+ if (data != null) {
+ httpRequest.setBodyBytes(data, getContentType(request));
+ }
+
+ return httpRequest;
+ }
+
+ private ContentType getContentType(Request request) {
+ ContentType contentType = null;
+ for (final Map.Entry> entry : request.headers().entrySet()) {
+ if (entry.getKey().equalsIgnoreCase("Content-Type")) {
+ final Collection values = entry.getValue();
+ if (values != null && !values.isEmpty()) {
+ contentType = ContentType.parse(values.iterator().next());
+ if (contentType.getCharset() == null) {
+ contentType = contentType.withCharset(request.charset());
+ }
+ break;
+ }
+ }
+ }
+ return contentType;
+ }
+
+ Response toFeignResponse(SimpleHttpResponse httpResponse, Request request) {
+ final int statusCode = httpResponse.getCode();
+
+ final String reason = httpResponse.getReasonPhrase();
+
+ final Map> headers = new HashMap>();
+ for (final Header header : httpResponse.getHeaders()) {
+ final String name = header.getName();
+ final String value = header.getValue();
+
+ Collection headerValues = headers.get(name);
+ if (headerValues == null) {
+ headerValues = new ArrayList();
+ headers.put(name, headerValues);
+ }
+ headerValues.add(value);
+ }
+
+ return Response.builder()
+ .status(statusCode)
+ .reason(reason)
+ .headers(headers)
+ .request(request)
+ .body(httpResponse
+ .getBodyBytes())
+ .build();
+ }
+
+}
diff --git a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java
new file mode 100644
index 0000000000..7f12c96993
--- /dev/null
+++ b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java
@@ -0,0 +1,1017 @@
+/**
+ * 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.hc5;
+
+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 com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+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.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicReference;
+import feign.*;
+import feign.Feign.ResponseMappingDecoder;
+import feign.Request.HttpMethod;
+import feign.Target.HardCodedTarget;
+import feign.codec.*;
+import feign.querymap.BeanQueryMapEncoder;
+import feign.querymap.FieldQueryMapEncoder;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okio.Buffer;
+
+public class AsyncApacheHttp5ClientTest {
+
+ @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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 = new TestInterfaceAsyncBuilder().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();
+
+ assertThat(encodedType.get()).isEqualTo(new TypeToken>() {}.getType());
+
+ checkCFCompletedSoon(cf);
+ }
+
+ @Test
+ public void singleInterceptor() throws Exception {
+ server.enqueue(new MockResponse().setBody("foo"));
+
+ final TestInterfaceAsync api =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 = new TestInterfaceAsyncBuilder()
+ .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 = new TestInterfaceAsyncBuilder().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 = AsyncFeign.asyncBuilder().decoder((response, type) -> {
+ throw new IOException("timeout");
+ }).target(TestInterfaceAsync.class, "http://localhost:" + server.getPort());
+
+ final CompletableFuture> cf = api.body("Request body");
+ server.takeRequest();
+ try {
+ unwrap(cf);
+ } catch (final FeignException e) {
+ assertThat(e.getMessage())
+ .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/");
+ assertThat(e.contentUTF8()).isEqualTo("Request body");
+ return;
+ }
+ fail();
+ }
+
+ @Test
+ public void throwsFeignExceptionWithoutBody() {
+ server.enqueue(new MockResponse().setBody("success!"));
+
+ final TestInterfaceAsync api = AsyncFeign.asyncBuilder().decoder((response, type) -> {
+ throw new IOException("timeout");
+ }).target(TestInterfaceAsync.class, "http://localhost:" + server.getPort());
+
+ try {
+ api.noContent();
+ } catch (final FeignException e) {
+ assertThat(e.getMessage())
+ .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/");
+ 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 = new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 = new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 = AsyncFeign.asyncBuilder().target(t1);
+ final TestInterfaceAsync i2 = AsyncFeign.asyncBuilder().target(t1);
+ final TestInterfaceAsync i3 = AsyncFeign.asyncBuilder().target(t2);
+ final OtherTestInterfaceAsync i4 = AsyncFeign.asyncBuilder().target(t3);
+
+ assertThat(i1).isEqualTo(i2).isNotEqualTo(i3).isNotEqualTo(i4);
+
+ assertThat(i1.hashCode()).isEqualTo(i2.hashCode()).isNotEqualTo(i3.hashCode())
+ .isNotEqualTo(i4.hashCode());
+
+ assertThat(i1.toString()).isEqualTo(i2.toString()).isNotEqualTo(i3.toString())
+ .isNotEqualTo(i4.toString());
+
+ assertThat(t1).isNotEqualTo(i1);
+
+ assertThat(t1.hashCode()).isEqualTo(i1.hashCode());
+
+ 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 =
+ AsyncFeign.asyncBuilder().target(OtherTestInterfaceAsync.class,
+ "http://localhost:" + server.getPort());
+
+ 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 =
+ AsyncFeign.asyncBuilder().target(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 =
+ new TestInterfaceAsyncBuilder().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);
+
+ assertThat(output).isEqualTo("RESPONSE");
+ }
+
+ 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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 =
+ new TestInterfaceAsyncBuilder().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 AsyncApacheHttp5Client())
+ .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);
+ }
+ }
+
+ 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 extends Object> x();
+ }
+
+ interface UpperWildApi {
+ @RequestLine("GET /")
+ CompletableFuture super Object> x();
+ }
+
+ interface WildApi {
+ @RequestLine("GET /")
+ CompletableFuture> x();
+ }
+
+
+}
diff --git a/hc5/src/test/java/feign/hc5/CustomPojo.java b/hc5/src/test/java/feign/hc5/CustomPojo.java
new file mode 100644
index 0000000000..7295707833
--- /dev/null
+++ b/hc5/src/test/java/feign/hc5/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.hc5;
+
+public class CustomPojo {
+
+ private final String name;
+ private final Integer number;
+
+ CustomPojo(String name, Integer number) {
+ this.name = name;
+ this.number = number;
+ }
+}