mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 17:44:11 +00:00
🏁 fix: Ignore Finalized SSE Transport Errors (#14114)
* fix: Ignore Finalized SSE Transport Errors * test: Rename Resumable SSE Suite
This commit is contained in:
parent
e8d76542b6
commit
f11b7379a1
2 changed files with 71 additions and 3 deletions
|
|
@ -236,7 +236,7 @@ const advanceRetryTimer = async (ms: number) => {
|
|||
await flushMicrotasks();
|
||||
};
|
||||
|
||||
describe('useResumableSSE - 404 error path', () => {
|
||||
describe('useResumableSSE', () => {
|
||||
beforeEach(() => {
|
||||
mockSSEInstances.length = 0;
|
||||
localStorage.clear();
|
||||
|
|
@ -1612,6 +1612,59 @@ describe('useResumableSSE - 404 error path', () => {
|
|||
unmount();
|
||||
});
|
||||
|
||||
it('ignores responseCode === 0 after FINAL instead of reconnecting a completed stream', async () => {
|
||||
jest.useFakeTimers();
|
||||
const submission = buildSubmission();
|
||||
const chatHelpers = buildChatHelpers();
|
||||
|
||||
const { unmount } = renderHook(() => useResumableSSE(submission, chatHelpers));
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const sse = getLastSSE();
|
||||
const sseCount = mockSSEInstances.length;
|
||||
const finalPayload = {
|
||||
final: true,
|
||||
conversation: { conversationId: CONV_ID },
|
||||
requestMessage: {
|
||||
messageId: 'msg-1',
|
||||
conversationId: CONV_ID,
|
||||
text: 'Hello',
|
||||
isCreatedByUser: true,
|
||||
},
|
||||
responseMessage: {
|
||||
messageId: 'resp-1',
|
||||
parentMessageId: 'msg-1',
|
||||
conversationId: CONV_ID,
|
||||
text: 'Done',
|
||||
isCreatedByUser: false,
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
sse._emit('message', {
|
||||
data: JSON.stringify(finalPayload),
|
||||
});
|
||||
});
|
||||
|
||||
expect(sse.close).toHaveBeenCalled();
|
||||
mockSetIsSubmitting.mockClear();
|
||||
mockSetShowStopButton.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
sse._emit('error', { responseCode: 0 });
|
||||
});
|
||||
await advanceRetryTimer(1000);
|
||||
|
||||
expect(mockSSEInstances).toHaveLength(sseCount);
|
||||
expect(mockSetIsSubmitting).not.toHaveBeenCalledWith(true);
|
||||
expect(mockSetShowStopButton).not.toHaveBeenCalledWith(true);
|
||||
expect(mockErrorHandler).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('parses and surfaces server-sent error events (no responseCode, JSON data)', async () => {
|
||||
const submission = buildSubmission();
|
||||
const chatHelpers = buildChatHelpers();
|
||||
|
|
|
|||
|
|
@ -573,6 +573,7 @@ export default function useResumableSSE(
|
|||
(currentStreamId: string, currentSubmission: TSubmission, isResume = false) => {
|
||||
let { userMessage } = currentSubmission;
|
||||
let textIndex: number | null = null;
|
||||
let finalReceived = false;
|
||||
const preCreatedStepEvents: Array<Parameters<typeof stepHandler>[0]> = [];
|
||||
const replayPreCreatedStepEvents = () => {
|
||||
if (preCreatedStepEvents.length === 0) {
|
||||
|
|
@ -663,6 +664,12 @@ export default function useResumableSSE(
|
|||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.final != null) {
|
||||
finalReceived = true;
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
reconnectAttemptRef.current = 0;
|
||||
logger.log('ResumableSSE', 'Received FINAL event', {
|
||||
aborted: data.aborted,
|
||||
conversationId: data.conversation?.conversationId,
|
||||
|
|
@ -996,11 +1003,19 @@ export default function useResumableSSE(
|
|||
* Order matters: check responseCode first since HTTP errors may also include data
|
||||
*/
|
||||
sse.addEventListener('error', async (e: MessageEvent) => {
|
||||
(startupConfig?.balance?.enabled ?? false) && balanceQuery.refetch();
|
||||
|
||||
/* @ts-ignore - sse.js types don't expose responseCode */
|
||||
const responseCode = e.responseCode;
|
||||
|
||||
if (finalReceived) {
|
||||
logger.log('ResumableSSE', 'Ignoring error after FINAL event', {
|
||||
responseCode,
|
||||
hasData: !!e.data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
(startupConfig?.balance?.enabled ?? false) && balanceQuery.refetch();
|
||||
|
||||
// 404 → job completed & was cleaned up; messages are persisted in DB.
|
||||
// Invalidate cache once so react-query refetches instead of showing an error.
|
||||
if (responseCode === 404) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue