From 7ea8d2e7dbb15046f109a808bca62e15c67e9e2e Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Wed, 19 Aug 2026 01:16:22 +0200 Subject: [PATCH 1/3] fix(deploy): bound integration status requests Session-Id: 01a01712-3a40-7572-89f4-f903c6f5638b --- packages/deploy/src/integrations-list.test.ts | 44 ++++++++++++++++ packages/deploy/src/integrations-list.ts | 51 +++++++++++++++---- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/packages/deploy/src/integrations-list.test.ts b/packages/deploy/src/integrations-list.test.ts index 79210ac1..177cda0e 100644 --- a/packages/deploy/src/integrations-list.test.ts +++ b/packages/deploy/src/integrations-list.test.ts @@ -140,6 +140,50 @@ test('listIntegrations throws loud endpoint errors while authenticated', async ( ); }); +test('listIntegrations bounds a status endpoint that never settles', async () => { + const startedAt = Date.now(); + const operation = listIntegrations({ + workspaceId: 'ws-1', + token: 'tok', + requestTimeoutMs: 20, + client: { + async fetch(pathname) { + if (pathname === '/api/v1/integrations/catalog') { + return json({ providers: [{ id: 'daytona' }] }); + } + if (pathname === '/api/v1/me/integrations') { + return json({ integrations: [] }); + } + if (pathname === '/api/v1/workspaces/ws-1/integrations') { + return json({ integrations: [] }); + } + if (pathname.endsWith('/status?scope=deployer_user')) { + return json({ provider: 'daytona', status: 'connected' }); + } + return new Promise(() => {}); + } + } + }); + const guard = new Promise((_, reject) => { + setTimeout(() => reject(new Error('test guard: integration request never settled')), 500); + }); + + await assert.rejects( + Promise.race([operation, guard]), + (err) => { + assert.ok(err instanceof IntegrationsListError); + assert.equal(err.status, 408); + assert.equal( + err.endpoint, + '/api/v1/workspaces/ws-1/integrations/daytona/status?scope=workspace' + ); + assert.match(err.message, /timed out after 20ms/); + return true; + } + ); + assert.ok(Date.now() - startedAt < 500); +}); + test('listIntegrations accepts adapter slug as provider filter and suggests it on unknown providers', async () => { const base = { activeWorkspace: null, diff --git a/packages/deploy/src/integrations-list.ts b/packages/deploy/src/integrations-list.ts index b675fab4..3cc050a4 100644 --- a/packages/deploy/src/integrations-list.ts +++ b/packages/deploy/src/integrations-list.ts @@ -52,6 +52,8 @@ export interface ListIntegrationsOptions { resolveWorkspaceToken?: typeof resolveWorkspaceToken; provider?: string; includeTriggers?: boolean; + /** Maximum time for any single Cloud catalog/status request. */ + requestTimeoutMs?: number; } export class IntegrationsListError extends Error { @@ -386,17 +388,44 @@ async function requestJson( pathname: string, init: RequestInit = {} ): Promise { - const response = options.client - ? await options.client.fetch(pathname, init) - : await (options.fetch ?? fetch)(`${cloudUrl}${pathname}`, { - ...init, - headers: { - accept: 'application/json', - 'content-type': 'application/json', - ...(auth.token ? { authorization: `Bearer ${auth.token}` } : {}), - ...(init.headers ?? {}) - } - }); + const timeoutMs = options.requestTimeoutMs ?? 10_000; + const controller = new AbortController(); + const upstreamSignal = init.signal; + const abortFromUpstream = () => controller.abort(upstreamSignal?.reason); + if (upstreamSignal?.aborted) abortFromUpstream(); + else upstreamSignal?.addEventListener('abort', abortFromUpstream, { once: true }); + + let timeout: ReturnType | undefined; + const timeoutError = new IntegrationsListError( + `integration catalog/status request timed out after ${timeoutMs}ms: ${pathname}`, + { status: 408, endpoint: pathname, body: '' } + ); + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout(() => { + controller.abort(timeoutError); + reject(timeoutError); + }, timeoutMs); + }); + + let response: Response; + try { + const request = options.client + ? options.client.fetch(pathname, { ...init, signal: controller.signal }) + : (options.fetch ?? fetch)(`${cloudUrl}${pathname}`, { + ...init, + signal: controller.signal, + headers: { + accept: 'application/json', + 'content-type': 'application/json', + ...(auth.token ? { authorization: `Bearer ${auth.token}` } : {}), + ...(init.headers ?? {}) + } + }); + response = await Promise.race([request, timeoutPromise]); + } finally { + if (timeout) clearTimeout(timeout); + upstreamSignal?.removeEventListener('abort', abortFromUpstream); + } if (!response.ok) { const body = await response.text().catch(() => ''); const excerpt = body.length > 400 ? `${body.slice(0, 400)}...` : body; From 55b2e56e40911b07a4617f5772f1716e4b4e8db7 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Wed, 19 Aug 2026 01:29:35 +0200 Subject: [PATCH 2/3] fix(deploy): keep timeout through response parsing Session-Id: 01a01712-3a40-7572-89f4-f903c6f5638b --- packages/deploy/src/integrations-list.test.ts | 90 +++++++++++++++---- packages/deploy/src/integrations-list.ts | 49 +++++----- 2 files changed, 99 insertions(+), 40 deletions(-) diff --git a/packages/deploy/src/integrations-list.test.ts b/packages/deploy/src/integrations-list.test.ts index 177cda0e..425288cd 100644 --- a/packages/deploy/src/integrations-list.test.ts +++ b/packages/deploy/src/integrations-list.test.ts @@ -147,7 +147,7 @@ test('listIntegrations bounds a status endpoint that never settles', async () => token: 'tok', requestTimeoutMs: 20, client: { - async fetch(pathname) { + async fetch(pathname, init = {}) { if (pathname === '/api/v1/integrations/catalog') { return json({ providers: [{ id: 'daytona' }] }); } @@ -160,30 +160,88 @@ test('listIntegrations bounds a status endpoint that never settles', async () => if (pathname.endsWith('/status?scope=deployer_user')) { return json({ provider: 'daytona', status: 'connected' }); } - return new Promise(() => {}); + return new Promise((_, reject) => { + init.signal?.addEventListener( + 'abort', + () => reject(init.signal?.reason ?? new Error('aborted')), + { once: true } + ); + }); } } }); + let guardTimer: ReturnType | undefined; const guard = new Promise((_, reject) => { - setTimeout(() => reject(new Error('test guard: integration request never settled')), 500); + guardTimer = setTimeout( + () => reject(new Error('test guard: integration request never settled')), + 500 + ); }); - await assert.rejects( - Promise.race([operation, guard]), - (err) => { - assert.ok(err instanceof IntegrationsListError); - assert.equal(err.status, 408); - assert.equal( - err.endpoint, - '/api/v1/workspaces/ws-1/integrations/daytona/status?scope=workspace' - ); - assert.match(err.message, /timed out after 20ms/); - return true; - } - ); + try { + await assert.rejects( + Promise.race([operation, guard]), + (err) => { + assert.ok(err instanceof IntegrationsListError); + assert.equal(err.status, 408); + assert.equal( + err.endpoint, + '/api/v1/workspaces/ws-1/integrations/daytona/status?scope=workspace' + ); + assert.match(err.message, /timed out after 20ms/); + return true; + } + ); + } finally { + if (guardTimer) clearTimeout(guardTimer); + } assert.ok(Date.now() - startedAt < 500); }); +test('listIntegrations bounds response body consumption and preserves the typed timeout', async () => { + let guardTimer: ReturnType | undefined; + const operation = listIntegrations({ + workspaceId: 'ws-1', + token: 'tok', + requestTimeoutMs: 20, + client: { + async fetch(pathname) { + if (pathname === '/api/v1/integrations/catalog') { + return json({ providers: [{ id: 'daytona' }] }); + } + if (pathname === '/api/v1/me/integrations') { + return new Response(new ReadableStream({ start() {} }), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + return json({ integrations: [] }); + } + } + }); + const guard = new Promise((_, reject) => { + guardTimer = setTimeout( + () => reject(new Error('test guard: response body never settled')), + 500 + ); + }); + + try { + await assert.rejects( + Promise.race([operation, guard]), + (err) => { + assert.ok(err instanceof IntegrationsListError); + assert.equal(err.status, 408); + assert.equal(err.endpoint, '/api/v1/me/integrations'); + assert.match(err.message, /timed out after 20ms/); + return true; + } + ); + } finally { + if (guardTimer) clearTimeout(guardTimer); + } +}); + test('listIntegrations accepts adapter slug as provider filter and suggests it on unknown providers', async () => { const base = { activeWorkspace: null, diff --git a/packages/deploy/src/integrations-list.ts b/packages/deploy/src/integrations-list.ts index 3cc050a4..5dc97f90 100644 --- a/packages/deploy/src/integrations-list.ts +++ b/packages/deploy/src/integrations-list.ts @@ -402,39 +402,40 @@ async function requestJson( ); const timeoutPromise = new Promise((_, reject) => { timeout = setTimeout(() => { - controller.abort(timeoutError); reject(timeoutError); + controller.abort(timeoutError); }, timeoutMs); }); - let response: Response; try { - const request = options.client - ? options.client.fetch(pathname, { ...init, signal: controller.signal }) - : (options.fetch ?? fetch)(`${cloudUrl}${pathname}`, { - ...init, - signal: controller.signal, - headers: { - accept: 'application/json', - 'content-type': 'application/json', - ...(auth.token ? { authorization: `Bearer ${auth.token}` } : {}), - ...(init.headers ?? {}) - } - }); - response = await Promise.race([request, timeoutPromise]); + const request = (async (): Promise => { + const response = options.client + ? await options.client.fetch(pathname, { ...init, signal: controller.signal }) + : await (options.fetch ?? fetch)(`${cloudUrl}${pathname}`, { + ...init, + signal: controller.signal, + headers: { + accept: 'application/json', + 'content-type': 'application/json', + ...(auth.token ? { authorization: `Bearer ${auth.token}` } : {}), + ...(init.headers ?? {}) + } + }); + if (!response.ok) { + const body = await response.text().catch(() => ''); + const excerpt = body.length > 400 ? `${body.slice(0, 400)}...` : body; + throw new IntegrationsListError( + `integration catalog/status request failed: ${response.status} ${pathname}${excerpt ? ` ${excerpt}` : ''}`, + { status: response.status, endpoint: pathname, body: excerpt } + ); + } + return await response.json(); + })(); + return await Promise.race([request, timeoutPromise]); } finally { if (timeout) clearTimeout(timeout); upstreamSignal?.removeEventListener('abort', abortFromUpstream); } - if (!response.ok) { - const body = await response.text().catch(() => ''); - const excerpt = body.length > 400 ? `${body.slice(0, 400)}...` : body; - throw new IntegrationsListError( - `integration catalog/status request failed: ${response.status} ${pathname}${excerpt ? ` ${excerpt}` : ''}`, - { status: response.status, endpoint: pathname, body: excerpt } - ); - } - return await response.json(); } function adapterSlugForCloudProvider(provider: string): string { From cdb2fa476f4bec382bd56217e3c0d5a060f9aff6 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Wed, 19 Aug 2026 01:41:12 +0200 Subject: [PATCH 3/3] test(deploy): preserve signal-ignoring timeout coverage Session-Id: 01a01712-3a40-7572-89f4-f903c6f5638b --- packages/deploy/src/integrations-list.test.ts | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/packages/deploy/src/integrations-list.test.ts b/packages/deploy/src/integrations-list.test.ts index 425288cd..56b59bc5 100644 --- a/packages/deploy/src/integrations-list.test.ts +++ b/packages/deploy/src/integrations-list.test.ts @@ -147,7 +147,7 @@ test('listIntegrations bounds a status endpoint that never settles', async () => token: 'tok', requestTimeoutMs: 20, client: { - async fetch(pathname, init = {}) { + async fetch(pathname) { if (pathname === '/api/v1/integrations/catalog') { return json({ providers: [{ id: 'daytona' }] }); } @@ -160,13 +160,7 @@ test('listIntegrations bounds a status endpoint that never settles', async () => if (pathname.endsWith('/status?scope=deployer_user')) { return json({ provider: 'daytona', status: 'connected' }); } - return new Promise((_, reject) => { - init.signal?.addEventListener( - 'abort', - () => reject(init.signal?.reason ?? new Error('aborted')), - { once: true } - ); - }); + return new Promise(() => {}); } } }); @@ -198,6 +192,47 @@ test('listIntegrations bounds a status endpoint that never settles', async () => assert.ok(Date.now() - startedAt < 500); }); +test('listIntegrations preserves its typed timeout when an abort-aware client rejects', async () => { + let guardTimer: ReturnType | undefined; + const operation = listIntegrations({ + workspaceId: 'ws-1', + token: 'tok', + requestTimeoutMs: 20, + client: { + async fetch(_pathname, init = {}) { + return new Promise((_, reject) => { + init.signal?.addEventListener( + 'abort', + () => reject(new Error('client abort rejection')), + { once: true } + ); + }); + } + } + }); + const guard = new Promise((_, reject) => { + guardTimer = setTimeout( + () => reject(new Error('test guard: abort-aware request never settled')), + 500 + ); + }); + + try { + await assert.rejects( + Promise.race([operation, guard]), + (err) => { + assert.ok(err instanceof IntegrationsListError); + assert.equal(err.status, 408); + assert.equal(err.endpoint, '/api/v1/integrations/catalog'); + assert.match(err.message, /timed out after 20ms/); + return true; + } + ); + } finally { + if (guardTimer) clearTimeout(guardTimer); + } +}); + test('listIntegrations bounds response body consumption and preserves the typed timeout', async () => { let guardTimer: ReturnType | undefined; const operation = listIntegrations({