fix: prevent SSE connection drops from silently truncating responses (#7831)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
|
||||||
import { AppEvents } from '../constants/events';
|
import { AppEvents } from '../constants/events';
|
||||||
import { ChatState } from '../types/chatState';
|
import { ChatState } from '../types/chatState';
|
||||||
|
import { toastError } from '../toasts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSession,
|
getSession,
|
||||||
@@ -206,7 +207,8 @@ async function streamFromResponse(
|
|||||||
initialMessages: Message[],
|
initialMessages: Message[],
|
||||||
dispatch: React.Dispatch<StreamAction>,
|
dispatch: React.Dispatch<StreamAction>,
|
||||||
onFinish: (error?: string) => void,
|
onFinish: (error?: string) => void,
|
||||||
sessionId: string
|
sessionId: string,
|
||||||
|
signal?: AbortController['signal']
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let currentMessages = initialMessages;
|
let currentMessages = initialMessages;
|
||||||
const reduceMotion = prefersReducedMotion();
|
const reduceMotion = prefersReducedMotion();
|
||||||
@@ -309,8 +311,22 @@ async function streamFromResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we reach here, the stream ended without a Finish or Error event.
|
||||||
|
// This happens when the connection drops and retries are exhausted — the
|
||||||
|
// generator exits its loop without yielding a terminal event. We call
|
||||||
|
// onFinish() without an error to keep the conversation visible (passing
|
||||||
|
// an error would trigger a full-page error screen via sessionLoadError),
|
||||||
|
// then show a toast so the user knows the response may be incomplete.
|
||||||
|
// If the signal was aborted, the user intentionally stopped streaming,
|
||||||
|
// so we skip the toast.
|
||||||
flushBatchedUpdates();
|
flushBatchedUpdates();
|
||||||
onFinish();
|
onFinish();
|
||||||
|
if (!signal?.aborted) {
|
||||||
|
toastError({
|
||||||
|
title: 'Connection lost',
|
||||||
|
msg: 'The response may be incomplete. You can try sending your message again.',
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
flushBatchedUpdates();
|
flushBatchedUpdates();
|
||||||
if (error instanceof Error && error.name !== 'AbortError') {
|
if (error instanceof Error && error.name !== 'AbortError') {
|
||||||
@@ -593,9 +609,10 @@ export function useChatStream({
|
|||||||
},
|
},
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
signal: abortControllerRef.current.signal,
|
signal: abortControllerRef.current.signal,
|
||||||
|
sseMaxRetryAttempts: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await streamFromResponse(stream, currentMessages, dispatch, onFinish, sessionId);
|
await streamFromResponse(stream, currentMessages, dispatch, onFinish, sessionId, abortControllerRef.current.signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// AbortError is expected when user stops streaming
|
// AbortError is expected when user stops streaming
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
@@ -634,9 +651,10 @@ export function useChatStream({
|
|||||||
},
|
},
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
signal: abortControllerRef.current.signal,
|
signal: abortControllerRef.current.signal,
|
||||||
|
sseMaxRetryAttempts: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await streamFromResponse(stream, currentMessages, dispatch, onFinish, sessionId);
|
await streamFromResponse(stream, currentMessages, dispatch, onFinish, sessionId, abortControllerRef.current.signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
// Silently handle abort
|
// Silently handle abort
|
||||||
@@ -773,9 +791,10 @@ export function useChatStream({
|
|||||||
},
|
},
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
signal: abortControllerRef.current.signal,
|
signal: abortControllerRef.current.signal,
|
||||||
|
sseMaxRetryAttempts: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await streamFromResponse(stream, messagesForUI, dispatch, onFinish, targetSessionId);
|
await streamFromResponse(stream, messagesForUI, dispatch, onFinish, targetSessionId, abortControllerRef.current.signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Idle });
|
dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Idle });
|
||||||
|
|||||||
Reference in New Issue
Block a user