From a932ebe0178d4456f64bf3fc92e98c2c7ee37902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 12 Jun 2026 15:59:38 +0800 Subject: [PATCH] fix(desktop): extend local API timeout (#773) Tested: cd desktop && bun run test -- src/api/client.test.ts Tested: cd desktop && bun run lint Confidence: high Scope-risk: narrow --- desktop/src/api/client.test.ts | 31 +++++++++++++++++++++++++++++++ desktop/src/api/client.ts | 3 ++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/desktop/src/api/client.test.ts b/desktop/src/api/client.test.ts index 4082fb7a..7ee05058 100644 --- a/desktop/src/api/client.test.ts +++ b/desktop/src/api/client.test.ts @@ -10,6 +10,7 @@ import { describe('api diagnostics reporting', () => { afterEach(() => { + vi.useRealTimers() setAuthToken(null) setBaseUrl(getDefaultBaseUrl()) vi.restoreAllMocks() @@ -114,6 +115,36 @@ describe('api diagnostics reporting', () => { expect(fetchMock).toHaveBeenCalledTimes(1) }) + it('defaults local API requests to a 120 second timeout', async () => { + vi.useFakeTimers() + const fetchMock = vi.spyOn(globalThis, 'fetch') + fetchMock.mockImplementation((url: string | URL | Request, init?: RequestInit) => { + if (String(url).endsWith('/api/slow')) { + return new Promise((_, reject) => { + init?.signal?.addEventListener('abort', () => { + reject(new DOMException('The operation was aborted.', 'AbortError')) + }) + }) + } + + return Promise.resolve(new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + })) + }) + + const request = expect(api.get('/api/slow')).rejects.toThrow('Request timed out after 120s') + + await vi.advanceTimersByTimeAsync(120_000) + await request + + expect(fetchMock).toHaveBeenCalledTimes(2) + const [, diagnosticInit] = fetchMock.mock.calls[1]! + const body = JSON.parse(String((diagnosticInit as RequestInit).body)) + expect(body.type).toBe('client_api_request_failed') + expect(body.details.message).toBe('Request timed out after 120s') + }) + it('can report raw client exceptions', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch') fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { diff --git a/desktop/src/api/client.ts b/desktop/src/api/client.ts index e9388786..fcb2367d 100644 --- a/desktop/src/api/client.ts +++ b/desktop/src/api/client.ts @@ -10,6 +10,7 @@ const DEFAULT_BASE_URL = ENV_BASE_URL || 'http://127.0.0.1:3456' let baseUrl = DEFAULT_BASE_URL let authToken: string | null = null const DIAGNOSTICS_PATH = '/api/diagnostics/events' +const DEFAULT_REQUEST_TIMEOUT_MS = 120_000 function getErrorMessage(status: number, body: unknown) { if (body && typeof body === 'object' && 'message' in body && typeof body.message === 'string') { @@ -72,7 +73,7 @@ async function request(method: string, path: string, body?: unknown, options? const headers = buildHeaders() const controller = new AbortController() - const timeoutMs = options?.timeout ?? 30_000 + const timeoutMs = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS const timeout = setTimeout(() => controller.abort(), timeoutMs) try { const res = await fetch(url, {