Skip to content

Commit

Permalink
Retrofit http client supplier npe fix (#1617)
Browse files Browse the repository at this point in the history
* Lombok update to 1.18.6

* Fixed NPE when http client supplier was specied and http client was not.

This patch addresses issue that resulted in NPE if http client supplier
was specified in `Call.Factory` builder and concrete http client was not,
because `getHttpClient()` method was not invoked while constructing
retrofit `Call` instance.

New, obviously less error prone approach is that http client supplier
gets constructed behind the scenes even if user specifies concrete http
client instance at call factory creation time and http client supplier
is being used exclusively also by `Call` instance. This way there are no
hard references to http client instance dangling around in case some
component creates a `Call` instance and never issues `newCall()` on it.

Fixes #1616.
  • Loading branch information
bfg authored and slandelle committed Feb 28, 2019
1 parent fdf9a7b commit bbaa4c3
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 38 deletions.
2 changes: 1 addition & 1 deletion extras/retrofit2/pom.xml
Expand Up @@ -13,7 +13,7 @@

<properties>
<retrofit2.version>2.5.0</retrofit2.version>
<lombok.version>1.16.20</lombok.version>
<lombok.version>1.18.6</lombok.version>
</properties>

<dependencies>
Expand Down
Expand Up @@ -30,6 +30,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
* {@link AsyncHttpClient} <a href="http://square.github.io/retrofit/">Retrofit2</a> {@link okhttp3.Call}
Expand All @@ -48,44 +49,52 @@ class AsyncHttpClientCall implements Cloneable, okhttp3.Call {
public static final long DEFAULT_EXECUTE_TIMEOUT_MILLIS = 30_000;

private static final ResponseBody EMPTY_BODY = ResponseBody.create(null, "");

/**
* Tells whether call has been executed.
*
* @see #isExecuted()
* @see #isCanceled()
*/
private final AtomicReference<CompletableFuture<Response>> futureRef = new AtomicReference<>();

/**
* HttpClient instance.
* {@link AsyncHttpClient} supplier
*/
@NonNull
AsyncHttpClient httpClient;
Supplier<AsyncHttpClient> httpClientSupplier;

/**
* {@link #execute()} response timeout in milliseconds.
*/
@Builder.Default
long executeTimeoutMillis = DEFAULT_EXECUTE_TIMEOUT_MILLIS;

/**
* Retrofit request.
*/
@NonNull
@Getter(AccessLevel.NONE)
Request request;

/**
* List of consumers that get called just before actual async-http-client request is being built.
*/
@Singular("requestCustomizer")
List<Consumer<RequestBuilder>> requestCustomizers;

/**
* List of consumers that get called just before actual HTTP request is being fired.
*/
@Singular("onRequestStart")
List<Consumer<Request>> onRequestStart;

/**
* List of consumers that get called when HTTP request finishes with an exception.
*/
@Singular("onRequestFailure")
List<Consumer<Throwable>> onRequestFailure;

/**
* List of consumers that get called when HTTP request finishes successfully.
*/
Expand Down Expand Up @@ -236,6 +245,20 @@ public Response onCompleted(org.asynchttpclient.Response response) {
return future;
}

/**
* Returns HTTP client.
*
* @return http client
* @throws IllegalArgumentException if {@link #httpClientSupplier} returned {@code null}.
*/
protected AsyncHttpClient getHttpClient() {
val httpClient = httpClientSupplier.get();
if (httpClient == null) {
throw new IllegalStateException("Async HTTP client instance supplier " + httpClientSupplier + " returned null.");
}
return httpClient;
}

/**
* Converts async-http-client response to okhttp response.
*
Expand Down
Expand Up @@ -18,42 +18,36 @@
import org.asynchttpclient.AsyncHttpClient;

import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;

import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumers;

/**
* {@link AsyncHttpClient} implementation of Retrofit2 {@link Call.Factory}
* {@link AsyncHttpClient} implementation of <a href="http://square.github.io/retrofit/">Retrofit2</a>
* {@link Call.Factory}.
*/
@Value
@Builder(toBuilder = true)
public class AsyncHttpClientCallFactory implements Call.Factory {
/**
* {@link AsyncHttpClient} in use.
*
* @see #httpClientSupplier
*/
@Getter(AccessLevel.NONE)
AsyncHttpClient httpClient;

/**
* Supplier of {@link AsyncHttpClient}, takes precedence over {@link #httpClient}.
* Supplier of {@link AsyncHttpClient}.
*/
@NonNull
@Getter(AccessLevel.NONE)
Supplier<AsyncHttpClient> httpClientSupplier;

/**
* List of {@link Call} builder customizers that are invoked just before creating it.
*/
@Singular("callCustomizer")
@Getter(AccessLevel.PACKAGE)
List<Consumer<AsyncHttpClientCall.AsyncHttpClientCallBuilder>> callCustomizers;

@Override
public Call newCall(Request request) {
val callBuilder = AsyncHttpClientCall.builder()
.httpClient(httpClient)
.httpClientSupplier(httpClientSupplier)
.request(request);

// customize builder before creating a call
Expand All @@ -64,15 +58,33 @@ public Call newCall(Request request) {
}

/**
* {@link AsyncHttpClient} in use by this factory.
* Returns {@link AsyncHttpClient} from {@link #httpClientSupplier}.
*
* @return
* @return http client.
*/
public AsyncHttpClient getHttpClient() {
return Optional.ofNullable(httpClientSupplier)
.map(Supplier::get)
.map(Optional::of)
.orElseGet(() -> Optional.ofNullable(httpClient))
.orElseThrow(() -> new IllegalStateException("HTTP client is not set."));
AsyncHttpClient getHttpClient() {
return httpClientSupplier.get();
}

/**
* Builder for {@link AsyncHttpClientCallFactory}.
*/
public static class AsyncHttpClientCallFactoryBuilder {
/**
* {@link AsyncHttpClient} supplier that returns http client to be used to execute HTTP requests.
*/
private Supplier<AsyncHttpClient> httpClientSupplier;

/**
* Sets concrete http client to be used by the factory to execute HTTP requests. Invocation of this method
* overrides any previous http client supplier set by {@link #httpClientSupplier(Supplier)}!
*
* @param httpClient http client
* @return reference to itself.
* @see #httpClientSupplier(Supplier)
*/
public AsyncHttpClientCallFactoryBuilder httpClient(@NonNull AsyncHttpClient httpClient) {
return httpClientSupplier(() -> httpClient);
}
}
}
Expand Up @@ -14,23 +14,34 @@

import lombok.extern.slf4j.Slf4j;
import lombok.val;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.RequestBuilder;
import org.testng.annotations.Test;

import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.REQUEST;
import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.createConsumer;
import static org.mockito.Mockito.mock;
import static org.testng.Assert.*;

@Slf4j
public class AsyncHttpClientCallFactoryTest {
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json");
private static final String JSON_BODY = "{\"foo\": \"bar\"}";
private static final RequestBody BODY = RequestBody.create(MEDIA_TYPE, JSON_BODY);
private static final String URL = "http://localhost:11000/foo/bar?a=b&c=d";
private static final Request REQUEST = new Request.Builder()
.post(BODY)
.addHeader("X-Foo", "Bar")
.url(URL)
.build();
@Test
void newCallShouldProduceExpectedResult() {
// given
Expand Down Expand Up @@ -152,7 +163,8 @@ void shouldApplyAllConsumersToCallBeingConstructed() {
assertTrue(call.getRequestCustomizers().size() == 2);
}

@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "HTTP client is not set.")
@Test(expectedExceptions = NullPointerException.class,
expectedExceptionsMessageRegExp = "httpClientSupplier is marked @NonNull but is null")
void shouldThrowISEIfHttpClientIsNotDefined() {
// given
val factory = AsyncHttpClientCallFactory.builder()
Expand All @@ -168,17 +180,23 @@ void shouldThrowISEIfHttpClientIsNotDefined() {
@Test
void shouldUseHttpClientInstanceIfSupplierIsNotAvailable() {
// given
val httpClientA = mock(AsyncHttpClient.class);
val httpClient = mock(AsyncHttpClient.class);

val factory = AsyncHttpClientCallFactory.builder()
.httpClient(httpClientA)
.httpClient(httpClient)
.build();

// when
val usedHttpClient = factory.getHttpClient();

// then
assertTrue(usedHttpClient == httpClientA);
assertTrue(usedHttpClient == httpClient);

// when
val call = (AsyncHttpClientCall) factory.newCall(REQUEST);

// then: call should contain correct http client
assertTrue(call.getHttpClient()== httpClient);
}

@Test
Expand All @@ -197,5 +215,12 @@ void shouldPreferHttpClientSupplierOverHttpClient() {

// then
assertTrue(usedHttpClient == httpClientB);

// when: try to create new call
val call = (AsyncHttpClientCall) factory.newCall(REQUEST);

// then: call should contain correct http client
assertNotNull(call);
assertTrue(call.getHttpClient() == httpClientB);
}
}
Expand Up @@ -24,6 +24,7 @@
import org.asynchttpclient.Response;
import org.mockito.ArgumentCaptor;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

Expand All @@ -35,10 +36,11 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;

import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumer;
import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumers;
import static org.mockito.Matchers.any;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;
Expand All @@ -47,19 +49,25 @@
public class AsyncHttpClientCallTest {
static final Request REQUEST = new Request.Builder().url("http://www.google.com/").build();

private AsyncHttpClient httpClient;
private Supplier<AsyncHttpClient> httpClientSupplier = () -> httpClient;

@BeforeMethod
void setup() {
this.httpClient = mock(AsyncHttpClient.class);
}

@Test(expectedExceptions = NullPointerException.class, dataProvider = "first")
void builderShouldThrowInCaseOfMissingProperties(AsyncHttpClientCall.AsyncHttpClientCallBuilder builder) {
builder.build();
}

@DataProvider(name = "first")
Object[][] dataProviderFirst() {
val httpClient = mock(AsyncHttpClient.class);

return new Object[][]{
{AsyncHttpClientCall.builder()},
{AsyncHttpClientCall.builder().request(REQUEST)},
{AsyncHttpClientCall.builder().httpClient(httpClient)}
{AsyncHttpClientCall.builder().httpClientSupplier(httpClientSupplier)}
};
}

Expand All @@ -77,7 +85,7 @@ void shouldInvokeConsumersOnEachExecution(Consumer<AsyncCompletionHandler<?>> ha
val numRequestCustomizer = new AtomicInteger();

// prepare http client mock
val httpClient = mock(AsyncHttpClient.class);
this.httpClient = mock(AsyncHttpClient.class);

val mockRequest = mock(org.asynchttpclient.Request.class);
when(mockRequest.getHeaders()).thenReturn(EmptyHttpHeaders.INSTANCE);
Expand All @@ -94,7 +102,7 @@ void shouldInvokeConsumersOnEachExecution(Consumer<AsyncCompletionHandler<?>> ha

// create call instance
val call = AsyncHttpClientCall.builder()
.httpClient(httpClient)
.httpClientSupplier(httpClientSupplier)
.request(REQUEST)
.onRequestStart(e -> numStarted.incrementAndGet())
.onRequestFailure(t -> numFailed.incrementAndGet())
Expand Down Expand Up @@ -163,7 +171,7 @@ Object[][] dataProviderSecond() {
void toIOExceptionShouldProduceExpectedResult(Throwable exception) {
// given
val call = AsyncHttpClientCall.builder()
.httpClient(mock(AsyncHttpClient.class))
.httpClientSupplier(httpClientSupplier)
.request(REQUEST)
.build();

Expand Down Expand Up @@ -295,6 +303,18 @@ public void bodyIsNotNullInResponse() throws Exception {
assertNotEquals(response.body(), null);
}

@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*returned null.")
void getHttpClientShouldThrowISEIfSupplierReturnsNull() {
// given:
val call = AsyncHttpClientCall.builder()
.httpClientSupplier(() -> null)
.request(requestWithBody())
.build();

// when: should throw ISE
call.getHttpClient();
}

private void givenResponseIsProduced(AsyncHttpClient client, Response response) {
when(client.executeRequest(any(org.asynchttpclient.Request.class), any())).thenAnswer(invocation -> {
AsyncCompletionHandler<Response> handler = invocation.getArgument(1);
Expand All @@ -304,9 +324,11 @@ private void givenResponseIsProduced(AsyncHttpClient client, Response response)
}

private okhttp3.Response whenRequestIsMade(AsyncHttpClient client, Request request) throws IOException {
AsyncHttpClientCall call = AsyncHttpClientCall.builder().httpClient(client).request(request).build();

return call.execute();
return AsyncHttpClientCall.builder()
.httpClientSupplier(() -> client)
.request(request)
.build()
.execute();
}

private Request requestWithBody() {
Expand Down

0 comments on commit bbaa4c3

Please sign in to comment.