diff --git a/src/modules/aix/client/aix.client.errors.ts b/src/modules/aix/client/aix.client.errors.ts new file mode 100644 index 000000000..9a4fcafb6 --- /dev/null +++ b/src/modules/aix/client/aix.client.errors.ts @@ -0,0 +1,88 @@ +import { TRPCClientError } from '@trpc/client'; + +import { presentErrorToHumans } from '~/common/util/errorUtils'; + + +// configuration +const AIX_CLIENT_DEV_ASSERTS = process.env.NODE_ENV === 'development'; + + +/** + * Classifies tRPC/network errors from the streaming loop. + * + * Responsibility: Connection, network, and stream errors (NOT particle processing). + * Particle processing errors are caught in ContentReassembler due to async timing. + * + * Returns error classification and user-facing message. Caller applies to reassembler. + * See comment above for-await loop for error handling split rationale. + */ +export function aixClassifyStreamingError(error: any, isUserAbort: boolean, hasFragments: boolean): { + errorType: 'client-aborted' | 'net-disconnected' | 'request-exceeded' | 'response-captive' | 'net-unknown'; + errorMessage: string; +} { + + // User abort or AbortError from elsewhere (e.g. server-side tRPC abort?) + const isErrorAbort = error instanceof Error && (error.name === 'AbortError' || (error.cause instanceof DOMException && error.cause.name === 'AbortError')); + if (isUserAbort || isErrorAbort) { + if (AIX_CLIENT_DEV_ASSERTS && isUserAbort !== isErrorAbort) + console.error(`[DEV] Aix streaming AbortError mismatch (${isUserAbort}, ${isErrorAbort})`, { error: error }); + return { errorType: 'client-aborted', errorMessage: '' }; // errorMessage unused for aborts + } + + // IMPORTANT: NOTE: this code path has also been almost replicated on `ContentReassembler.#processWireBacklog.catch() {...}` + + if (AIX_CLIENT_DEV_ASSERTS) console.error('[DEV] Aix streaming Error:', { error }); + + // Browser-level network connection drops (TypeError, happens below tRPC error wrapping layer) + // Network errors - when the client is disconnected (Vercel 5min timeout, Mobile timeout / disconnect, etc) - they show up as TypeErrors + if (error instanceof TypeError && error.message === 'network error') + return { errorType: 'net-disconnected', errorMessage: 'An unexpected issue occurred: **network error**.' }; + + // tRPC-level protocol errors (wrapped by tRPC client) + // Initial connection failures, HTTP errors, or text responses that blow up tRPC's JSON parser + if (error instanceof TRPCClientError) { + switch (error.cause?.message) { + /** + * The body of the response was "Request Entity Too Large". + * - this caused trpc, in ...stream/jsonl.ts, function createConsumerStream, to throw an error due to parsing the line as JSON + * - "const head = JSON.parse(line);" + * - as the error bubbles up to here, and cannot be handled by the superjson transformer either, which happens after this + */ + case `Unexpected token 'R', "Request En"... is not valid JSON`: + return { errorType: 'request-exceeded', errorMessage: '**Request too large**: Your message or attachments exceed the 4.5MB limit of the Vercel edge network. Tip: use the cleanup button in the right pane to hide messages, remove large attachments or reduce conversation length.' }; + + /** + * This happened many times in the past with captive portals and alike. Jet's just improve the messaging here. + */ + case `Unexpected token '<', "