mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 02:10:15 +01:00
🛡️ fix: Improve Error Handling and Null Safety in SSE Event Processing (#10751)
* 🔧 fix: Handle null content parts in message processing - Added checks to filter out null content parts in various message handling functions, ensuring robustness against undefined values. - Updated the `extractMessageContent`, `useContentHandler`, `useEventHandlers`, and `useStepHandler` hooks to prevent errors caused by null parts. - Enhanced the `getAllContentText` utility to only include valid content types, improving overall message integrity. * 🔧 fix: Enhance error handling in event and SSE handlers - Wrapped critical sections in try-catch blocks within `useEventHandlers` and `useSSE` hooks to improve error management and prevent application crashes. - Added console error logging for better debugging and tracking of issues during message processing and conversation aborting. - Ensured that UI states like `setIsSubmitting` and `setShowStopButton` are correctly updated in case of errors, maintaining a consistent user experience. * 🔧 fix: Filter out null and empty content in message export - Enhanced the `useExportConversation` hook to filter out null content parts and empty strings during message processing, ensuring only valid content is included in the export. - This change improves the integrity of exported conversations by preventing unnecessary empty entries in the output.
This commit is contained in:
parent
6c0aad423f
commit
026890cd27
7 changed files with 201 additions and 156 deletions
|
|
@ -87,12 +87,14 @@ const createErrorMessage = ({
|
|||
let isValidContentPart = false;
|
||||
if (latestContent.length > 0) {
|
||||
const latestContentPart = latestContent[latestContent.length - 1];
|
||||
const latestPartValue = latestContentPart?.[latestContentPart.type ?? ''];
|
||||
isValidContentPart =
|
||||
latestContentPart.type !== ContentTypes.TEXT ||
|
||||
(latestContentPart.type === ContentTypes.TEXT && typeof latestPartValue === 'string')
|
||||
? true
|
||||
: latestPartValue?.value !== '';
|
||||
if (latestContentPart != null) {
|
||||
const latestPartValue = latestContentPart[latestContentPart.type ?? ''];
|
||||
isValidContentPart =
|
||||
latestContentPart.type !== ContentTypes.TEXT ||
|
||||
(latestContentPart.type === ContentTypes.TEXT && typeof latestPartValue === 'string')
|
||||
? true
|
||||
: latestPartValue?.value !== '';
|
||||
}
|
||||
}
|
||||
if (
|
||||
latestMessage?.conversationId &&
|
||||
|
|
@ -455,141 +457,145 @@ export default function useEventHandlers({
|
|||
isTemporary = false,
|
||||
} = submission;
|
||||
|
||||
if (responseMessage?.attachments && responseMessage.attachments.length > 0) {
|
||||
// Process each attachment through the attachmentHandler
|
||||
responseMessage.attachments.forEach((attachment) => {
|
||||
const attachmentData = {
|
||||
...attachment,
|
||||
messageId: responseMessage.messageId,
|
||||
};
|
||||
try {
|
||||
if (responseMessage?.attachments && responseMessage.attachments.length > 0) {
|
||||
// Process each attachment through the attachmentHandler
|
||||
responseMessage.attachments.forEach((attachment) => {
|
||||
const attachmentData = {
|
||||
...attachment,
|
||||
messageId: responseMessage.messageId,
|
||||
};
|
||||
|
||||
attachmentHandler({
|
||||
data: attachmentData,
|
||||
submission: submission as EventSubmission,
|
||||
attachmentHandler({
|
||||
data: attachmentData,
|
||||
submission: submission as EventSubmission,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setShowStopButton(false);
|
||||
setCompleted((prev) => new Set(prev.add(submission.initialResponse.messageId)));
|
||||
setCompleted((prev) => new Set(prev.add(submission.initialResponse.messageId)));
|
||||
|
||||
const currentMessages = getMessages();
|
||||
/* Early return if messages are empty; i.e., the user navigated away */
|
||||
if (!currentMessages || currentMessages.length === 0) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
const currentMessages = getMessages();
|
||||
/* Early return if messages are empty; i.e., the user navigated away */
|
||||
if (!currentMessages || currentMessages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* a11y announcements */
|
||||
announcePolite({ message: 'end', isStatus: true });
|
||||
announcePolite({ message: getAllContentText(responseMessage) });
|
||||
/* a11y announcements */
|
||||
announcePolite({ message: 'end', isStatus: true });
|
||||
announcePolite({ message: getAllContentText(responseMessage) });
|
||||
|
||||
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
|
||||
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
|
||||
|
||||
const setFinalMessages = (id: string | null, _messages: TMessage[]) => {
|
||||
setMessages(_messages);
|
||||
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, id], _messages);
|
||||
};
|
||||
const setFinalMessages = (id: string | null, _messages: TMessage[]) => {
|
||||
setMessages(_messages);
|
||||
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, id], _messages);
|
||||
};
|
||||
|
||||
const hasNoResponse =
|
||||
responseMessage?.content?.[0]?.['text']?.value ===
|
||||
submission.initialResponse?.content?.[0]?.['text']?.value ||
|
||||
!!responseMessage?.content?.[0]?.['tool_call']?.auth;
|
||||
const hasNoResponse =
|
||||
responseMessage?.content?.[0]?.['text']?.value ===
|
||||
submission.initialResponse?.content?.[0]?.['text']?.value ||
|
||||
!!responseMessage?.content?.[0]?.['tool_call']?.auth;
|
||||
|
||||
/** Handle edge case where stream is cancelled before any response, which creates a blank page */
|
||||
if (!conversation.conversationId && hasNoResponse) {
|
||||
const currentConvoId =
|
||||
(submissionConvo.conversationId ?? conversation.conversationId) || Constants.NEW_CONVO;
|
||||
if (isNewConvo && submissionConvo.conversationId) {
|
||||
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId);
|
||||
}
|
||||
|
||||
const isNewChat =
|
||||
location.pathname === `/c/${Constants.NEW_CONVO}` &&
|
||||
currentConvoId === Constants.NEW_CONVO;
|
||||
|
||||
setFinalMessages(currentConvoId, isNewChat ? [] : [...messages]);
|
||||
setDraft({ id: currentConvoId, value: requestMessage?.text });
|
||||
if (isNewChat) {
|
||||
navigate(`/c/${Constants.NEW_CONVO}`, { replace: true, state: { focusChat: true } });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Update messages; if assistants endpoint, client doesn't receive responseMessage */
|
||||
let finalMessages: TMessage[] = [];
|
||||
if (runMessages) {
|
||||
finalMessages = [...runMessages];
|
||||
} else if (isRegenerate && responseMessage) {
|
||||
finalMessages = [...messages, responseMessage];
|
||||
} else if (requestMessage != null && responseMessage != null) {
|
||||
finalMessages = [...messages, requestMessage, responseMessage];
|
||||
}
|
||||
if (finalMessages.length > 0) {
|
||||
setFinalMessages(conversation.conversationId, finalMessages);
|
||||
} else if (
|
||||
isAssistantsEndpoint(submissionConvo.endpoint) &&
|
||||
(!submissionConvo.conversationId ||
|
||||
submissionConvo.conversationId === Constants.NEW_CONVO)
|
||||
) {
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[...currentMessages],
|
||||
);
|
||||
}
|
||||
|
||||
/** Handle edge case where stream is cancelled before any response, which creates a blank page */
|
||||
if (!conversation.conversationId && hasNoResponse) {
|
||||
const currentConvoId =
|
||||
(submissionConvo.conversationId ?? conversation.conversationId) || Constants.NEW_CONVO;
|
||||
if (isNewConvo && submissionConvo.conversationId) {
|
||||
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId);
|
||||
}
|
||||
|
||||
const isNewChat =
|
||||
location.pathname === `/c/${Constants.NEW_CONVO}` &&
|
||||
currentConvoId === Constants.NEW_CONVO;
|
||||
|
||||
setFinalMessages(currentConvoId, isNewChat ? [] : [...messages]);
|
||||
setDraft({ id: currentConvoId, value: requestMessage?.text });
|
||||
setIsSubmitting(false);
|
||||
if (isNewChat) {
|
||||
navigate(`/c/${Constants.NEW_CONVO}`, { replace: true, state: { focusChat: true } });
|
||||
/* Refresh title */
|
||||
if (
|
||||
genTitle &&
|
||||
isNewConvo &&
|
||||
!isTemporary &&
|
||||
requestMessage &&
|
||||
requestMessage.parentMessageId === Constants.NO_PARENT
|
||||
) {
|
||||
setTimeout(() => {
|
||||
genTitle.mutate({ conversationId: conversation.conversationId as string });
|
||||
}, 2500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Update messages; if assistants endpoint, client doesn't receive responseMessage */
|
||||
let finalMessages: TMessage[] = [];
|
||||
if (runMessages) {
|
||||
finalMessages = [...runMessages];
|
||||
} else if (isRegenerate && responseMessage) {
|
||||
finalMessages = [...messages, responseMessage];
|
||||
} else if (requestMessage != null && responseMessage != null) {
|
||||
finalMessages = [...messages, requestMessage, responseMessage];
|
||||
}
|
||||
if (finalMessages.length > 0) {
|
||||
setFinalMessages(conversation.conversationId, finalMessages);
|
||||
} else if (
|
||||
isAssistantsEndpoint(submissionConvo.endpoint) &&
|
||||
(!submissionConvo.conversationId || submissionConvo.conversationId === Constants.NEW_CONVO)
|
||||
) {
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[...currentMessages],
|
||||
);
|
||||
}
|
||||
|
||||
if (isNewConvo && submissionConvo.conversationId) {
|
||||
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId);
|
||||
}
|
||||
|
||||
/* Refresh title */
|
||||
if (
|
||||
genTitle &&
|
||||
isNewConvo &&
|
||||
!isTemporary &&
|
||||
requestMessage &&
|
||||
requestMessage.parentMessageId === Constants.NO_PARENT
|
||||
) {
|
||||
setTimeout(() => {
|
||||
genTitle.mutate({ conversationId: conversation.conversationId as string });
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
if (setConversation && isAddedRequest !== true) {
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...(conversation as TConversation),
|
||||
};
|
||||
if (prevState?.model != null && prevState.model !== submissionConvo.model) {
|
||||
update.model = prevState.model;
|
||||
}
|
||||
const cachedConvo = queryClient.getQueryData<TConversation>([
|
||||
QueryKeys.conversation,
|
||||
conversation.conversationId,
|
||||
]);
|
||||
if (!cachedConvo) {
|
||||
queryClient.setQueryData([QueryKeys.conversation, conversation.conversationId], update);
|
||||
}
|
||||
return update;
|
||||
});
|
||||
|
||||
if (conversation.conversationId && submission.ephemeralAgent) {
|
||||
applyAgentTemplate({
|
||||
targetId: conversation.conversationId,
|
||||
sourceId: submissionConvo.conversationId,
|
||||
ephemeralAgent: submission.ephemeralAgent,
|
||||
specName: submission.conversation?.spec,
|
||||
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
|
||||
if (setConversation && isAddedRequest !== true) {
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...(conversation as TConversation),
|
||||
};
|
||||
if (prevState?.model != null && prevState.model !== submissionConvo.model) {
|
||||
update.model = prevState.model;
|
||||
}
|
||||
const cachedConvo = queryClient.getQueryData<TConversation>([
|
||||
QueryKeys.conversation,
|
||||
conversation.conversationId,
|
||||
]);
|
||||
if (!cachedConvo) {
|
||||
queryClient.setQueryData(
|
||||
[QueryKeys.conversation, conversation.conversationId],
|
||||
update,
|
||||
);
|
||||
}
|
||||
return update;
|
||||
});
|
||||
}
|
||||
|
||||
if (location.pathname === `/c/${Constants.NEW_CONVO}`) {
|
||||
navigate(`/c/${conversation.conversationId}`, { replace: true });
|
||||
if (conversation.conversationId && submission.ephemeralAgent) {
|
||||
applyAgentTemplate({
|
||||
targetId: conversation.conversationId,
|
||||
sourceId: submissionConvo.conversationId,
|
||||
ephemeralAgent: submission.ephemeralAgent,
|
||||
specName: submission.conversation?.spec,
|
||||
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
|
||||
});
|
||||
}
|
||||
|
||||
if (location.pathname === `/c/${Constants.NEW_CONVO}`) {
|
||||
navigate(`/c/${conversation.conversationId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setShowStopButton(false);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
[
|
||||
navigate,
|
||||
|
|
@ -722,26 +728,37 @@ export default function useEventHandlers({
|
|||
messages[messages.length - 2] != null
|
||||
) {
|
||||
let requestMessage = messages[messages.length - 2];
|
||||
const responseMessage = messages[messages.length - 1];
|
||||
if (requestMessage.messageId !== responseMessage.parentMessageId) {
|
||||
const _responseMessage = messages[messages.length - 1];
|
||||
if (requestMessage.messageId !== _responseMessage.parentMessageId) {
|
||||
// the request message is the parent of response, which we search for backwards
|
||||
for (let i = messages.length - 3; i >= 0; i--) {
|
||||
if (messages[i].messageId === responseMessage.parentMessageId) {
|
||||
if (messages[i].messageId === _responseMessage.parentMessageId) {
|
||||
requestMessage = messages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finalHandler(
|
||||
{
|
||||
conversation: {
|
||||
conversationId,
|
||||
/** Sanitize content array to remove undefined parts from interrupted streaming */
|
||||
const responseMessage = {
|
||||
..._responseMessage,
|
||||
content: _responseMessage.content?.filter((part) => part != null),
|
||||
};
|
||||
try {
|
||||
finalHandler(
|
||||
{
|
||||
conversation: {
|
||||
conversationId,
|
||||
},
|
||||
requestMessage,
|
||||
responseMessage,
|
||||
},
|
||||
requestMessage,
|
||||
responseMessage,
|
||||
},
|
||||
submission,
|
||||
);
|
||||
submission,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in finalHandler during abort:', error);
|
||||
setShowStopButton(false);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
return;
|
||||
} else if (!isAssistantsEndpoint(endpoint)) {
|
||||
const convoId = conversationId || `_${v4()}`;
|
||||
|
|
@ -809,13 +826,14 @@ export default function useEventHandlers({
|
|||
}
|
||||
},
|
||||
[
|
||||
finalHandler,
|
||||
newConversation,
|
||||
setIsSubmitting,
|
||||
token,
|
||||
cancelHandler,
|
||||
getMessages,
|
||||
setMessages,
|
||||
finalHandler,
|
||||
cancelHandler,
|
||||
newConversation,
|
||||
setIsSubmitting,
|
||||
setShowStopButton,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue