fix: use i18nKey for error display and add timeout enforcement#12164
fix: use i18nKey for error display and add timeout enforcement#12164kangfenmao merged 12 commits intomainfrom
Conversation
EurFelux
left a comment
There was a problem hiding this comment.
Code Review Summary
Thanks for addressing the timeout and streaming-only documentation issues related to #12016. The intent is clear and the upstream limitation context is appreciated. However, I've identified several issues that should be addressed before merging.
🔴 Critical Issues
-
Abort error message change breaks i18n-based rendering — The
baseCallbacks.tschange from'pause_placeholder'toformatErrorMessageWithPrefix(...)breaks the downstream rendering inErrorBlock.tsx(see inline comment). -
Timeout signal triggers
isAbortError→ user sees "Paused" instead of timeout error —AbortSignal.timeout()produces aTimeoutError(DOMException withname === 'TimeoutError'), but when combined viaAbortSignal.any(), the resulting abort reason propagates. The currentisAbortError()checks forDOMExceptionwithname === 'AbortError'and messages like'signal is aborted without reason'. A timeout fromAbortSignal.timeout()may match some of these checks depending on the runtime, causing timeouts to be silently swallowed as "Paused" rather than surfaced as timeout errors.
🟡 Moderate Issues
-
_headersvariable naming — Destructuringheadersas_headerssuggests it's unused, but it is used. This violates the naming convention where_prefix means "intentionally unused". -
Double timeout — Timeout is now applied in two places:
parameterBuilder.tscreatesAbortSignal.timeout(timeout)and combines it with the caller's signal, but the caller inmessageThunk.tsalso passestimeout: defaultTimeout. It's unclear which layer is responsible for timeout enforcement. This creates a risk of conflicting or redundant timeout behavior. -
TODO/FIXME in production code — The comments
// TODO: make it configurableand// FIXME: temporary solution. It may be too long.inmessageThunk.tsshould be tracked as issues rather than left as code comments, especially in a PR meant for merging.
🟢 Minor Issues
-
JSDoc comments are informational-only — The added JSDoc comments in
index_new.tsandApiService.tsdocument existing behavior accurately but don't change any logic. Consider whether inline comments (closer to thestreamTextcall) would be more discoverable. -
Type change from
Record<string, string>toRecord<string, string | undefined>— This headers type relaxation is fine but should be noted as it widens the accepted type at the interface level.
Summary
The core ideas (longer timeout, better documentation) are sound, but the abort error message change introduces a regression in how paused messages are rendered, and the timeout signal interaction with isAbortError needs careful testing. Recommend addressing the critical items before merging.
src/renderer/src/store/thunk/__tests__/streamCallback.integration.test.ts
Show resolved
Hide resolved
Use formatErrorMessageWithPrefix to properly localize the pause placeholder error message with i18next
Use defaultTimeout from shared config instead of hardcoded values Combine multiple abort signals for better request handling
- Use dedicated `i18nKey` field on SerializedError instead of overwriting `message` with i18n keys, preserving original error info for debugging - Rename i18n key `pause_placeholder` → `stream_paused` and extract as constant `ERROR_I18N_KEY_STREAM_PAUSED` - Add `isTimeoutError()` to distinguish timeout from user-initiated abort - Fix `_headers` naming to `inputHeaders` in parameterBuilder - Update i18n resources and tests Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add `ERROR_I18N_KEY_REQUEST_TIMEOUT` constant and `request_timeout` i18n key with translations for all supported languages - Handle timeout errors in baseCallbacks and ActionUtils using the same i18nKey pattern as stream_paused - Add isTimeoutError mock to ActionUtils tests Co-Authored-By: Claude Opus 4.5 <[email protected]>
…nt timeout - Rename `defaultTimeout` → `DEFAULT_TIMEOUT` (SCREAMING_SNAKE_CASE) across all import sites for consistency with other exported constants. - Remove redundant `timeout: defaultTimeout` from messageThunk.ts since parameterBuilder.ts already falls back to DEFAULT_TIMEOUT internally. - Add comment in parameterBuilder.ts clarifying the timeout control point. Co-Authored-By: Claude Opus 4.5 <[email protected]>
Code Review: Error Handling ImprovementsOverviewThis PR improves error handling in the streaming AI SDK path by:
Detailed Review✅ Strengths
|
EurFelux
left a comment
There was a problem hiding this comment.
Review Summary
Two minor issues identified:
-
Missing test coverage: The isTimeoutError utility should have unit tests for the error.cause wrapping case.
-
Translation issue: The French translation for stream_paused contains Cyrillic text which appears to be an error.
Otherwise the PR looks good!
Replace Cyrillic text "Прервано" with proper French "Interrompu" Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add comprehensive tests for isTimeoutError including: - Direct DOM TimeoutError detection - Wrapped timeout errors via error.cause - Negative cases for AbortError and generic errors Co-Authored-By: Claude Opus 4.5 <[email protected]>
EurFelux
left a comment
There was a problem hiding this comment.
Review Summary
This PR is well-crafted and addresses important error handling improvements.
Highlights
-
i18nKey Pattern: Using
i18nKeyproperty instead of overwritingmessagepreserves original error info for debugging while enabling proper localization. This follows the existing pattern used in ErrorHandlerMiddleware.ts. -
Timeout Error Detection: The
isTimeoutError()utility correctly handles both directDOMExceptiontimeout errors and wrapped errors viaerror.cause. The explicit exclusion inisAbortError()ensures timeouts are not misclassified as user aborts. -
Single Timeout Control: Establishing
parameterBuilder.tsas the single timeout control point removes dead code frommessageThunk.tsand makes timeout behavior predictable. -
Test Coverage: Unit tests for
isTimeoutErrorcover the main cases including wrapped errors.
Minor Notes
- The French translation was already fixed from Cyrillic to proper French ("Interrompu")
- The AbortSignal.any() approach is clean and handles both timeout and user abort correctly
- JSDoc documentation is comprehensive
Recommendation: APPROVE - ready to merge.
@kangfenmao @0xfullex Please take a look at it. The error message still shows a pause_placeholder key in production version. |

What this PR does
Before this PR:
messagewith raw i18n keys (e.g.'pause_placeholder'), losing the original error info for debugging.DOMExceptionwithname === 'TimeoutError') were not distinguished from user-initiated aborts, causing timeouts to display as "Paused" in the UI.AbortSignal.timeout()enforcement existed in the modern AI SDK path (parameterBuilder.ts);messageThunk.tspassedtimeout: 30000inrequestOptionsbut it was never used to create an abort signal — effectively dead code.defaultTimeoutused camelCase, inconsistent with other exported constants.After this PR:
i18nKeyfield onSerializedError(instead of overwritingmessage) for translated error display, preserving original error info for the detail modal.pause_placeholder→stream_pausedand extract as a named constant (ERROR_I18N_KEY_STREAM_PAUSED).isTimeoutError()utility to distinguishDOMException TimeoutErrorfrom user-initiatedAbortError, checking botherroranderror.cause.isAbortError()to explicitly exclude timeout errors.AbortSignal.timeout(DEFAULT_TIMEOUT)(10 min) inparameterBuilder.ts, combined with the external signal viaAbortSignal.any().timeout: 30000frommessageThunk.ts, establishingparameterBuilder.tsas the single timeout control point.defaultTimeout→DEFAULT_TIMEOUT(SCREAMING_SNAKE_CASE) across all import sites.request_timeouti18n key (ERROR_I18N_KEY_REQUEST_TIMEOUT) for all supported languages.SerializedErrordocumenting known dynamic properties (i18nKey,providerId).streamText-only path inindex_new.tsandApiService.ts.Fixes # (not applicable; relates to #12016)
Why we need it and why it was done in this way
This PR attempts to address #12016, but per the upstream limitation described in vercel/ai#9655, the root cause is not solvable purely from the client side. The changes here document the streaming-only behavior, improve error handling, and add proper timeout enforcement via
AbortSignal.timeout().The following tradeoffs were made:
i18nKeyfield (already supported byErrorBlock.tsxandErrorHandlerMiddleware.ts) instead of overwritingmessage— this preserves original error info for the detail modal while enabling proper i18n display.parameterBuilder.tsas the single timeout control point rather than allowing callers to pass their own timeout values, since no caller currently provides a custom timeout.The following alternatives were considered:
generateText), but that is not used in this runtime path and does not address the upstream limitation.messagewith i18n keys (original approach) — rejected because it loses debugging context and creates an implicit contract between producers and consumers.i18nKeyas an explicit optional property onSerializedError— rejected due to TypeScript's index signature constraint (string | undefinedis incompatible with[key: string]: Serializable); documented via JSDoc instead.Links to places where the discussion took place: vercel/ai#9655
Breaking changes
None.
Special notes for your reviewer
i18nKeypattern onSerializedErroris already used byErrorHandlerMiddleware.tsfor auth/quota errors. This PR extends the same pattern to abort and timeout errors.isTimeoutError()checks botherroranderror.causeforDOMExceptionwithname === 'TimeoutError'to handle cases where the AI SDK wraps the original timeout error.timeout: 30000removed frommessageThunk.tswas effectively dead code —parameterBuilder.tspreviously only passed throughrequestOptions.signalbut never usedrequestOptions.timeoutfor abort signal creation.Checklist
Release note