{"record":{"id":"d17ada297557fc49","repo":"datawhalechina/hello-agents","slug":"error-d17ada","errorCode":null,"errorMessage":"请求超时，请稍后重试或检查后端服务","messagePattern":"请求超时，请稍后重试或检查后端服务","errorType":"exception","errorClass":"ApiError","httpStatus":408,"severity":"error","filePath":"Co-creation-projects/aatanxiao12-beep-YingQian/frontend/src/api/client.ts","lineNumber":62,"sourceCode":"        payload &&\n        typeof payload === 'object' &&\n        'detail' in payload &&\n        typeof (payload as { detail: unknown }).detail === 'string'\n          ? (payload as { detail: string }).detail\n          : payload &&\n              typeof payload === 'object' &&\n              'message' in payload &&\n              typeof (payload as { message: unknown }).message === 'string'\n            ? (payload as { message: string }).message\n            : `请求失败（${res.status}）`\n      throw new ApiError(detail, res.status)\n    }\n\n    return payload as T\n  } catch (err) {\n    if (err instanceof ApiError) throw err\n    if (err instanceof DOMException && err.name === 'AbortError') {\n      throw new ApiError('请求超时，请稍后重试或检查后端服务', 408)\n    }\n    throw new ApiError(err instanceof Error ? err.message : '网络异常', 0)\n  } finally {\n    window.clearTimeout(timer)\n  }\n}\n","sourceCodeStart":44,"sourceCodeEnd":69,"githubUrl":"https://github.com/datawhalechina/hello-agents/blob/606a07d341a47be773fab7f4b71177f53f96b2c3/Co-creation-projects/aatanxiao12-beep-YingQian/frontend/src/api/client.ts#L44-L69","documentation":"Timeout branch of the typed ApiClient: a per-request AbortController timer fires, fetch rejects with a DOMException named 'AbortError', and the catch maps it to ApiError('请求超时...', 408). Seeing this means the request was aborted by the client's own timer, not by the server — the server may still be processing.","triggerScenarios":"Backend endpoint slower than the configured timeout (long LLM/agent calls easily exceed typical 10-30s timers), cold-start latency of serverless backends, or a hung backend connection. Note AbortError with name 'AbortError' only — a user-driven abort would also land here unless filtered.","commonSituations":"AI generation endpoints that take 60s+ behind a 15s client timeout; dev server hibernation; slow mobile networks; accidental shared AbortController aborting many requests at once.","solutions":["Raise the timeout for slow endpoints — make timeout a per-call option instead of a constant","Measure actual backend latency (server logs / browser network tab) and set the timer with headroom","Distinguish user-initiated aborts from timer aborts by tagging the AbortController (e.g. controller.timerAbort = true) so only timeouts map to 408","If the backend is genuinely hanging, fix the backend (add its own timeout / streaming response)"],"exampleFix":"// before\nif (err instanceof DOMException && err.name === 'AbortError') {\n    throw new ApiError('请求超时，请稍后重试或检查后端服务', 408)\n}\n\n// after\nif (err instanceof DOMException && err.name === 'AbortError') {\n    if (signal?.aborted && !timerFired) throw err; // caller-initiated abort, rethrow\n    throw new ApiError('请求超时，请稍后重试或检查后端服务', 408)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await client.post('/slow', body, { timeout: 60000 }); } catch (e) { if (e instanceof ApiError && e.status === 408) { await sleep(2000); return retryOnce(); } throw e; }","preventionTips":["Make timeout per-call configurable for slow AI endpoints","Tag timer-driven aborts so user aborts aren't mislabeled as timeouts","Measure real backend latency and set the timer with headroom","Consider SSE/streaming for long operations to avoid client timeouts entirely"],"tags":["timeout","abort-controller","fetch","typescript"],"backgroundTag":null,"analyzedSha":"606a07d341a47be773fab7f4b71177f53f96b2c3","analyzedAt":"2026-08-14T22:57:27.446Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}