diff --git a/.codegen.json b/.codegen.json index fcdea3595..ef6446242 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "482939a", "specHash": "77eac4b", "version": "10.5.0" } +{ "engineHash": "bc04b80", "specHash": "77eac4b", "version": "10.5.0" } diff --git a/docs/client.md b/docs/client.md index 9a871d55e..7e63120f4 100644 --- a/docs/client.md +++ b/docs/client.md @@ -17,6 +17,7 @@ divided across resource managers. - [Custom headers](#custom-headers) - [Custom Base URLs](#custom-base-urls) - [Interceptors](#interceptors) +- [Use Timeouts for API calls](#use-timeouts-for-api-calls) - [Use Proxy for API calls](#use-proxy-for-api-calls) @@ -178,6 +179,18 @@ List interceptors = new ArrayList<>() { BoxClient clientWithInterceptor = client.withInterceptors(interceptors); ``` +# Use Timeouts for API calls + +In order to configure timeout for API calls, calling the `client.withTimeouts(config)` method creates a new client with timeout settings, leaving the original client unmodified. + +```java +TimeoutConfig timeoutConfig = new TimeoutConfig.Builder() + .connectionTimeoutMs(10000L) + .readTimeoutMs(30000L) + .build(); +BoxClient newClient = client.withTimeouts(timeoutConfig); +``` + # Use Proxy for API calls In order to use a proxy for API calls, calling the `client.withProxy(proxyConfig)` method creates a new client, leaving the original client unmodified, with the username and password being optional. We only support adding proxy for BoxNetworkClient. If you are using your own implementation of NetworkClient, you would need to configure proxy on your own. diff --git a/docs/configuration.md b/docs/configuration.md index 8b49875b1..cb65813c5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,6 +13,7 @@ - [Network Exception Handling](#network-exception-handling) - [Customizing Retry Parameters](#customizing-retry-parameters) - [Custom Retry Strategy](#custom-retry-strategy) +- [Timeouts](#timeouts) @@ -173,3 +174,34 @@ BoxClient client = new BoxClient.Builder(auth) .networkSession(session) .build(); ``` + +## Timeouts + +You can configure network timeouts with `TimeoutConfig` on `NetworkSession`. +Java SDK supports separate values for connection and read timeouts, both in milliseconds. + +```java +BoxDeveloperTokenAuth auth = new BoxDeveloperTokenAuth("DEVELOPER_TOKEN"); +TimeoutConfig timeoutConfig = new TimeoutConfig.Builder() + .connectionTimeoutMs(10000L) + .readTimeoutMs(30000L) + .build(); + +NetworkSession session = new NetworkSession() + .withTimeoutConfig(timeoutConfig); + +BoxClient client = new BoxClient.Builder(auth) + .networkSession(session) + .build(); +``` + +How timeout handling works: + +- `connectionTimeoutMs` controls how long the client waits to establish a connection. +- `readTimeoutMs` controls how long the client waits for data while reading the response. +- Each timeout is optional. If a value is not provided, the client keeps its existing timeout for that setting. +- To disable both timeouts, set `connectionTimeoutMs(0L)` and `readTimeoutMs(0L)`. +- You can also disable only one timeout by setting just one of them to `0L` and leaving the other configured. +- Timeout failures are handled as request exceptions, then retry behavior is controlled by the configured retry strategy +- If retries are exhausted after timeout failures, the SDK throws `BoxSDKError` with the underlying timeout exception as the cause. +- Timeout applies to a single HTTP request attempt to the Box API (not the total time across all retries). diff --git a/src/main/java/com/box/sdkgen/client/BoxClient.java b/src/main/java/com/box/sdkgen/client/BoxClient.java index 4ead1e87f..e11f46248 100644 --- a/src/main/java/com/box/sdkgen/client/BoxClient.java +++ b/src/main/java/com/box/sdkgen/client/BoxClient.java @@ -92,6 +92,7 @@ import com.box.sdkgen.networking.interceptors.Interceptor; import com.box.sdkgen.networking.network.NetworkSession; import com.box.sdkgen.networking.proxyconfig.ProxyConfig; +import com.box.sdkgen.networking.timeoutconfig.TimeoutConfig; import java.util.List; import java.util.Map; @@ -1046,6 +1047,17 @@ public BoxClient withProxy(ProxyConfig config) { .build(); } + /** + * Create a new client with custom timeouts that will be used for every API call + * + * @param config Timeout configuration. + */ + public BoxClient withTimeouts(TimeoutConfig config) { + return new BoxClient.Builder(this.auth) + .networkSession(this.networkSession.withTimeoutConfig(config)) + .build(); + } + /** * Create a new client with a custom set of interceptors that will be used for every API call * diff --git a/src/main/java/com/box/sdkgen/networking/boxnetworkclient/BoxNetworkClient.java b/src/main/java/com/box/sdkgen/networking/boxnetworkclient/BoxNetworkClient.java index 5f8d18851..a63c8b1d2 100644 --- a/src/main/java/com/box/sdkgen/networking/boxnetworkclient/BoxNetworkClient.java +++ b/src/main/java/com/box/sdkgen/networking/boxnetworkclient/BoxNetworkClient.java @@ -19,6 +19,7 @@ import com.box.sdkgen.networking.network.NetworkSession; import com.box.sdkgen.networking.networkclient.NetworkClient; import com.box.sdkgen.networking.proxyconfig.ProxyConfig; +import com.box.sdkgen.networking.timeoutconfig.TimeoutConfig; import com.fasterxml.jackson.databind.JsonNode; import java.io.IOException; import java.net.InetSocketAddress; @@ -97,6 +98,31 @@ public BoxNetworkClient withProxy(ProxyConfig config) { return new BoxNetworkClient(clientBuilder.build()); } + public BoxNetworkClient withTimeoutConfig(TimeoutConfig config) { + if (config == null) { + throw new IllegalArgumentException("TimeoutConfig cannot be null"); + } + + OkHttpClient.Builder clientBuilder = httpClient.newBuilder(); + + Long connectionTimeoutMs = config.getConnectionTimeoutMs(); + if (connectionTimeoutMs != null) { + if (connectionTimeoutMs < 0) { + throw new IllegalArgumentException("connectionTimeoutMs cannot be negative"); + } + clientBuilder.connectTimeout(connectionTimeoutMs.longValue(), TimeUnit.MILLISECONDS); + } + + Long readTimeoutMs = config.getReadTimeoutMs(); + if (readTimeoutMs != null) { + if (readTimeoutMs < 0) { + throw new IllegalArgumentException("readTimeoutMs cannot be negative"); + } + clientBuilder.readTimeout(readTimeoutMs.longValue(), TimeUnit.MILLISECONDS); + } + return new BoxNetworkClient(clientBuilder.build()); + } + public FetchResponse fetch(FetchOptions options) { NetworkSession networkSession = options.getNetworkSession() == null ? new NetworkSession() : options.getNetworkSession(); diff --git a/src/main/java/com/box/sdkgen/networking/network/NetworkSession.java b/src/main/java/com/box/sdkgen/networking/network/NetworkSession.java index 3c453ec9d..60a32bff6 100644 --- a/src/main/java/com/box/sdkgen/networking/network/NetworkSession.java +++ b/src/main/java/com/box/sdkgen/networking/network/NetworkSession.java @@ -9,6 +9,7 @@ import com.box.sdkgen.networking.proxyconfig.ProxyConfig; import com.box.sdkgen.networking.retries.BoxRetryStrategy; import com.box.sdkgen.networking.retries.RetryStrategy; +import com.box.sdkgen.networking.timeoutconfig.TimeoutConfig; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -31,6 +32,8 @@ public class NetworkSession { protected ProxyConfig proxyConfig; + protected TimeoutConfig timeoutConfig; + public NetworkSession() { networkClient = new BoxNetworkClient(); retryStrategy = new BoxRetryStrategy(); @@ -45,6 +48,7 @@ protected NetworkSession(Builder builder) { this.retryStrategy = builder.retryStrategy; this.dataSanitizer = builder.dataSanitizer; this.proxyConfig = builder.proxyConfig; + this.timeoutConfig = builder.timeoutConfig; } public NetworkSession withAdditionalHeaders() { @@ -63,6 +67,7 @@ public NetworkSession withAdditionalHeaders(Map additionalHeader .retryStrategy(this.retryStrategy) .dataSanitizer(this.dataSanitizer) .proxyConfig(this.proxyConfig) + .timeoutConfig(this.timeoutConfig) .build(); } @@ -75,6 +80,7 @@ public NetworkSession withCustomBaseUrls(BaseUrls baseUrls) { .retryStrategy(this.retryStrategy) .dataSanitizer(this.dataSanitizer) .proxyConfig(this.proxyConfig) + .timeoutConfig(this.timeoutConfig) .build(); } @@ -90,6 +96,7 @@ public NetworkSession withInterceptors(List interceptors) { .retryStrategy(this.retryStrategy) .dataSanitizer(this.dataSanitizer) .proxyConfig(this.proxyConfig) + .timeoutConfig(this.timeoutConfig) .build(); } @@ -102,6 +109,7 @@ public NetworkSession withNetworkClient(NetworkClient networkClient) { .retryStrategy(this.retryStrategy) .dataSanitizer(this.dataSanitizer) .proxyConfig(this.proxyConfig) + .timeoutConfig(this.timeoutConfig) .build(); } @@ -114,6 +122,7 @@ public NetworkSession withRetryStrategy(RetryStrategy retryStrategy) { .retryStrategy(retryStrategy) .dataSanitizer(this.dataSanitizer) .proxyConfig(this.proxyConfig) + .timeoutConfig(this.timeoutConfig) .build(); } @@ -126,6 +135,7 @@ public NetworkSession withDataSanitizer(DataSanitizer dataSanitizer) { .retryStrategy(this.retryStrategy) .dataSanitizer(dataSanitizer) .proxyConfig(this.proxyConfig) + .timeoutConfig(this.timeoutConfig) .build(); } @@ -145,6 +155,30 @@ public NetworkSession withProxy(ProxyConfig config) { .retryStrategy(this.retryStrategy) .dataSanitizer(this.dataSanitizer) .proxyConfig(config) + .timeoutConfig(this.timeoutConfig) + .build(); + } + + public NetworkSession withTimeoutConfig(TimeoutConfig timeoutConfig) { + if (timeoutConfig == null) { + throw new IllegalArgumentException("TimeoutConfig cannot be null"); + } + + if (!(this.networkClient instanceof BoxNetworkClient)) { + throw new BoxSDKError("Timeouts are only supported for BoxNetworkClient"); + } + + BoxNetworkClient newClient = + ((BoxNetworkClient) this.networkClient).withTimeoutConfig(timeoutConfig); + return new Builder() + .additionalHeaders(this.additionalHeaders) + .baseUrls(this.baseUrls) + .interceptors(this.interceptors) + .networkClient(newClient) + .retryStrategy(this.retryStrategy) + .dataSanitizer(this.dataSanitizer) + .proxyConfig(this.proxyConfig) + .timeoutConfig(timeoutConfig) .build(); } @@ -176,6 +210,10 @@ public ProxyConfig getProxyConfig() { return proxyConfig; } + public TimeoutConfig getTimeoutConfig() { + return timeoutConfig; + } + public static class Builder { protected Map additionalHeaders = new HashMap<>(); @@ -192,6 +230,8 @@ public static class Builder { protected ProxyConfig proxyConfig; + protected TimeoutConfig timeoutConfig; + public Builder() { networkClient = new BoxNetworkClient(); retryStrategy = new BoxRetryStrategy(); @@ -233,6 +273,11 @@ public Builder proxyConfig(ProxyConfig proxyConfig) { return this; } + public Builder timeoutConfig(TimeoutConfig timeoutConfig) { + this.timeoutConfig = timeoutConfig; + return this; + } + public NetworkSession build() { return new NetworkSession(this); } diff --git a/src/main/java/com/box/sdkgen/networking/timeoutconfig/TimeoutConfig.java b/src/main/java/com/box/sdkgen/networking/timeoutconfig/TimeoutConfig.java new file mode 100644 index 000000000..728f5b092 --- /dev/null +++ b/src/main/java/com/box/sdkgen/networking/timeoutconfig/TimeoutConfig.java @@ -0,0 +1,44 @@ +package com.box.sdkgen.networking.timeoutconfig; + +public class TimeoutConfig { + + public Long connectionTimeoutMs; + + public Long readTimeoutMs; + + public TimeoutConfig() {} + + protected TimeoutConfig(Builder builder) { + this.connectionTimeoutMs = builder.connectionTimeoutMs; + this.readTimeoutMs = builder.readTimeoutMs; + } + + public Long getConnectionTimeoutMs() { + return connectionTimeoutMs; + } + + public Long getReadTimeoutMs() { + return readTimeoutMs; + } + + public static class Builder { + + protected Long connectionTimeoutMs; + + protected Long readTimeoutMs; + + public Builder connectionTimeoutMs(Long connectionTimeoutMs) { + this.connectionTimeoutMs = connectionTimeoutMs; + return this; + } + + public Builder readTimeoutMs(Long readTimeoutMs) { + this.readTimeoutMs = readTimeoutMs; + return this; + } + + public TimeoutConfig build() { + return new TimeoutConfig(this); + } + } +} diff --git a/src/test/java/com/box/sdkgen/client/ClientITest.java b/src/test/java/com/box/sdkgen/client/ClientITest.java index dcae23882..68f4a1a41 100644 --- a/src/test/java/com/box/sdkgen/client/ClientITest.java +++ b/src/test/java/com/box/sdkgen/client/ClientITest.java @@ -26,6 +26,7 @@ import com.box.sdkgen.networking.fetchoptions.MultipartItem; import com.box.sdkgen.networking.fetchoptions.ResponseFormat; import com.box.sdkgen.networking.fetchresponse.FetchResponse; +import com.box.sdkgen.networking.timeoutconfig.TimeoutConfig; import com.box.sdkgen.schemas.filefull.FileFull; import com.box.sdkgen.schemas.files.Files; import com.box.sdkgen.schemas.folderfull.FolderFull; @@ -199,6 +200,22 @@ public void testWithCustomBaseUrls() { assertThrows(RuntimeException.class, () -> customBaseClient.getUsers().getUserMe()); } + @Test + public void testWithTimeoutWhenTimeoutOccurs() { + long readTimeoutMs = 1; + BoxClient clientWithTimeout = + client.withTimeouts(new TimeoutConfig.Builder().readTimeoutMs(readTimeoutMs).build()); + assertThrows(RuntimeException.class, () -> clientWithTimeout.getUsers().getUserMe()); + } + + @Test + public void testWithTimeoutWhenTimeoutDoesNotOccur() { + long readTimeoutMs = 10000; + BoxClient clientWithTimeout = + client.withTimeouts(new TimeoutConfig.Builder().readTimeoutMs(readTimeoutMs).build()); + clientWithTimeout.getUsers().getUserMe(); + } + @Test public void testWithInterceptors() { UserFull user = client.getUsers().getUserMe();