From 835a16060dbc7da2c72fb2199ac29f15cf474018 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 08:13:36 +0000 Subject: [PATCH] feat: default requests to a 30 second timeout Requests had no timeout, so a hung connection blocked the caller indefinitely. Axios defaults `timeout` to 0, which means no timeout at all, and the SDK never set it. Set the Axios `timeout` to 30 seconds, matching the API's own request timeout, and add a `timeout` option so callers can raise, lower, or disable it. Passing `timeout` through `axiosOptions` still wins, keeping the existing escape hatch authoritative. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XMgDauUA2R9u2THCHmMgv1 --- README.md | 18 +++++++++++++ src/lib/client.ts | 4 +++ src/lib/parse-options.ts | 3 ++- test/seam/connect/timeout.test.ts | 45 +++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 test/seam/connect/timeout.test.ts diff --git a/README.md b/README.md index baa962e6..fa164a81 100644 --- a/README.md +++ b/README.md @@ -427,6 +427,7 @@ the constructor takes some advanced options that affect behavior. const seam = new SeamHttp({ apiKey: 'your-api-key', endpoint: 'https://example.com', + timeout: 30000, axiosOptions: {}, axiosRetryOptions: {}, }) @@ -438,6 +439,7 @@ these options may be passed in as the last argument. ```ts const seam = SeamHttp.fromApiKey('some-api-key', { endpoint: 'https://example.com', + timeout: 30000, axiosOptions: {}, axiosRetryOptions: {}, }) @@ -451,6 +453,22 @@ This option corresponds to the Axios `baseURL` setting. Either pass the `endpoint` option, or set the `SEAM_ENDPOINT` environment variable. +#### Setting the request timeout + +Requests time out after 30 seconds by default. +Pass the `timeout` option, in milliseconds, to override this: + +```ts +const seam = new SeamHttp({ + apiKey: 'your-api-key', + timeout: 60000, +}) +``` + +Set `timeout` to `0` to disable the timeout entirely. +A request that times out rejects with an Axios `ECONNABORTED` error, +and is retried according to the retry options. + #### Configuring the Axios Client The Axios client and retry behavior may be configured with custom initiation options diff --git a/src/lib/client.ts b/src/lib/client.ts index 87588358..8bcb3b26 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -6,7 +6,10 @@ import { errorInterceptor } from './error-interceptor.js' export type Client = AxiosInstance +export const defaultTimeout = 30_000 + export interface ClientOptions { + timeout?: number axiosOptions?: AxiosRequestConfig axiosRetryOptions?: AxiosRetryConfig } @@ -17,6 +20,7 @@ export const createClient = (options: ClientOptions): AxiosInstance => { const client = axios.create({ paramsSerializer: serializeUrlSearchParams, adapter: 'fetch', + timeout: options.timeout ?? defaultTimeout, ...options.axiosOptions, }) diff --git a/src/lib/parse-options.ts b/src/lib/parse-options.ts index 0b08a2a9..ec74e31a 100644 --- a/src/lib/parse-options.ts +++ b/src/lib/parse-options.ts @@ -2,7 +2,7 @@ import { seamApiLtsVersion } from 'lib/lts-version.js' import version from 'lib/version.js' import { getAuthHeaders } from './auth.js' -import type { Client, ClientOptions } from './client.js' +import { type Client, type ClientOptions, defaultTimeout } from './client.js' import { isSeamHttpOptionsWithClient, isSeamHttpOptionsWithClientSessionToken, @@ -40,6 +40,7 @@ export const parseOptions = ( return { ...options, + timeout: options.timeout ?? defaultTimeout, axiosOptions: { baseURL: options.endpoint ?? getEndpointFromEnv() ?? defaultEndpoint, withCredentials: isSeamHttpOptionsWithClientSessionToken(options), diff --git a/test/seam/connect/timeout.test.ts b/test/seam/connect/timeout.test.ts new file mode 100644 index 00000000..cf808863 --- /dev/null +++ b/test/seam/connect/timeout.test.ts @@ -0,0 +1,45 @@ +import test from 'ava' +import { AxiosError } from 'axios' +import { getTestServer } from 'fixtures/seam/connect/api.js' + +import { SeamHttp } from '@seamapi/http/connect' + +test('SeamHttp: times out requests after 30s by default', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint }) + t.is(seam.client.defaults.timeout, 30_000) +}) + +test('SeamHttp: timeout option overrides the default', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { + endpoint, + timeout: 60_000, + }) + t.is(seam.client.defaults.timeout, 60_000) +}) + +test('SeamHttp: axiosOptions timeout overrides the timeout option', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { + endpoint, + timeout: 60_000, + axiosOptions: { timeout: 1_000 }, + }) + t.is(seam.client.defaults.timeout, 1_000) +}) + +test('SeamHttp: timeout option aborts slow requests', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { + endpoint, + timeout: 1, + axiosRetryOptions: { retries: 0 }, + }) + + const err = await t.throwsAsync(async () => await seam.devices.list(), { + instanceOf: AxiosError, + }) + + t.is(err?.code, AxiosError.ECONNABORTED) +})