mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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 { AppEvents } from '../constants/events';
|
||||
import { ChatState } from '../types/chatState';
|
||||
import { toastError } from '../toasts';
|
||||
|
||||
import {
|
||||
getSession,
|
||||
@@ -206,7 +207,8 @@ async function streamFromResponse(
|
||||
initialMessages: Message[],
|
||||
dispatch: React.Dispatch<StreamAction>,
|
||||
onFinish: (error?: string) => void,
|
||||
sessionId: string
|
||||
sessionId: string,
|
||||
signal?: AbortController['signal']
|
||||
): Promise<void> {
|
||||
let currentMessages = initialMessages;
|
||||
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();
|
||||
onFinish();
|
||||
if (!signal?.aborted) {
|
||||
toastError({
|
||||
title: 'Connection lost',
|
||||
msg: 'The response may be incomplete. You can try sending your message again.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
flushBatchedUpdates();
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
@@ -593,9 +609,10 @@ export function useChatStream({
|
||||
},
|
||||
throwOnError: true,
|
||||
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) {
|
||||
// AbortError is expected when user stops streaming
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
@@ -634,9 +651,10 @@ export function useChatStream({
|
||||
},
|
||||
throwOnError: true,
|
||||
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) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
// Silently handle abort
|
||||
@@ -773,9 +791,10 @@ export function useChatStream({
|
||||
},
|
||||
throwOnError: true,
|
||||
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) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Idle });
|
||||
|
||||
Reference in New Issue
Block a user