mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 06:52:47 +00:00
🩹 fix(SSE): Treat responseCode === 0 as Transport Failure, Not Server Error (#12834)
* fix(sse): treat responseCode===0 as transport failure, not server error
When a long-running model response (e.g. gpt-5.4 with web_search:true)
takes longer than the browser's idle connection timeout, the SSE transport
drops and sse.js fires an error event with responseCode=0 and e.data set
to the raw response buffer (non-JSON SSE text).
The previous guard `!responseCode` is truthy for both 0 (transport drop)
and undefined (genuine server-sent error event), so the client incorrectly
entered the server-error branch, tried to JSON.parse raw SSE text, logged
"Failed to parse server error", and showed the user a red error banner --
even though the backend continued processing and delivered the final answer
seconds later.
Fix 1 (client): change guard from `!responseCode` to `responseCode == null`
so that only undefined/null (no HTTP status at all) triggers the server-error
parse path. responseCode===0 now correctly falls through to the reconnect path.
Fix 2 (backend): after res.flushHeaders() the response is already committed
as SSE. The fallback branch that wrote res.status(404).json() was an HTTP/SSE
protocol violation. Replace with an SSE-conformant event:error frame + res.end().
* fix(sse): use onError helper on subscribe failure + add regression tests
Replace silent res.end() with onError('Failed to subscribe to stream')
so the client receives a parseable SSE error event instead of a stream
that closes with no signal. The previous res.end() left the UI stuck
in "submitting" state because no error/abort/final event ever fired.
Also adds two missing test cases for the responseCode guard change:
- responseCode === 0 with raw SSE buffer data must NOT call errorHandler
(transport failure should reconnect, not display garbage)
- responseCode == null with JSON error data MUST call errorHandler
(server-sent error events should still surface to the user)
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
85894c11c7
commit
61b9b1daa7
3 changed files with 52 additions and 2 deletions
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue