mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🛠️ fix: Custom Endpoint issues, Improve SSE Response Handling (#1510)
* fix(custom): prevent presets using removed custom endpoints from causing frontend errors * refactor(abortMiddleware): send 204 status when abortController is not found/active, set expected header `application/json` when not set * fix(useSSE): general improvements: - Add endpointType to fetch URL in useSSE hook - use EndpointURLs enum - handle 204 response by setting `data` to initiated response - add better error handling UX, make clear when there is an explicit error
This commit is contained in:
parent
84892b5b98
commit
bebfffb2d9
5 changed files with 62 additions and 14 deletions
|
|
@ -14,7 +14,7 @@ async function abortMessage(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!abortControllers.has(abortKey) && !res.headersSent) {
|
if (!abortControllers.has(abortKey) && !res.headersSent) {
|
||||||
return res.status(404).send({ message: 'Request not found' });
|
return res.status(204).send({ message: 'Request not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { abortController } = abortControllers.get(abortKey);
|
const { abortController } = abortControllers.get(abortKey);
|
||||||
|
|
@ -26,6 +26,8 @@ async function abortMessage(req, res) {
|
||||||
return sendMessage(res, finalEvent);
|
return sendMessage(res, finalEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
res.send(JSON.stringify(finalEvent));
|
res.send(JSON.stringify(finalEvent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||||
const iconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
const iconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||||
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
|
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
|
||||||
|
const Icon = icons[iconKey];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
|
|
@ -31,7 +32,8 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||||
<div className="mb-3 h-[72px] w-[72px]">
|
<div className="mb-3 h-[72px] w-[72px]">
|
||||||
<div className="gizmo-shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black">
|
<div className="gizmo-shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black">
|
||||||
{endpoint &&
|
{endpoint &&
|
||||||
icons[iconKey]({
|
Icon &&
|
||||||
|
Icon({
|
||||||
size: 41,
|
size: 41,
|
||||||
context: 'landing',
|
context: 'landing',
|
||||||
className: 'h-2/3 w-2/3',
|
className: 'h-2/3 w-2/3',
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||||
<div className="flex grow items-center justify-between gap-2">
|
<div className="flex grow items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{
|
{Icon && (
|
||||||
<Icon
|
<Icon
|
||||||
size={18}
|
size={18}
|
||||||
endpoint={endpoint}
|
endpoint={endpoint}
|
||||||
|
|
@ -90,7 +90,7 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||||
className="icon-md shrink-0 dark:text-white"
|
className="icon-md shrink-0 dark:text-white"
|
||||||
iconURL={getEndpointField(endpointsConfig, endpoint, 'iconURL')}
|
iconURL={getEndpointField(endpointsConfig, endpoint, 'iconURL')}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<div>
|
<div>
|
||||||
{title}
|
{title}
|
||||||
<div className="text-token-text-tertiary">{description}</div>
|
<div className="text-token-text-tertiary">{description}</div>
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,8 @@ const PresetItems: FC<{
|
||||||
|
|
||||||
const iconKey = getEndpointField(endpointsConfig, preset.endpoint, 'type')
|
const iconKey = getEndpointField(endpointsConfig, preset.endpoint, 'type')
|
||||||
? 'unknown'
|
? 'unknown'
|
||||||
: preset.endpoint ?? 'unknown';
|
: preset.endpointType ?? preset.endpoint ?? 'unknown';
|
||||||
|
const Icon = icons[iconKey];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Close asChild key={`preset-${preset.presetId}`}>
|
<Close asChild key={`preset-${preset.presetId}`}>
|
||||||
|
|
@ -109,12 +110,15 @@ const PresetItems: FC<{
|
||||||
title={getPresetTitle(preset)}
|
title={getPresetTitle(preset)}
|
||||||
disableHover={true}
|
disableHover={true}
|
||||||
onClick={() => onSelectPreset(preset)}
|
onClick={() => onSelectPreset(preset)}
|
||||||
icon={icons[iconKey]({
|
icon={
|
||||||
context: 'menu-item',
|
Icon &&
|
||||||
iconURL: getEndpointField(endpointsConfig, preset.endpoint, 'iconURL'),
|
Icon({
|
||||||
className: 'icon-md mr-1 dark:text-white',
|
context: 'menu-item',
|
||||||
endpoint: preset.endpoint,
|
iconURL: getEndpointField(endpointsConfig, preset.endpoint, 'iconURL'),
|
||||||
})}
|
className: 'icon-md mr-1 dark:text-white',
|
||||||
|
endpoint: preset.endpoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
selected={false}
|
selected={false}
|
||||||
data-testid={`preset-item-${preset}`}
|
data-testid={`preset-item-${preset}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
/* @ts-ignore */
|
/* @ts-ignore */
|
||||||
SSE,
|
SSE,
|
||||||
|
EndpointURLs,
|
||||||
createPayload,
|
createPayload,
|
||||||
tMessageSchema,
|
tMessageSchema,
|
||||||
tConvoUpdateSchema,
|
tConvoUpdateSchema,
|
||||||
|
|
@ -268,10 +269,11 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
|
|
||||||
const abortConversation = (conversationId = '', submission: TSubmission) => {
|
const abortConversation = (conversationId = '', submission: TSubmission) => {
|
||||||
console.log(submission);
|
console.log(submission);
|
||||||
const { endpoint } = submission?.conversation || {};
|
const { endpoint: _endpoint, endpointType } = submission?.conversation || {};
|
||||||
|
const endpoint = endpointType ?? _endpoint;
|
||||||
let res: Response;
|
let res: Response;
|
||||||
|
|
||||||
fetch(`/api/ask/${endpoint}/abort`, {
|
fetch(`${EndpointURLs[endpoint ?? '']}/abort`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -283,7 +285,29 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
res = response;
|
res = response;
|
||||||
return response.json();
|
// Check if the response is JSON
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
return response.json();
|
||||||
|
} else if (response.status === 204) {
|
||||||
|
const responseMessage = {
|
||||||
|
...submission.initialResponse,
|
||||||
|
text: submission.initialResponse.text.replace(
|
||||||
|
'<span className="result-streaming">█</span>',
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestMessage: submission.message,
|
||||||
|
responseMessage: responseMessage,
|
||||||
|
conversation: submission.conversation,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'Unexpected response from server; Status: ' + res.status + ' ' + res.statusText,
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('aborted', data);
|
console.log('aborted', data);
|
||||||
|
|
@ -295,6 +319,22 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error aborting request');
|
console.error('Error aborting request');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const convoId = conversationId ?? v4();
|
||||||
|
|
||||||
|
const text =
|
||||||
|
submission.initialResponse?.text?.length > 45 ? submission.initialResponse?.text : '';
|
||||||
|
|
||||||
|
const errorMessage = {
|
||||||
|
...submission,
|
||||||
|
...submission.initialResponse,
|
||||||
|
text: text ?? error.message ?? 'Error cancelling request',
|
||||||
|
unfinished: !!text.length,
|
||||||
|
error: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorResponse = tMessageSchema.parse(errorMessage);
|
||||||
|
setMessages([...submission.messages, submission.message, errorResponse]);
|
||||||
|
newConversation({ template: { conversationId: convoId } });
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue