🛠️ fix: UI/UX for Known Server-sent Errors (#11343)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

This commit is contained in:
Danny Avila 2026-01-13 14:13:06 -05:00 committed by GitHub
parent a95fea19bb
commit 9d5e80d7a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -4,11 +4,13 @@ import { SSE } from 'sse.js';
import { useSetRecoilState } from 'recoil';
import { useQueryClient } from '@tanstack/react-query';
import {
apiBaseUrl,
request,
Constants,
QueryKeys,
ErrorTypes,
apiBaseUrl,
createPayload,
ViolationTypes,
LocalStorageKeys,
removeNullishValues,
} from 'librechat-data-provider';
@ -334,8 +336,11 @@ export default function useResumableSSE(
});
/**
* Error event - fired on actual network failures (non-200, connection lost, etc.)
* This should trigger reconnection with exponential backoff, except for 404 errors.
* Error event handler - handles BOTH:
* 1. HTTP-level errors (responseCode present) - 404, 401, network failures
* 2. Server-sent error events (event: error with data) - known errors like ViolationTypes/ErrorTypes
*
* Order matters: check responseCode first since HTTP errors may also include data
*/
sse.addEventListener('error', async (e: MessageEvent) => {
(startupConfig?.balance?.enabled ?? false) && balanceQuery.refetch();
@ -347,7 +352,6 @@ export default function useResumableSSE(
if (responseCode === 404) {
console.log('[ResumableSSE] Stream not found (404) - job completed or expired');
sse.close();
// Optimistically remove from active jobs since job is gone
removeActiveJob(currentStreamId);
setIsSubmitting(false);
setShowStopButton(false);
@ -356,8 +360,6 @@ export default function useResumableSSE(
return;
}
console.log('[ResumableSSE] Stream error (network failure) - will attempt reconnect');
// Check for 401 and try to refresh token (same pattern as useSSE)
if (responseCode === 401) {
try {
@ -366,7 +368,6 @@ export default function useResumableSSE(
if (!newToken) {
throw new Error('Token refresh failed.');
}
// Update headers on same SSE instance and retry (like useSSE)
sse.headers = {
Authorization: `Bearer ${newToken}`,
};
@ -378,6 +379,64 @@ 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.
*/
if (!responseCode && e.data) {
console.log('[ResumableSSE] Server-sent error event received:', e.data);
sse.close();
removeActiveJob(currentStreamId);
try {
const errorData = JSON.parse(e.data);
const errorString = errorData.error ?? errorData.message ?? JSON.stringify(errorData);
// Check if it's a known error type (ViolationTypes or ErrorTypes)
let isKnownError = false;
try {
const parsed =
typeof errorString === 'string' ? JSON.parse(errorString) : errorString;
const errorType = parsed?.type ?? parsed?.code;
if (errorType) {
const violationValues = Object.values(ViolationTypes) as string[];
const errorTypeValues = Object.values(ErrorTypes) as string[];
isKnownError =
violationValues.includes(errorType) || errorTypeValues.includes(errorType);
}
} catch {
// Not JSON or parsing failed - treat as generic error
}
console.log('[ResumableSSE] Error type check:', { isKnownError, errorString });
// Display the error to user via errorHandler
errorHandler({
data: { text: errorString } as unknown as Parameters<typeof errorHandler>[0]['data'],
submission: currentSubmission as EventSubmission,
});
} catch (parseError) {
console.error('[ResumableSSE] Failed to parse server error:', parseError);
errorHandler({
data: { text: e.data } as unknown as Parameters<typeof errorHandler>[0]['data'],
submission: currentSubmission as EventSubmission,
});
}
setIsSubmitting(false);
setShowStopButton(false);
setStreamId(null);
reconnectAttemptRef.current = 0;
return;
}
// Network failure or unknown HTTP error - attempt reconnection with backoff
console.log('[ResumableSSE] Stream error (network failure) - will attempt reconnect', {
responseCode,
hasData: !!e.data,
});
if (reconnectAttemptRef.current < MAX_RETRIES) {
// Increment counter BEFORE close() so abort handler knows we're reconnecting
reconnectAttemptRef.current++;