From 11c166db9c3a125c6e08f04787878d1dbd880721 Mon Sep 17 00:00:00 2001 From: Bufo Date: Tue, 7 Jul 2026 10:29:24 +0200 Subject: [PATCH] fix: enforce configured request timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit timeoutMs was resolved and stored (default 30s) but never applied — the GraphQLClient was constructed without any signal, so requests could hang indefinitely. Wrap the configured fetch so every request carries an AbortSignal.timeout(timeoutMs), honoring a caller-supplied signal when present. Covers both gqlRequest and the resource SDK path, since both share the client's fetch. --- packages/core/src/client.test.ts | 32 ++++++++++++++++++++++++++++++++ packages/core/src/client.ts | 5 ++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/core/src/client.test.ts b/packages/core/src/client.test.ts index b8b89ab..ecb223c 100644 --- a/packages/core/src/client.test.ts +++ b/packages/core/src/client.test.ts @@ -66,4 +66,36 @@ describe('AmbossClient', () => { ); assert.equal(new Probe({ serviceApiKey: 'amb_live_test' }).check(), 'amb_live_test'); }); + + it('applies timeoutMs as an abort signal on requests', async () => { + let receivedSignal: AbortSignal | undefined; + const fetchImpl: typeof fetch = async (_input, init) => { + receivedSignal = init?.signal ?? undefined; + return new Response(JSON.stringify({ data: { ok: true } }), { + headers: { 'content-type': 'application/json' }, + }); + }; + class Probe extends AmbossClient { + run(): Promise { + return this.gqlRequest('{ ok }', undefined, 'Probe'); + } + } + await new Probe({ apiKey: 'sk_test', fetch: fetchImpl, timeoutMs: 5000 }).run(); + assert.ok(receivedSignal instanceof AbortSignal); + }); + + it('rejects a request that exceeds timeoutMs', async () => { + const hangingFetch: typeof fetch = (_input, init) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener('abort', () => reject(new Error('aborted'))); + }); + class Probe extends AmbossClient { + run(): Promise { + return this.gqlRequest('{ ok }', undefined, 'Probe'); + } + } + await assert.rejects(() => + new Probe({ apiKey: 'sk_test', fetch: hangingFetch, timeoutMs: 10 }).run(), + ); + }); }); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 30507c6..5d405c1 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -12,8 +12,11 @@ export class AmbossClient { constructor(config: ClientConfig = {}) { this.config = AmbossClient.resolveConfig(config); + const { fetch: fetchImpl, timeoutMs } = this.config; this.graphqlClient = new GraphQLClient(this.config.baseUrl, { - fetch: this.config.fetch, + // Apply timeoutMs as a per-request abort signal, honoring a caller-supplied signal if present. + fetch: (input, init) => + fetchImpl(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(timeoutMs) }), headers: this.buildHeaders(), }); }