diff --git a/api/server/routes/agents/index.js b/api/server/routes/agents/index.js index eb42046bed..bbb39f5d2c 100644 --- a/api/server/routes/agents/index.js +++ b/api/server/routes/agents/index.js @@ -143,7 +143,8 @@ router.get('/chat/stream/:streamId', async (req, res) => { } if (!result) { - return res.status(404).json({ error: 'Failed to subscribe to stream' }); + onError('Failed to subscribe to stream'); + return; } req.on('close', () => { diff --git a/client/src/hooks/SSE/__tests__/useResumableSSE.spec.ts b/client/src/hooks/SSE/__tests__/useResumableSSE.spec.ts index 1717d27c22..269f38c865 100644 --- a/client/src/hooks/SSE/__tests__/useResumableSSE.spec.ts +++ b/client/src/hooks/SSE/__tests__/useResumableSSE.spec.ts @@ -281,4 +281,51 @@ describe('useResumableSSE - 404 error path', () => { unmount(); }, ); + + it('treats responseCode === 0 with raw SSE buffer data as transport failure (reconnect path)', async () => { + const submission = buildSubmission(); + const chatHelpers = buildChatHelpers(); + + const { unmount } = renderHook(() => useResumableSSE(submission, chatHelpers)); + + await act(async () => { + await Promise.resolve(); + }); + + const sse = getLastSSE(); + + await act(async () => { + sse._emit('error', { + responseCode: 0, + data: 'event: message\ndata: {"created":true,"message":{}}\n\n', + }); + }); + + expect(mockErrorHandler).not.toHaveBeenCalled(); + unmount(); + }); + + it('parses and surfaces server-sent error events (no responseCode, JSON data)', async () => { + const submission = buildSubmission(); + const chatHelpers = buildChatHelpers(); + + const { unmount } = renderHook(() => useResumableSSE(submission, chatHelpers)); + + await act(async () => { + await Promise.resolve(); + }); + + const sse = getLastSSE(); + + const errorPayload = JSON.stringify({ + error: JSON.stringify({ type: 'token_limit' }), + }); + + await act(async () => { + sse._emit('error', { data: errorPayload }); + }); + + expect(mockErrorHandler).toHaveBeenCalledTimes(1); + unmount(); + }); }); diff --git a/client/src/hooks/SSE/useResumableSSE.ts b/client/src/hooks/SSE/useResumableSSE.ts index 39dc610dae..f0ee12ecc1 100644 --- a/client/src/hooks/SSE/useResumableSSE.ts +++ b/client/src/hooks/SSE/useResumableSSE.ts @@ -391,8 +391,10 @@ export default function useResumableSSE( * Server-sent error event (event: error with data) - no responseCode. * These are known errors (ErrorTypes, ViolationTypes) that should be displayed to user. * Only check e.data if there's no HTTP responseCode, since HTTP errors may also have body data. + * Note: responseCode === 0 means transport failure (connection dropped) - treat as network error, + * not a server-sent error payload. Use `== null` to only match undefined/null (no HTTP status). */ - if (!responseCode && e.data) { + if (responseCode == null && e.data) { console.log('[ResumableSSE] Server-sent error event received:', e.data); sse.close(); removeActiveJob(currentStreamId);