Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ GoCardlessClient client = GoCardlessClient.newBuilder(accessToken)
.build();
```

By default requests use OkHttp's timeouts: 10 seconds each to connect, to read and to write, and no overall call
timeout. To change them, use `withConnectTimeout`, `withReadTimeout`, `withWriteTimeout` and `withCallTimeout`, each of which takes a
`java.time.Duration`. A request that times out raises a `GoCardlessNetworkException` once any automatic retries are
exhausted:

```java
GoCardlessClient client = GoCardlessClient.newBuilder(accessToken)
.withConnectTimeout(Duration.ofSeconds(5))
.withCallTimeout(Duration.ofSeconds(30))
.build();
```

To see the configurable options in full, see the documentation for `GoCardlessClient.Builder`.

If you're upgrading from v2.x, you'll need to update your code for initialising `GoCardlessClient`. See the
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/com/gocardless/GoCardlessClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.gocardless.services.*;
import com.google.common.annotations.VisibleForTesting;
import java.net.Proxy;
import java.time.Duration;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
Expand Down Expand Up @@ -65,6 +66,10 @@ public static final class Builder {
private final String accessToken;
private String baseUrl;
private Proxy proxy;
private Duration connectTimeout;
private Duration readTimeout;
private Duration writeTimeout;
private Duration callTimeout;
private SSLSocketFactory sslSocketFactory;
private X509TrustManager trustManager;
private boolean errorOnIdempotencyConflict;
Expand Down Expand Up @@ -114,6 +119,51 @@ public Builder withProxy(Proxy proxy) {
return this;
}

/**
* Configures how long to wait for a connection to be established. Defaults to 10 seconds.
*
* @param connectTimeout the maximum time to wait while connecting; zero means no timeout
*/
public Builder withConnectTimeout(Duration connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}

/**
* Configures how long to wait for data to arrive once connected, for example while waiting
* for the response to start. Defaults to 10 seconds.
*
* @param readTimeout the maximum time to wait for each read; zero means no timeout
*/
public Builder withReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
return this;
}

/**
* Configures how long to wait for each write of the request to complete. Defaults to 10
* seconds.
*
* @param writeTimeout the maximum time to wait for each write; zero means no timeout
*/
public Builder withWriteTimeout(Duration writeTimeout) {
this.writeTimeout = writeTimeout;
return this;
}

/**
* Configures a limit on the whole of a single attempt, from connecting to reading the
* response. There is no call timeout by default. Failed attempts are still retried (see
* {@link #withMaxNoOfRetries(int)}), so the total time before an error is raised can exceed
* this value.
*
* @param callTimeout the maximum time to allow for a single attempt; zero means no timeout
*/
public Builder withCallTimeout(Duration callTimeout) {
this.callTimeout = callTimeout;
return this;
}

/**
* Configures the SSL socket factory to be used when making HTTP requests
*
Expand Down Expand Up @@ -168,6 +218,18 @@ public Builder withWaitBetweenRetriesInMilliSeconds(long waitBetweenRetriesInMil
*/
public GoCardlessClient build() {
OkHttpClient.Builder rawClientBuilder = new OkHttpClient.Builder().proxy(proxy);
if (connectTimeout != null) {
rawClientBuilder.connectTimeout(connectTimeout);
}
if (readTimeout != null) {
rawClientBuilder.readTimeout(readTimeout);
}
if (writeTimeout != null) {
rawClientBuilder.writeTimeout(writeTimeout);
}
if (callTimeout != null) {
rawClientBuilder.callTimeout(callTimeout);
}
if (sslSocketFactory != null && trustManager != null) {
rawClientBuilder.sslSocketFactory(sslSocketFactory, trustManager);
}
Expand Down
63 changes: 63 additions & 0 deletions src/test/java/com/gocardless/GoCardlessClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static com.gocardless.services.MandateService.MandateListRequest.Status.FAILED;
import static com.gocardless.services.SubscriptionService.SubscriptionCreateRequest.IntervalUnit.MONTHLY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.gocardless.http.ApiResponse;
import com.gocardless.http.GoCardlessNetworkException;
Expand All @@ -16,7 +17,12 @@
import com.gocardless.services.SubscriptionService.SubscriptionCreateRequest;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.io.InterruptedIOException;
import java.lang.reflect.Field;
import java.net.SocketTimeoutException;
import java.time.Duration;
import java.util.List;
import okhttp3.OkHttpClient;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand Down Expand Up @@ -349,4 +355,61 @@ public void shouldDoMaxNoOfRetries() {
.withCity("London").withPostalCode("E8 3GX").withCountryCode("GB");
assertThat(client.getHttpClient().getMaxNoOfRetries()).isEqualTo(HttpClient.MAX_RETRIES);
}

private OkHttpClient rawClientOf(GoCardlessClient goCardlessClient) throws Exception {
Field field = HttpClient.class.getDeclaredField("rawClient");
field.setAccessible(true);
return (OkHttpClient) field.get(goCardlessClient.getHttpClient());
}

@Test
public void shouldUseFiniteTimeoutsByDefault() throws Exception {
OkHttpClient rawClient = rawClientOf(GoCardlessClient.newBuilder(ACCESS_TOKEN).build());
assertThat(rawClient.connectTimeoutMillis()).isEqualTo(10_000);
assertThat(rawClient.readTimeoutMillis()).isEqualTo(10_000);
assertThat(rawClient.writeTimeoutMillis()).isEqualTo(10_000);
assertThat(rawClient.callTimeoutMillis()).isEqualTo(0);
}

@Test
public void shouldApplyConfiguredTimeouts() throws Exception {
OkHttpClient rawClient = rawClientOf(GoCardlessClient.newBuilder(ACCESS_TOKEN)
.withConnectTimeout(Duration.ofSeconds(1)).withReadTimeout(Duration.ofSeconds(2))
.withWriteTimeout(Duration.ofSeconds(3)).withCallTimeout(Duration.ofSeconds(4))
.build());
assertThat(rawClient.connectTimeoutMillis()).isEqualTo(1_000);
assertThat(rawClient.readTimeoutMillis()).isEqualTo(2_000);
assertThat(rawClient.writeTimeoutMillis()).isEqualTo(3_000);
assertThat(rawClient.callTimeoutMillis()).isEqualTo(4_000);
}

@Test
public void shouldFailWhenReadTimeoutIsExceeded() throws Exception {
http.enqueueDelayedResponse(200, "fixtures/client/get_a_customer.json", 2000);
client = GoCardlessClient.newBuilder(ACCESS_TOKEN).withBaseUrl(http.getBaseUrl())
.withMaxNoOfRetries(1).withReadTimeout(Duration.ofMillis(100)).build();
assertThatThrownBy(() -> client.customers().get("CU00003068FG73").execute())
.isInstanceOf(GoCardlessNetworkException.class)
.hasCauseInstanceOf(SocketTimeoutException.class);
}

@Test
public void shouldFailWhenCallTimeoutIsExceeded() throws Exception {
http.enqueueDelayedResponse(200, "fixtures/client/get_a_customer.json", 2000);
client = GoCardlessClient.newBuilder(ACCESS_TOKEN).withBaseUrl(http.getBaseUrl())
.withMaxNoOfRetries(1).withCallTimeout(Duration.ofMillis(100)).build();
assertThatThrownBy(() -> client.customers().get("CU00003068FG73").execute())
.isInstanceOf(GoCardlessNetworkException.class)
.hasCauseInstanceOf(InterruptedIOException.class);
}

@Test
public void shouldSucceedWhenResponseArrivesWithinTimeout() throws Exception {
http.enqueueDelayedResponse(200, "fixtures/client/get_a_customer.json", 50);
client = GoCardlessClient.newBuilder(ACCESS_TOKEN).withBaseUrl(http.getBaseUrl())
.withReadTimeout(Duration.ofSeconds(5)).withCallTimeout(Duration.ofSeconds(5))
.build();
Customer customer = client.customers().get("CU00003068FG73").execute();
assertThat(customer.getId()).isEqualTo("CU00003068FG73");
}
}
7 changes: 7 additions & 0 deletions src/test/java/com/gocardless/http/MockHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ public void enqueueResponse(int statusCode, String fixturePath, Map<String, Stri
server.enqueue(response);
}

public void enqueueDelayedResponse(int statusCode, String fixturePath, long delayMillis)
throws Exception {
String body = Resources.toString(Resources.getResource(fixturePath), UTF_8);
server.enqueue(new MockResponse().setBody(body).setResponseCode(statusCode)
.setHeadersDelay(delayMillis, java.util.concurrent.TimeUnit.MILLISECONDS));
}

public void enqueueNetworkFailure() {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
}
Expand Down
Loading