Skip to content

Commit

Permalink
Adds OkHttp integration
Browse files Browse the repository at this point in the history
closes #134
  • Loading branch information
Adrian Cole committed Jan 26, 2015
1 parent 4fb242c commit a6724a4
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### Version 7.1
* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
* Adds OkHttp integration
* Allows multiple headers with the same name.

### Version 7.0
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ GitHub github = Feign.builder()
.contract(new JAXRSModule.JAXRSContract())
.target(GitHub.class, "https://api.github.com");
```
### OkHttp
[OkHttpClient](https://github.com/Netflix/feign/tree/master/okhttp) 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");
```

### Ribbon
[RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon).

Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/feign/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public static Response create(
return new Response(status, reason, headers, ByteArrayBody.orNull(text, charset));
}

public static Response create(
int status, String reason, Map<String, Collection<String>> headers, Body body) {
return new Response(status, reason, headers, body);
}

private Response(int status, String reason, Map<String, Collection<String>> headers, Body body) {
checkState(status >= 200, "Invalid status code: %s", status);
this.status = status;
Expand Down
3 changes: 2 additions & 1 deletion core/src/test/java/feign/codec/DefaultDecoderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ private Response knownResponse() {
}

private Response nullBodyResponse() {
return Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), null);
return Response.create(
200, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
}
}
4 changes: 2 additions & 2 deletions core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public void throwsFeignException() throws Throwable {
thrown.expect(FeignException.class);
thrown.expectMessage("status 500 reading Service#foo()");

Response response = Response.create(500, "Internal server error", headers, null);
Response response = Response.create(500, "Internal server error", headers, (byte[]) null);

throw errorDecoder.decode("Service#foo()", response);
}
Expand All @@ -62,7 +62,7 @@ public void retryAfterHeaderThrowsRetryableException() throws Throwable {
thrown.expectMessage("status 503 reading Service#foo()");

headers.put(RETRY_AFTER, Arrays.asList("Sat, 1 Jan 2000 00:00:00 GMT"));
Response response = Response.create(503, "Service Unavailable", headers, null);
Response response = Response.create(503, "Service Unavailable", headers, (byte[]) null);

throw errorDecoder.decode("Service#foo()", response);
}
Expand Down
3 changes: 2 additions & 1 deletion gson/src/test/java/feign/gson/GsonModuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ public void nullBodyDecodesToNull() throws Exception {
ObjectGraph.create(bindings).inject(bindings);

Response response =
Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), null);
Response.create(
204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
assertNull(bindings.decoder.decode(response, String.class));
}

Expand Down
3 changes: 2 additions & 1 deletion jackson/src/test/java/feign/jackson/JacksonModuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ public void nullBodyDecodesToNull() throws Exception {
ObjectGraph.create(bindings).inject(bindings);

Response response =
Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), null);
Response.create(
204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
assertNull(bindings.decoder.decode(response, String.class));
}

Expand Down
12 changes: 12 additions & 0 deletions okhttp/README.md
Original file line number Diff line number Diff line change
@@ -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");
```
12 changes: 12 additions & 0 deletions okhttp/build.gradle
Original file line number Diff line number Diff line change
@@ -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
}
140 changes: 140 additions & 0 deletions okhttp/src/main/java/feign/okhttp/OkHttpClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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();
}
};
}
}
107 changes: 107 additions & 0 deletions okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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 static feign.Util.UTF_8;
import static feign.assertj.MockWebServerAssertions.assertThat;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;

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 java.io.ByteArrayInputStream;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

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");
}
}
3 changes: 2 additions & 1 deletion sax/src/test/java/feign/sax/SAXDecoderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ public void characters(char ch[], int start, int length) {
@Test
public void nullBodyDecodesToNull() throws Exception {
Response response =
Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), null);
Response.create(
204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
assertNull(decoder.decode(response, String.class));
}
}
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
rootProject.name='feign'
include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia'
include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia'

rootProject.children.each { childProject ->
childProject.name = 'feign-' + childProject.name
Expand Down

0 comments on commit a6724a4

Please sign in to comment.