Tristan Burch
10 years ago
committed by
Adrian Cole
6 changed files with 344 additions and 1 deletions
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
Apache HttpClient |
||||
=================== |
||||
|
||||
This module directs Feign's http requests to Apache's [HttpClient](https://hc.apache.org/httpcomponents-client-ga/). |
||||
|
||||
To use HttpClient with Feign, add the HttpClient module to your classpath. Then, configure Feign to use the HttpClient: |
||||
|
||||
```java |
||||
GitHub github = Feign.builder() |
||||
.client(new ApacheHttpClient()) |
||||
.target(GitHub.class, "https://api.github.com"); |
||||
``` |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'java' |
||||
|
||||
sourceCompatibility = 1.6 |
||||
|
||||
dependencies { |
||||
compile project(':feign-core') |
||||
compile 'org.apache.httpcomponents:httpclient:4.4.1' |
||||
testCompile 'junit:junit:4.12' |
||||
testCompile 'org.assertj:assertj-core:1.7.1' |
||||
testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' |
||||
testCompile project(':feign-core').sourceSets.test.output // for assertions |
||||
} |
@ -0,0 +1,207 @@
@@ -0,0 +1,207 @@
|
||||
/* |
||||
* Copyright 2015 Netflix, Inc. |
||||
* |
||||
* 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.httpclient; |
||||
|
||||
import feign.Client; |
||||
import feign.Request; |
||||
import feign.Response; |
||||
import feign.Util; |
||||
import org.apache.http.Header; |
||||
import org.apache.http.HttpEntity; |
||||
import org.apache.http.HttpResponse; |
||||
import org.apache.http.NameValuePair; |
||||
import org.apache.http.StatusLine; |
||||
import org.apache.http.client.HttpClient; |
||||
import org.apache.http.client.config.RequestConfig; |
||||
import org.apache.http.client.methods.HttpUriRequest; |
||||
import org.apache.http.client.methods.RequestBuilder; |
||||
import org.apache.http.client.utils.URIBuilder; |
||||
import org.apache.http.client.utils.URLEncodedUtils; |
||||
import org.apache.http.entity.ByteArrayEntity; |
||||
import org.apache.http.entity.StringEntity; |
||||
import org.apache.http.impl.client.HttpClientBuilder; |
||||
import org.apache.http.util.EntityUtils; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.InputStreamReader; |
||||
import java.io.Reader; |
||||
import java.io.UnsupportedEncodingException; |
||||
import java.net.MalformedURLException; |
||||
import java.net.URI; |
||||
import java.net.URISyntaxException; |
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* This module directs Feign's http requests to Apache's |
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">HttpClient</a>. Ex. |
||||
* <pre> |
||||
* GitHub github = Feign.builder().client(new ApacheHttpClient()).target(GitHub.class, |
||||
* "https://api.github.com"); |
||||
*/ |
||||
/* |
||||
* Based on Square, Inc's Retrofit ApacheClient implementation |
||||
*/ |
||||
public final class ApacheHttpClient implements Client { |
||||
private static final String ACCEPT_HEADER_NAME = "Accept"; |
||||
|
||||
private final HttpClient client; |
||||
|
||||
public ApacheHttpClient() { |
||||
this(HttpClientBuilder.create().build()); |
||||
} |
||||
|
||||
public ApacheHttpClient(HttpClient client) { |
||||
this.client = client; |
||||
} |
||||
|
||||
@Override |
||||
public Response execute(Request request, Request.Options options) throws IOException { |
||||
HttpUriRequest httpUriRequest; |
||||
try { |
||||
httpUriRequest = toHttpUriRequest(request, options); |
||||
} catch (URISyntaxException e) { |
||||
throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); |
||||
} |
||||
HttpResponse httpResponse = client.execute(httpUriRequest); |
||||
return toFeignResponse(httpResponse); |
||||
} |
||||
|
||||
HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws |
||||
UnsupportedEncodingException, MalformedURLException, URISyntaxException { |
||||
RequestBuilder requestBuilder = RequestBuilder.create(request.method()); |
||||
|
||||
//per request timeouts
|
||||
RequestConfig requestConfig = RequestConfig |
||||
.custom() |
||||
.setConnectTimeout(options.connectTimeoutMillis()) |
||||
.setSocketTimeout(options.readTimeoutMillis()) |
||||
.build(); |
||||
requestBuilder.setConfig(requestConfig); |
||||
|
||||
URI uri = new URIBuilder(request.url()).build(); |
||||
|
||||
//request url
|
||||
requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getPath()); |
||||
|
||||
//request query params
|
||||
List<NameValuePair> queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name()); |
||||
for (NameValuePair queryParam: queryParams) { |
||||
requestBuilder.addParameter(queryParam); |
||||
} |
||||
|
||||
//request body
|
||||
if (request.body() != null) { |
||||
HttpEntity entity = request.charset() != null ? |
||||
new StringEntity(new String(request.body(), request.charset())) : |
||||
new ByteArrayEntity(request.body()); |
||||
requestBuilder.setEntity(entity); |
||||
} |
||||
|
||||
//request headers
|
||||
boolean hasAcceptHeader = false; |
||||
for (Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) { |
||||
String headerName = headerEntry.getKey(); |
||||
if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) { |
||||
hasAcceptHeader = true; |
||||
} |
||||
if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH) && |
||||
requestBuilder.getHeaders(headerName) != null) { |
||||
//if the 'Content-Length' header is already present, it's been set from HttpEntity, so we
|
||||
//won't add it again
|
||||
continue; |
||||
} |
||||
|
||||
for (String headerValue : headerEntry.getValue()) { |
||||
requestBuilder.addHeader(headerName, headerValue); |
||||
} |
||||
} |
||||
//some servers choke on the default accept string, so we'll set it to anything
|
||||
if (!hasAcceptHeader) { |
||||
requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*"); |
||||
} |
||||
|
||||
return requestBuilder.build(); |
||||
} |
||||
|
||||
Response toFeignResponse(HttpResponse httpResponse) throws IOException { |
||||
StatusLine statusLine = httpResponse.getStatusLine(); |
||||
int statusCode = statusLine.getStatusCode(); |
||||
|
||||
String reason = statusLine.getReasonPhrase(); |
||||
|
||||
Map<String, Collection<String>> headers = new HashMap<String, Collection<String>>(); |
||||
for (Header header : httpResponse.getAllHeaders()) { |
||||
String name = header.getName(); |
||||
String value = header.getValue(); |
||||
|
||||
Collection<String> headerValues = headers.get(name); |
||||
if (headerValues == null) { |
||||
headerValues = new ArrayList<String>(); |
||||
headers.put(name, headerValues); |
||||
} |
||||
headerValues.add(value); |
||||
} |
||||
|
||||
return Response.create(statusCode, reason, headers, toFeignBody(httpResponse)); |
||||
} |
||||
|
||||
Response.Body toFeignBody(HttpResponse httpResponse) throws IOException { |
||||
HttpEntity entity = httpResponse.getEntity(); |
||||
final Integer length = entity != null && entity.getContentLength() != -1 ? |
||||
(int) entity.getContentLength() : |
||||
null; |
||||
final InputStream input = entity != null ? |
||||
new ByteArrayInputStream(EntityUtils.toByteArray(entity)) : |
||||
null; |
||||
|
||||
return new Response.Body() { |
||||
|
||||
@Override |
||||
public void close() throws IOException { |
||||
if (input != null) { |
||||
input.close(); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public Integer length() { |
||||
return length; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isRepeatable() { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public InputStream asInputStream() throws IOException { |
||||
return input; |
||||
} |
||||
|
||||
@Override |
||||
public Reader asReader() throws IOException { |
||||
return new InputStreamReader(input); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
|
||||
/* |
||||
* Copyright 2015 Netflix, Inc. |
||||
* |
||||
* 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.httpclient; |
||||
|
||||
import com.squareup.okhttp.mockwebserver.MockResponse; |
||||
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; |
||||
import feign.Feign; |
||||
import feign.FeignException; |
||||
import feign.Headers; |
||||
import feign.RequestLine; |
||||
import feign.Response; |
||||
import org.junit.Rule; |
||||
import org.junit.Test; |
||||
import org.junit.rules.ExpectedException; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.io.IOException; |
||||
|
||||
import static feign.Util.UTF_8; |
||||
import static feign.assertj.MockWebServerAssertions.assertThat; |
||||
import static java.util.Arrays.asList; |
||||
import static org.junit.Assert.assertEquals; |
||||
|
||||
public class ApacheHttpClientTest { |
||||
|
||||
@Rule |
||||
public final ExpectedException thrown = ExpectedException.none(); |
||||
@Rule |
||||
public final MockWebServerRule server = new MockWebServerRule(); |
||||
|
||||
@Test |
||||
public void parsesRequestAndResponse() throws IOException, InterruptedException { |
||||
server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar")); |
||||
|
||||
TestInterface api = Feign.builder() |
||||
.client(new ApacheHttpClient()) |
||||
.target(TestInterface.class, "http://localhost:" + server.getPort()); |
||||
|
||||
Response response = api.post("foo"); |
||||
|
||||
assertThat(response.status()).isEqualTo(200); |
||||
assertThat(response.reason()).isEqualTo("OK"); |
||||
assertThat(response.headers()) |
||||
.containsEntry("Content-Length", asList("3")) |
||||
.containsEntry("Foo", asList("Bar")); |
||||
assertThat(response.body().asInputStream()) |
||||
.hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8))); |
||||
|
||||
assertThat(server.takeRequest()).hasMethod("POST") |
||||
.hasPath("/?foo=bar&foo=baz&qux=") |
||||
.hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Accept: */*", "Content-Length: 3") |
||||
.hasBody("foo"); |
||||
} |
||||
|
||||
@Test |
||||
public void parsesErrorResponse() throws IOException, InterruptedException { |
||||
thrown.expect(FeignException.class); |
||||
thrown.expectMessage("status 500 reading TestInterface#post(String); content:\n" + "ARGHH"); |
||||
|
||||
server.enqueue(new MockResponse().setResponseCode(500).setBody("ARGHH")); |
||||
|
||||
TestInterface api = Feign.builder() |
||||
.client(new ApacheHttpClient()) |
||||
.target(TestInterface.class, "http://localhost:" + server.getPort()); |
||||
|
||||
api.post("foo"); |
||||
} |
||||
|
||||
@Test |
||||
public void patch() throws IOException, InterruptedException { |
||||
server.enqueue(new MockResponse().setBody("foo")); |
||||
server.enqueue(new MockResponse()); |
||||
|
||||
TestInterface api = Feign.builder() |
||||
.client(new ApacheHttpClient()) |
||||
.target(TestInterface.class, "http://localhost:" + server.getPort()); |
||||
|
||||
assertEquals("foo", api.patch()); |
||||
|
||||
assertThat(server.takeRequest()) |
||||
.hasHeaders("Accept: text/plain") |
||||
.hasNoHeaderNamed("Content-Type") |
||||
.hasMethod("PATCH"); |
||||
} |
||||
|
||||
interface TestInterface { |
||||
|
||||
@RequestLine("POST /?foo=bar&foo=baz&qux=") |
||||
@Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) |
||||
Response post(String body); |
||||
|
||||
@RequestLine("PATCH /") |
||||
@Headers("Accept: text/plain") |
||||
String patch(); |
||||
} |
||||
} |
Loading…
Reference in new issue