Adrian Cole
10 years ago
13 changed files with 281 additions and 7 deletions
@ -0,0 +1,12 @@ |
|||||||
|
OkHttp |
||||||
|
=================== |
||||||
|
|
||||||
|
This module directs Feign's http requests to [OkHttp](http://square.github.io/okhttp/), which enables SPDY and better network control. |
||||||
|
|
||||||
|
To use OkHttp with Feign, add the OkHttp module to your classpath. Then, configure Feign to use the OkHttpClient: |
||||||
|
|
||||||
|
```java |
||||||
|
GitHub github = Feign.builder() |
||||||
|
.client(new OkHttpClient()) |
||||||
|
.target(GitHub.class, "https://api.github.com"); |
||||||
|
``` |
@ -0,0 +1,12 @@ |
|||||||
|
apply plugin: 'java' |
||||||
|
|
||||||
|
sourceCompatibility = 1.6 |
||||||
|
|
||||||
|
dependencies { |
||||||
|
compile project(':feign-core') |
||||||
|
compile 'com.squareup.okhttp:okhttp:2.2.0' |
||||||
|
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,131 @@ |
|||||||
|
/* |
||||||
|
* 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.okhttp; |
||||||
|
|
||||||
|
import com.squareup.okhttp.Headers; |
||||||
|
import com.squareup.okhttp.MediaType; |
||||||
|
import com.squareup.okhttp.Request; |
||||||
|
import com.squareup.okhttp.RequestBody; |
||||||
|
import com.squareup.okhttp.Response; |
||||||
|
import com.squareup.okhttp.ResponseBody; |
||||||
|
import feign.Client; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.io.Reader; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
/** |
||||||
|
* This module directs Feign's http requests to <a href="http://square.github.io/okhttp/">OkHttp</a>, which enables |
||||||
|
* SPDY and better network control. |
||||||
|
* Ex. |
||||||
|
* <pre> |
||||||
|
* GitHub github = Feign.builder().client(new OkHttpClient()).target(GitHub.class, "https://api.github.com"); |
||||||
|
*/ |
||||||
|
public final class OkHttpClient implements Client { |
||||||
|
private final com.squareup.okhttp.OkHttpClient delegate; |
||||||
|
|
||||||
|
public OkHttpClient() { |
||||||
|
this(new com.squareup.okhttp.OkHttpClient()); |
||||||
|
} |
||||||
|
|
||||||
|
public OkHttpClient(com.squareup.okhttp.OkHttpClient delegate) { |
||||||
|
this.delegate = delegate; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public feign.Response execute(feign.Request input, feign.Request.Options options) throws IOException { |
||||||
|
com.squareup.okhttp.OkHttpClient requestScoped; |
||||||
|
if (delegate.getConnectTimeout() != options.connectTimeoutMillis() |
||||||
|
|| delegate.getReadTimeout() != options.readTimeoutMillis()) { |
||||||
|
requestScoped = delegate.clone(); |
||||||
|
requestScoped.setConnectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS); |
||||||
|
requestScoped.setReadTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS); |
||||||
|
} else { |
||||||
|
requestScoped = delegate; |
||||||
|
} |
||||||
|
Request request = toOkHttpRequest(input); |
||||||
|
Response response = requestScoped.newCall(request).execute(); |
||||||
|
return toFeignResponse(response); |
||||||
|
} |
||||||
|
|
||||||
|
static Request toOkHttpRequest(feign.Request input) { |
||||||
|
Request.Builder requestBuilder = new Request.Builder(); |
||||||
|
requestBuilder.url(input.url()); |
||||||
|
|
||||||
|
MediaType mediaType = null; |
||||||
|
for (String field : input.headers().keySet()) { |
||||||
|
for (String value : input.headers().get(field)) { |
||||||
|
if (field.equalsIgnoreCase("Content-Type")) { |
||||||
|
mediaType = MediaType.parse(value); |
||||||
|
if (input.charset() != null) mediaType.charset(input.charset()); |
||||||
|
} else { |
||||||
|
requestBuilder.addHeader(field, value); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
RequestBody body = input.body() != null ? RequestBody.create(mediaType, input.body()) : null; |
||||||
|
requestBuilder.method(input.method(), body); |
||||||
|
return requestBuilder.build(); |
||||||
|
} |
||||||
|
|
||||||
|
private static feign.Response toFeignResponse(Response input) { |
||||||
|
return feign.Response.create(input.code(), input.message(), toMap(input.headers()), toBody(input.body())); |
||||||
|
} |
||||||
|
|
||||||
|
private static Map<String, Collection<String>> toMap(Headers headers) { |
||||||
|
Map<String, Collection<String>> result = new LinkedHashMap<String, Collection<String>>(headers.size()); |
||||||
|
for (String name : headers.names()) { |
||||||
|
// TODO: this is very inefficient as headers.values iterate case insensitively.
|
||||||
|
result.put(name, headers.values(name)); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private static feign.Response.Body toBody(final ResponseBody input) { |
||||||
|
if (input == null || input.contentLength() == 0) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
if (input.contentLength() > Integer.MAX_VALUE) { |
||||||
|
throw new UnsupportedOperationException("Length too long "+ input.contentLength()); |
||||||
|
} |
||||||
|
final Integer length = input.contentLength() != -1 ? (int) input.contentLength() : null; |
||||||
|
|
||||||
|
return new feign.Response.Body() { |
||||||
|
|
||||||
|
@Override public void close() throws IOException { |
||||||
|
input.close(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override public Integer length() { |
||||||
|
return length; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public boolean isRepeatable() { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public InputStream asInputStream() throws IOException { |
||||||
|
return input.byteStream(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override public Reader asReader() throws IOException { |
||||||
|
return input.charStream(); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,103 @@ |
|||||||
|
/* |
||||||
|
* 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.okhttp; |
||||||
|
|
||||||
|
import com.squareup.okhttp.mockwebserver.MockResponse; |
||||||
|
import com.squareup.okhttp.mockwebserver.SocketPolicy; |
||||||
|
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; |
||||||
|
import dagger.Lazy; |
||||||
|
import feign.Client; |
||||||
|
import feign.Feign; |
||||||
|
import feign.FeignException; |
||||||
|
import feign.Headers; |
||||||
|
import feign.RequestLine; |
||||||
|
import feign.Response; |
||||||
|
import feign.Util; |
||||||
|
import java.io.ByteArrayInputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import org.junit.Rule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.rules.ExpectedException; |
||||||
|
|
||||||
|
import static feign.Util.UTF_8; |
||||||
|
import static feign.assertj.MockWebServerAssertions.assertThat; |
||||||
|
import static java.util.Arrays.asList; |
||||||
|
import static org.assertj.core.data.MapEntry.entry; |
||||||
|
import static org.hamcrest.core.Is.isA; |
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
|
||||||
|
public class OkHttpClientTest { |
||||||
|
@Rule public final ExpectedException thrown = ExpectedException.none(); |
||||||
|
@Rule public final MockWebServerRule server = new MockWebServerRule(); |
||||||
|
|
||||||
|
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 /") String patch(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test public void parsesRequestAndResponse() throws IOException, InterruptedException { |
||||||
|
server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar")); |
||||||
|
|
||||||
|
TestInterface api = Feign.builder() |
||||||
|
.client(new OkHttpClient()) |
||||||
|
.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: ", "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 OkHttpClient()) |
||||||
|
.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 OkHttpClient()) |
||||||
|
.target(TestInterface.class, "http://localhost:" + server.getPort()); |
||||||
|
|
||||||
|
assertEquals("foo", api.patch()); |
||||||
|
|
||||||
|
assertThat(server.takeRequest()) |
||||||
|
.hasHeaders("Content-Length: 0") // Note: OkHttp adds content length.
|
||||||
|
.hasNoHeaderNamed("Content-Type") |
||||||
|
.hasMethod("PATCH"); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue