From be84888b0a58a0a8a8983d95c2dc64e0e796e457 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Mon, 16 Feb 2026 02:50:16 -0800 Subject: [PATCH 1/4] test: assume that exception is thrown from delete archive (box/box-codegen#927) --- .codegen.json | 2 +- package-lock.json | 24 ++++++++++++------------ src/test/archives.test.ts | 1 - 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.codegen.json b/.codegen.json index cf002cd6..16f59e2d 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "bfb97cc", "specHash": "77eac4b", "version": "10.4.0" } +{ "engineHash": "f36ed52", "specHash": "77eac4b", "version": "10.4.0" } diff --git a/package-lock.json b/package-lock.json index 93d981f6..628099f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1925,9 +1925,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "peer": true, @@ -2402,9 +2402,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", "dev": true, "funding": [ { @@ -6273,9 +6273,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.105.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.1.tgz", - "integrity": "sha512-Gdj3X74CLJJ8zy4URmK42W7wTZUJrqL+z8nyGEr4dTN0kb3nVs+ZvjbTOqRYPD7qX4tUmwyHL9Q9K6T1seW6Yw==", + "version": "5.105.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", + "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", "dev": true, "license": "MIT", "peer": true, @@ -6323,9 +6323,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "peer": true, diff --git a/src/test/archives.test.ts b/src/test/archives.test.ts index 3651cc65..3483d8a6 100644 --- a/src/test/archives.test.ts +++ b/src/test/archives.test.ts @@ -65,7 +65,6 @@ test('testArchivesCreateListDelete', async function testArchivesCreateListDelete if (!(archives.entries!.length > 0)) { throw new Error('Assertion failed'); } - await client.archives.deleteArchiveByIdV2025R0(archive.id); await expect(async () => { await client.archives.deleteArchiveByIdV2025R0(archive.id); }).rejects.toThrow(); From 93d971ae1b25a5fd118e3b6342c02319d39e5a07 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Mon, 16 Feb 2026 06:20:23 -0800 Subject: [PATCH 2/4] fix: Convert retryAfter seconds to milliseconds for setTimeout (box/box-codegen#926) --- .codegen.json | 2 +- src/networking/boxNetworkClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.codegen.json b/.codegen.json index 16f59e2d..bc25f9a9 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "f36ed52", "specHash": "77eac4b", "version": "10.4.0" } +{ "engineHash": "9dcb945", "specHash": "77eac4b", "version": "10.4.0" } diff --git a/src/networking/boxNetworkClient.ts b/src/networking/boxNetworkClient.ts index a1df52e0..d0ecd063 100644 --- a/src/networking/boxNetworkClient.ts +++ b/src/networking/boxNetworkClient.ts @@ -263,7 +263,7 @@ export class BoxNetworkClient implements NetworkClient { fetchResponse, attemptForRetry, ); - await new Promise((resolve) => setTimeout(resolve, retryTimeout)); + await new Promise((resolve) => setTimeout(resolve, retryTimeout * 1000)); return this.fetch({ ...options, attemptNumber: attemptNumber + 1, From ac54e64ab01fa968c7bad19bb87b1dba45faac70 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Mon, 16 Feb 2026 09:20:33 -0800 Subject: [PATCH 3/4] docs: Improve documentation for retry strategies (box/box-codegen#925) --- .codegen.json | 2 +- docs/configuration.md | 147 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 136 insertions(+), 13 deletions(-) diff --git a/.codegen.json b/.codegen.json index bc25f9a9..f8eaf4e6 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "9dcb945", "specHash": "77eac4b", "version": "10.4.0" } +{ "engineHash": "482939a", "specHash": "77eac4b", "version": "10.4.0" } diff --git a/docs/configuration.md b/docs/configuration.md index 83fa0f65..a5bf1086 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -3,37 +3,160 @@ -- [Max retry attempts](#max-retry-attempts) -- [Custom retry strategy](#custom-retry-strategy) +- [Retry Strategy](#retry-strategy) + - [Overview](#overview) + - [Default Configuration](#default-configuration) + - [Retry Decision Flow](#retry-decision-flow) + - [Exponential Backoff Algorithm](#exponential-backoff-algorithm) + - [Example Delays (with default settings)](#example-delays-with-default-settings) + - [Retry-After Header](#retry-after-header) + - [Network Exception Handling](#network-exception-handling) + - [Customizing Retry Parameters](#customizing-retry-parameters) + - [Custom Retry Strategy](#custom-retry-strategy) -## Max retry attempts +## Retry Strategy -The default maximum number of retries in case of failed API call is 5. -To change this number you should initialize `BoxRetryStrategy` with the new value and pass it to `NetworkSession`. +### Overview -```js +The SDK ships with a built-in retry strategy (`BoxRetryStrategy`) that implements the `RetryStrategy` interface. The `BoxNetworkClient`, which serves as the default network client, uses this strategy to automatically retry failed API requests with exponential backoff. + +The retry strategy exposes two methods: + +- **`shouldRetry`** — Determines whether a failed request should be retried based on the HTTP status code, response headers, attempt count, and authentication state. +- **`retryAfter`** — Computes the delay (in seconds) before the next retry attempt, using either the server-provided `Retry-After` header or an exponential backoff formula. + +### Default Configuration + +| Parameter | Default | Description | +| -------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `maxAttempts` | `5` | Maximum number of retry attempts for HTTP error responses (status 4xx/5xx). | +| `retryBaseInterval` | `1` (second) | Base interval used in the exponential backoff calculation. | +| `retryRandomizationFactor` | `0.5` | Jitter factor applied to the backoff delay. The actual delay is multiplied by a random value between `1 - factor` and `1 + factor`. | +| `maxRetriesOnException` | `2` | Maximum number of retries for network-level exceptions (connection failures, timeouts). These are tracked by a separate counter from HTTP error retries. | + +### Retry Decision Flow + +The following diagram shows how `BoxRetryStrategy.shouldRetry` decides whether to retry a request: + +``` + shouldRetry(fetchOptions, fetchResponse, attemptNumber) + | + v + +-----------------------+ + | status == 0 | Yes + | (network exception)? |----------> attemptNumber <= maxRetriesOnException? + +-----------------------+ | | + | No Yes No + v | | + +-----------------------+ [RETRY] [NO RETRY] + | attemptNumber >= | + | maxAttempts? | + +-----------------------+ + | | + Yes No + | | + [NO RETRY] v + +-----------------------+ + | status == 202 AND | Yes + | Retry-After header? |----------> [RETRY] + +-----------------------+ + | No + v + +-----------------------+ + | status >= 500 | Yes + | (server error)? |----------> [RETRY] + +-----------------------+ + | No + v + +-----------------------+ + | status == 429 | Yes + | (rate limited)? |----------> [RETRY] + +-----------------------+ + | No + v + +-----------------------+ + | status == 401 AND | Yes + | auth available? |----------> Refresh token, then [RETRY] + +-----------------------+ + | No + v + [NO RETRY] +``` + +### Exponential Backoff Algorithm + +When the response does not include a `Retry-After` header, the retry delay is computed using exponential backoff with randomized jitter: + +``` +delay = 2^attemptNumber * retryBaseInterval * random(1 - factor, 1 + factor) +``` + +Where: + +- `attemptNumber` is the current attempt (1-based) +- `retryBaseInterval` defaults to `1` second +- `factor` is `retryRandomizationFactor` (default `0.5`) +- `random(min, max)` returns a uniformly distributed value in `[min, max]` + +#### Example Delays (with default settings) + +| Attempt | Base Delay | Min Delay (factor=0.5) | Max Delay (factor=0.5) | +| ------- | ---------- | ---------------------- | ---------------------- | +| 1 | 2s | 1.0s | 3.0s | +| 2 | 4s | 2.0s | 6.0s | +| 3 | 8s | 4.0s | 12.0s | +| 4 | 16s | 8.0s | 24.0s | + +### Retry-After Header + +When the server includes a `Retry-After` header in the response, the SDK uses the header value directly as the delay in seconds instead of computing an exponential backoff delay. This applies to any retryable response that includes the header, including: + +- `202 Accepted` with `Retry-After` (long-running operations) +- `429 Too Many Requests` with `Retry-After` +- `5xx` server errors with `Retry-After` + +The header value is parsed as a floating-point number representing seconds. + +### Network Exception Handling + +Network-level failures (connection refused, DNS resolution errors, timeouts, TLS errors) are represented internally as responses with status `0`. These exceptions are tracked by a **separate counter** (`maxRetriesOnException`, default `2`) from the regular HTTP error retry counter (`maxAttempts`). + +This means: + +- Network exception retries are tracked independently from HTTP error retries, each with their own counter and backoff progression. +- A request can fail up to `maxRetriesOnException` times due to network exceptions, but each exception retry also increments the overall attempt counter, so the total number of retries across both exception and HTTP error types is bounded by `maxAttempts`. + +### Customizing Retry Parameters + +You can customize all retry parameters by initializing `BoxRetryStrategy` with the desired values and passing it to `NetworkSession`: + +```ts const auth = new BoxDeveloperTokenAuth({ token: 'DEVELOPER_TOKEN_GOES_HERE' }); const networkSession = new NetworkSession({ - retryStrategy: new BoxRetryStrategy({ maxAttempts: 6 }), + retryStrategy: new BoxRetryStrategy({ + maxAttempts: 3, + retryBaseInterval: 2, + retryRandomizationFactor: 0.3, + maxRetriesOnException: 1, + }), }); const client = new BoxClient({ auth, networkSession }); ``` -## Custom retry strategy +### Custom Retry Strategy -You can also implement your own retry strategy by subclassing `RetryStrategy` and overriding `shouldRetry` and `retryAfter` methods. -This example shows how to set custom strategy that retries on 5xx status codes and waits 1 second between retries. +You can implement your own retry strategy by implementing the `RetryStrategy` interface and overriding the `shouldRetry` and `retryAfter` methods: ```ts -export class CustomRetryStrategy implements RetryStrategy { +class CustomRetryStrategy implements RetryStrategy { async shouldRetry( fetchOptions: FetchOptions, fetchResponse: FetchResponse, attemptNumber: number, ): Promise { - return false; + return fetchResponse.status >= 500 && attemptNumber < 3; } retryAfter( From 30e498952d2a31e80ebfb44f5d6e4672290a0423 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 18 Feb 2026 08:50:20 -0800 Subject: [PATCH 4/4] feat: Add customizable timeouts for SDKs (box/box-codegen#924) --- .codegen.json | 2 +- docs/client.md | 15 +++++ docs/configuration.md | 22 +++++++ src/client.ts | 13 +++++ src/networking/boxNetworkClient.ts | 93 ++++++++++++++++++++++++++---- src/networking/network.ts | 33 +++++++++++ src/networking/timeoutConfig.ts | 3 + src/test/client.test.ts | 17 ++++++ 8 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 src/networking/timeoutConfig.ts diff --git a/.codegen.json b/.codegen.json index f8eaf4e6..a18e284b 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "482939a", "specHash": "77eac4b", "version": "10.4.0" } +{ "engineHash": "bc04b80", "specHash": "77eac4b", "version": "10.4.0" } diff --git a/docs/client.md b/docs/client.md index d14b5ed0..3ac082db 100644 --- a/docs/client.md +++ b/docs/client.md @@ -17,6 +17,7 @@ divided across resource managers. - [Custom Base URLs](#custom-base-urls) - [Custom Agent Options](#custom-agent-options) - [Interceptors](#interceptors) +- [Use Timeouts for API calls](#use-timeouts-for-api-calls) - [Use Proxy for API calls](#use-proxy-for-api-calls) @@ -181,6 +182,20 @@ const clientWithInterceptor: BoxClient = client.withInterceptors([ ]); ``` +# Use Timeouts for API calls + +In order to configure timeout for API calls, call `client.withTimeouts(config)` to create a new client with timeout settings, leaving the original client unmodified. + +`timeoutMs` is in milliseconds and is applied to each request attempt. + +```js +const newClient = client.withTimeouts({ + timeoutMs: 30000, +}); +``` + +If `timeoutMs` is not provided or is less than or equal to `0`, no SDK timeout is applied. + # 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. diff --git a/docs/configuration.md b/docs/configuration.md index a5bf1086..b6bba368 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) @@ -174,3 +175,24 @@ const networkSession = new NetworkSession({ }); const client = new BoxClient({ auth, networkSession }); ``` + +## Timeouts + +You can configure request timeout using `timeoutConfig` on `NetworkSession`. +`timeoutMs` is in milliseconds and applies to each HTTP request attempt. + +```js +const auth = new BoxDeveloperTokenAuth({ token: 'DEVELOPER_TOKEN_GOES_HERE' }); +const networkSession = new NetworkSession({ + timeoutConfig: { timeoutMs: 30000 }, +}); +const client = new BoxClient({ auth, networkSession }); +``` + +How timeout handling works: + +- The SDK applies timeout only when `timeoutMs` is provided and greater than `0`. +- To disable SDK timeout handling, set `timeoutMs` to `0` (or a negative value), or omit `timeoutMs`. +- On timeout, the request is aborted and treated as a network error (`Connection timeout after ms`); if retries are exhausted, the SDK throws `BoxSdkError`. +- Timeout failures are handled as request exceptions, then retry behavior is controlled by the configured retry strategy. +- Timeout applies to a single HTTP request attempt to the Box API (not the total time across all retries). diff --git a/src/client.ts b/src/client.ts index fb93bf84..38719f20 100644 --- a/src/client.ts +++ b/src/client.ts @@ -90,6 +90,7 @@ import { BoxSdkError } from './box/errors'; import { FetchOptions } from './networking/fetchOptions'; import { FetchResponse } from './networking/fetchResponse'; import { BaseUrls } from './networking/baseUrls'; +import { TimeoutConfig } from './networking/timeoutConfig'; import { ProxyConfig } from './networking/proxyConfig'; import { AgentOptions } from './internal/utils'; import { Interceptor } from './networking/interceptors'; @@ -279,6 +280,7 @@ export class BoxClient { | 'withExtraHeaders' | 'withCustomBaseUrls' | 'withProxy' + | 'withTimeouts' | 'withCustomAgentOptions' | 'withInterceptors' > & @@ -739,6 +741,17 @@ export class BoxClient { networkSession: this.networkSession.withProxy(config), }); } + /** + * Create a new client with custom timeouts that will be used for every API call + * @param {TimeoutConfig} config Timeout configuration. + * @returns {BoxClient} + */ + withTimeouts(config: TimeoutConfig): BoxClient { + return new BoxClient({ + auth: this.auth, + networkSession: this.networkSession.withTimeoutConfig(config), + }); + } /** * Create a new client with a custom set of agent options that will be used for every API call * @param {AgentOptions} agentOptions Custom set of agent options that will be used for every API call diff --git a/src/networking/boxNetworkClient.ts b/src/networking/boxNetworkClient.ts index d0ecd063..2a9251c2 100644 --- a/src/networking/boxNetworkClient.ts +++ b/src/networking/boxNetworkClient.ts @@ -40,6 +40,55 @@ export const shouldIncludeBoxUaHeader = (options: FetchOptions) => { ); }; +function createAbortSignalWithTimeout( + baseSignal: RequestInit['signal'], + timeoutMs: number, +): { + signal: AbortSignal; + clearTimeout: () => void; + didTimeout: () => boolean; +} { + const controller = new AbortController(); + const upstream = baseSignal as unknown as AbortSignal | undefined; + let timedOut = false; + + const abortFromUpstream = () => { + try { + (controller as any).abort((upstream as any)?.reason); + } catch { + controller.abort(); + } + }; + + if (upstream) { + if (upstream.aborted) { + abortFromUpstream(); + } else { + upstream.addEventListener('abort', abortFromUpstream, { once: true }); + } + } + + const timeoutId = setTimeout(() => { + timedOut = true; + controller.abort(); + }, timeoutMs); + + // Node.js timers keep the event loop alive. If the only pending work is this + // watchdog timeout, we don't want it to prevent process exit (e.g. short CLI + // runs, tests, scripts). `unref()` detaches the timer from the event loop. + // It’s a no-op in environments where `unref` isn’t available. + (timeoutId as any)?.unref?.(); + + return { + signal: controller.signal, + clearTimeout: () => { + clearTimeout(timeoutId); + if (upstream) upstream.removeEventListener('abort', abortFromUpstream); + }, + didTimeout: () => timedOut, + }; +} + type FetchOptionsExtended = FetchOptions & { attemptNumber?: number; numberOfRetriesOnException?: number; @@ -193,19 +242,33 @@ export class BoxNetworkClient implements NetworkClient { : void 0, }); + const timeoutConfig = fetchOptions.networkSession?.timeoutConfig; + const timeoutMs = timeoutConfig?.timeoutMs; + + const requestTimeout = + timeoutMs != null && timeoutMs > 0 + ? createAbortSignalWithTimeout(requestInit.signal, timeoutMs) + : undefined; + const requestInitWithTimeout: RequestInit = requestTimeout + ? { + ...requestInit, + signal: requestTimeout.signal as unknown as RequestInit['signal'], + } + : requestInit; + try { - const response = await nodeFetch( - ''.concat( - fetchOptions.url, - Object.keys(params).length === 0 || fetchOptions.url.endsWith('?') - ? '' - : '?', - new URLSearchParams(params).toString(), - ), - { ...requestInit, redirect: isBrowser() ? 'follow' : 'manual' }, + const requestUrl = ''.concat( + fetchOptions.url, + Object.keys(params).length === 0 || fetchOptions.url.endsWith('?') + ? '' + : '?', + new URLSearchParams(params).toString(), ); + const response = await nodeFetch(requestUrl, { + ...requestInitWithTimeout, + redirect: isBrowser() ? 'follow' : 'manual', + }); - const contentType = response.headers.get('content-type') ?? ''; const ignoreResponseBody = fetchOptions.followRedirects === false; let data: SerializedData | undefined; @@ -244,8 +307,14 @@ export class BoxNetworkClient implements NetworkClient { } catch (error) { isExceptionCase = true; numberOfRetriesOnException++; - caughtError = error instanceof Error ? error : new Error(String(error)); + if (requestTimeout?.didTimeout()) { + caughtError = new Error(`Connection timeout after ${timeoutMs}ms`); + } else { + caughtError = error instanceof Error ? error : new Error(String(error)); + } fetchResponse = fetchResponse ?? { status: 0, headers: {} }; + } finally { + requestTimeout?.clearTimeout(); } const attemptForRetry = isExceptionCase ? numberOfRetriesOnException @@ -325,7 +394,7 @@ export class BoxNetworkClient implements NetworkClient { : []; if (fetchResponse.status === 0) { throw new BoxSdkError({ - message: `Unexpected Error occurred`, + message: caughtError?.message || `Unexpected Error occurred`, timestamp: `${Date.now()}`, error: caughtError, }); diff --git a/src/networking/network.ts b/src/networking/network.ts index bae14e3a..c346040b 100644 --- a/src/networking/network.ts +++ b/src/networking/network.ts @@ -5,6 +5,7 @@ import { Agent } from '../internal/utils'; import { AgentOptions } from '../internal/utils'; import { createAgent } from '../internal/utils'; import { ProxyConfig } from './proxyConfig'; +import { TimeoutConfig } from './timeoutConfig'; import { BoxNetworkClient } from './boxNetworkClient'; import { NetworkClient } from './networkClient'; import { RetryStrategy } from './retries'; @@ -22,6 +23,7 @@ export class NetworkSession { readonly networkClient: NetworkClient = new BoxNetworkClient({}); readonly retryStrategy: RetryStrategy = new BoxRetryStrategy({}); readonly dataSanitizer: DataSanitizer = new DataSanitizer({}); + readonly timeoutConfig?: TimeoutConfig; constructor( fields: Omit< NetworkSession, @@ -40,6 +42,7 @@ export class NetworkSession { | 'withNetworkClient' | 'withRetryStrategy' | 'withDataSanitizer' + | 'withTimeoutConfig' > & Partial< Pick< @@ -81,6 +84,9 @@ export class NetworkSession { if (fields.dataSanitizer !== undefined) { this.dataSanitizer = fields.dataSanitizer; } + if (fields.timeoutConfig !== undefined) { + this.timeoutConfig = fields.timeoutConfig; + } } /** * Generate a fresh network session by duplicating the existing configuration and network parameters, while also including additional headers to be attached to every API call. @@ -122,6 +128,7 @@ export class NetworkSession { networkClient: this.networkClient, retryStrategy: this.retryStrategy, dataSanitizer: this.dataSanitizer, + timeoutConfig: this.timeoutConfig, }); } /** @@ -140,6 +147,7 @@ export class NetworkSession { networkClient: this.networkClient, retryStrategy: this.retryStrategy, dataSanitizer: this.dataSanitizer, + timeoutConfig: this.timeoutConfig, }); } /** @@ -158,6 +166,7 @@ export class NetworkSession { networkClient: this.networkClient, retryStrategy: this.retryStrategy, dataSanitizer: this.dataSanitizer, + timeoutConfig: this.timeoutConfig, }); } /** @@ -176,6 +185,7 @@ export class NetworkSession { networkClient: this.networkClient, retryStrategy: this.retryStrategy, dataSanitizer: this.dataSanitizer, + timeoutConfig: this.timeoutConfig, }); } /** @@ -194,6 +204,7 @@ export class NetworkSession { networkClient: networkClient, retryStrategy: this.retryStrategy, dataSanitizer: this.dataSanitizer, + timeoutConfig: this.timeoutConfig, }); } /** @@ -212,6 +223,7 @@ export class NetworkSession { networkClient: this.networkClient, retryStrategy: retryStrategy, dataSanitizer: this.dataSanitizer, + timeoutConfig: this.timeoutConfig, }); } /** @@ -231,6 +243,26 @@ export class NetworkSession { networkClient: this.networkClient, retryStrategy: this.retryStrategy, dataSanitizer: dataSanitizer, + timeoutConfig: this.timeoutConfig, + }); + } + /** + * Generate a fresh network session by duplicating the existing configuration and network parameters, while also applying timeout config + * @param {TimeoutConfig} timeoutConfig + * @returns {NetworkSession} + */ + withTimeoutConfig(timeoutConfig: TimeoutConfig): NetworkSession { + return new NetworkSession({ + additionalHeaders: this.additionalHeaders, + baseUrls: this.baseUrls, + interceptors: this.interceptors, + agent: this.agent, + agentOptions: this.agentOptions, + proxyConfig: this.proxyConfig, + networkClient: this.networkClient, + retryStrategy: this.retryStrategy, + dataSanitizer: this.dataSanitizer, + timeoutConfig: timeoutConfig, }); } } @@ -246,4 +278,5 @@ export interface NetworkSessionInput { readonly networkClient?: NetworkClient; readonly retryStrategy?: RetryStrategy; readonly dataSanitizer?: DataSanitizer; + readonly timeoutConfig?: TimeoutConfig; } diff --git a/src/networking/timeoutConfig.ts b/src/networking/timeoutConfig.ts new file mode 100644 index 00000000..af71bf3e --- /dev/null +++ b/src/networking/timeoutConfig.ts @@ -0,0 +1,3 @@ +export interface TimeoutConfig { + readonly timeoutMs?: number; +} diff --git a/src/test/client.test.ts b/src/test/client.test.ts index 7a64c295..e0d7e0e7 100644 --- a/src/test/client.test.ts +++ b/src/test/client.test.ts @@ -37,6 +37,7 @@ import { FileFull } from '@/schemas/fileFull'; import { ResponseFormat } from '@/networking/fetchOptions'; import { UserFull } from '@/schemas/userFull'; import { CreateUserRequestBody } from '@/managers/users'; +import { TimeoutConfig } from '@/networking/timeoutConfig'; import { getUuid } from '@/internal/utils'; import { generateByteStream } from '@/internal/utils'; import { bufferEquals } from '@/internal/utils'; @@ -368,6 +369,22 @@ test('testWithCustomBaseUrls', async function testWithCustomBaseUrls(): Promise< await customBaseClient.users.getUserMe(); }).rejects.toThrow(); }); +test('testWithTimeoutWhenTimeoutOccurs', async function testWithTimeoutWhenTimeoutOccurs(): Promise { + const timeoutMs: number = 1; + const clientWithTimeout: BoxClient = client.withTimeouts({ + timeoutMs: timeoutMs, + } satisfies TimeoutConfig); + await expect(async () => { + await clientWithTimeout.users.getUserMe(); + }).rejects.toThrow(); +}); +test('testWithTimeoutWhenTimeoutDoesNotOccur', async function testWithTimeoutWhenTimeoutDoesNotOccur(): Promise { + const timeoutMs: number = 10000; + const clientWithTimeout: BoxClient = client.withTimeouts({ + timeoutMs: timeoutMs, + } satisfies TimeoutConfig); + await clientWithTimeout.users.getUserMe(); +}); test('testWithInterceptors', async function testWithInterceptors(): Promise { const user: UserFull = await client.users.getUserMe(); if (!(user.role == void 0)) {