mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
Merge ffbe093068
into 99135a3dc1
This commit is contained in:
commit
753f0178c5
5 changed files with 399 additions and 12 deletions
|
@ -8,6 +8,7 @@ const { spendTokens } = require('~/models/spendTokens');
|
|||
const abortControllers = require('./abortControllers');
|
||||
const { saveMessage, getConvo } = require('~/models');
|
||||
const { abortRun } = require('./abortRun');
|
||||
const { formatAbortError } = require('~/server/utils/routeErrorHandlers');
|
||||
|
||||
const abortDataMap = new WeakMap();
|
||||
|
||||
|
@ -306,6 +307,9 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
|
|||
* @returns { Promise<void> }
|
||||
*/
|
||||
const handleAbortError = async (res, req, error, data) => {
|
||||
|
||||
const classifiedError = formatAbortError(error, req, data);
|
||||
|
||||
if (error?.message?.includes('base64')) {
|
||||
logger.error('[handleAbortError] Error in base64 encoding', {
|
||||
...error,
|
||||
|
@ -323,9 +327,7 @@ const handleAbortError = async (res, req, error, data) => {
|
|||
);
|
||||
}
|
||||
|
||||
let errorText = error?.message?.includes('"type"')
|
||||
? error.message
|
||||
: 'An error occurred while processing your request. Please contact the Admin.';
|
||||
let errorText = JSON.stringify(classifiedError);
|
||||
|
||||
if (error?.type === ErrorTypes.INVALID_REQUEST) {
|
||||
errorText = `{"type":"${ErrorTypes.INVALID_REQUEST}"}`;
|
||||
|
|
|
@ -11,6 +11,7 @@ const { reinitMCPServer } = require('~/server/services/Tools/mcp');
|
|||
const { requireJwtAuth } = require('~/server/middleware');
|
||||
const { findPluginAuthsByKeys } = require('~/models');
|
||||
const { getLogStores } = require('~/cache');
|
||||
const { handleMCPError } = require('~/server/utils/routeErrorHandlers');
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
@ -344,7 +345,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
|||
});
|
||||
} catch (error) {
|
||||
logger.error('[MCP Reinitialize] Unexpected error', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
handleMCPError(error, req, res, 'reinitialize-server');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc');
|
|||
const { getConvosQueried } = require('~/models/Conversation');
|
||||
const { countTokens } = require('~/server/utils');
|
||||
const { Message } = require('~/db/models');
|
||||
const { handleMessagesError } = require('~/server/utils/routeErrorHandlers');
|
||||
|
||||
const router = express.Router();
|
||||
router.use(requireJwtAuth);
|
||||
|
@ -94,7 +95,7 @@ router.get('/', async (req, res) => {
|
|||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching messages:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
handleMessagesError(error, req, res, 'list-messages');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -156,7 +157,7 @@ router.post('/artifact/:messageId', async (req, res) => {
|
|||
});
|
||||
} catch (error) {
|
||||
logger.error('Error editing artifact:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
handleMessagesError(error, req, res, 'update-artifact');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -168,7 +169,7 @@ router.get('/:conversationId', validateMessageReq, async (req, res) => {
|
|||
res.status(200).json(messages);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching messages:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
handleMessagesError(error, req, res, 'get-conversation');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -187,7 +188,7 @@ router.post('/:conversationId', validateMessageReq, async (req, res) => {
|
|||
res.status(201).json(savedMessage);
|
||||
} catch (error) {
|
||||
logger.error('Error saving message:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
handleMessagesError(error, req, res, 'save-message');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -201,7 +202,7 @@ router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) =
|
|||
res.status(200).json(message);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching message:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
handleMessagesError(error, req, res, 'get-message');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -254,7 +255,7 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) =
|
|||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
logger.error('Error updating message:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
handleMessagesError(error, req, res, 'update-message');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -279,7 +280,7 @@ router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (re
|
|||
});
|
||||
} catch (error) {
|
||||
logger.error('Error updating message feedback:', error);
|
||||
res.status(500).json({ error: 'Failed to update feedback' });
|
||||
handleMessagesError(error, req, res, 'update-feedback');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -290,7 +291,7 @@ router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res
|
|||
res.status(204).send();
|
||||
} catch (error) {
|
||||
logger.error('Error deleting message:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
handleMessagesError(error, req, res, 'delete-message');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
239
api/server/utils/errorClassifier.js
Normal file
239
api/server/utils/errorClassifier.js
Normal file
|
@ -0,0 +1,239 @@
|
|||
const ErrorTypes = {
|
||||
THROTTLING_ERROR: 'THROTTLING_ERROR',
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||
RESOURCE_ERROR: 'RESOURCE_ERROR',
|
||||
MODEL_ERROR: 'MODEL_ERROR',
|
||||
REQUEST_ERROR: 'REQUEST_ERROR',
|
||||
NETWORK_ERROR: 'NETWORK_ERROR',
|
||||
AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
|
||||
CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
|
||||
DATABASE_ERROR: 'DATABASE_ERROR',
|
||||
PERMISSION_ERROR: 'PERMISSION_ERROR',
|
||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
||||
};
|
||||
|
||||
const ErrorPatterns = {
|
||||
[ErrorTypes.THROTTLING_ERROR]: [
|
||||
/rate limit/i,
|
||||
/quota exceeded/i,
|
||||
/too many requests/i,
|
||||
/throttled/i,
|
||||
/429/,
|
||||
/overloaded/i,
|
||||
/capacity/i,
|
||||
/rate_limit_exceeded/i,
|
||||
/anthropic.*temporarily overloaded/i
|
||||
],
|
||||
[ErrorTypes.VALIDATION_ERROR]: [
|
||||
/invalid parameter/i,
|
||||
/validation error/i,
|
||||
/invalid input/i,
|
||||
/parameter.*required/i,
|
||||
/malformed.*parameter/i,
|
||||
/invalid.*format/i,
|
||||
/invalid request/i
|
||||
],
|
||||
[ErrorTypes.RESOURCE_ERROR]: [
|
||||
/context.*too large/i,
|
||||
/token limit/i,
|
||||
/context window/i,
|
||||
/memory.*full/i,
|
||||
/out of memory/i,
|
||||
/maximum.*tokens/i,
|
||||
/context.*exceeded/i,
|
||||
/INPUT_LENGTH/i,
|
||||
/context_length_exceeded/i
|
||||
],
|
||||
[ErrorTypes.MODEL_ERROR]: [
|
||||
/model.*not found/i,
|
||||
/model.*unavailable/i,
|
||||
/model.*not supported/i,
|
||||
/decommissioned/i,
|
||||
/no longer supported/i,
|
||||
/invalid model/i,
|
||||
/model.*does not exist/i,
|
||||
/deployment.*not found/i
|
||||
],
|
||||
[ErrorTypes.REQUEST_ERROR]: [
|
||||
/bad request/i,
|
||||
/malformed/i,
|
||||
/invalid json/i,
|
||||
/400/,
|
||||
/syntax error/i,
|
||||
/parse error/i
|
||||
],
|
||||
[ErrorTypes.NETWORK_ERROR]: [
|
||||
/network/i,
|
||||
/timeout/i,
|
||||
/connection/i,
|
||||
/unreachable/i,
|
||||
/econnrefused/i,
|
||||
/enotfound/i,
|
||||
/socket/i,
|
||||
/dns/i
|
||||
],
|
||||
[ErrorTypes.AUTHENTICATION_ERROR]: [
|
||||
/oauth/i,
|
||||
/unauthorized/i,
|
||||
/401/,
|
||||
/authentication/i,
|
||||
/auth.*required/i,
|
||||
/invalid.*token/i,
|
||||
/expired.*token/i,
|
||||
/user mismatch/i,
|
||||
/access denied/i,
|
||||
/not authenticated/i,
|
||||
/flow not found/i,
|
||||
/invalid state/i
|
||||
],
|
||||
[ErrorTypes.CONFIGURATION_ERROR]: [
|
||||
/config.*not found/i,
|
||||
/missing.*configuration/i,
|
||||
/invalid.*config/i,
|
||||
/not.*configured/i,
|
||||
/server.*not found/i,
|
||||
/mcp.*not found/i,
|
||||
/missing server url/i,
|
||||
/invalid flow state/i
|
||||
],
|
||||
[ErrorTypes.DATABASE_ERROR]: [
|
||||
/database/i,
|
||||
/mongoose/i,
|
||||
/mongodb/i,
|
||||
/collection/i,
|
||||
/document not found/i,
|
||||
/save.*failed/i,
|
||||
/update.*failed/i,
|
||||
/delete.*failed/i,
|
||||
/message not found/i,
|
||||
/conversation not found/i,
|
||||
/artifact.*not found/i
|
||||
],
|
||||
[ErrorTypes.PERMISSION_ERROR]: [
|
||||
/permission/i,
|
||||
/forbidden/i,
|
||||
/403/,
|
||||
/access.*denied/i,
|
||||
/not.*authorized/i,
|
||||
/user.*mismatch/i,
|
||||
/invalid.*user/i
|
||||
]
|
||||
};
|
||||
|
||||
const UserMessages = {
|
||||
[ErrorTypes.THROTTLING_ERROR]: {
|
||||
title: "Service Temporarily Unavailable",
|
||||
message: "The AI service is currently experiencing high demand. Please wait a moment and try again.",
|
||||
suggestion: "Try again in a few seconds, or switch to a different model if available."
|
||||
},
|
||||
[ErrorTypes.VALIDATION_ERROR]: {
|
||||
title: "Invalid Request Parameters",
|
||||
message: "There was an issue with the request parameters sent to the service.",
|
||||
suggestion: "Please check your input and try again. If using tools, verify the parameters are correct."
|
||||
},
|
||||
[ErrorTypes.RESOURCE_ERROR]: {
|
||||
title: "Resource Limit Exceeded",
|
||||
message: "The request exceeded available resources (memory, token limits, or context window).",
|
||||
suggestion: "Try shortening your message, reducing context, or starting a new conversation."
|
||||
},
|
||||
[ErrorTypes.MODEL_ERROR]: {
|
||||
title: "Model Not Available",
|
||||
message: "The requested AI model is currently unavailable or doesn't exist.",
|
||||
suggestion: "Try selecting a different model from the available options."
|
||||
},
|
||||
[ErrorTypes.REQUEST_ERROR]: {
|
||||
title: "Request Format Error",
|
||||
message: "The request could not be processed due to formatting issues.",
|
||||
suggestion: "Please try rephrasing your request or contact support if the issue persists."
|
||||
},
|
||||
[ErrorTypes.NETWORK_ERROR]: {
|
||||
title: "Connection Error",
|
||||
message: "Unable to connect to the AI service due to network issues.",
|
||||
suggestion: "Check your internet connection and try again. If the problem persists, the service may be temporarily down."
|
||||
},
|
||||
[ErrorTypes.AUTHENTICATION_ERROR]: {
|
||||
title: "Authentication Required",
|
||||
message: "You need to authenticate to access this service.",
|
||||
suggestion: "Please log in or complete the authentication process to continue."
|
||||
},
|
||||
[ErrorTypes.CONFIGURATION_ERROR]: {
|
||||
title: "Configuration Error",
|
||||
message: "There's an issue with the service configuration.",
|
||||
suggestion: "Please contact your administrator to check the service configuration."
|
||||
},
|
||||
[ErrorTypes.DATABASE_ERROR]: {
|
||||
title: "Data Access Error",
|
||||
message: "There was a problem accessing or updating the requested data.",
|
||||
suggestion: "Please refresh the page and try again. If the issue persists, contact support."
|
||||
},
|
||||
[ErrorTypes.PERMISSION_ERROR]: {
|
||||
title: "Access Denied",
|
||||
message: "You don't have permission to perform this action.",
|
||||
suggestion: "Please check your permissions or contact an administrator."
|
||||
},
|
||||
[ErrorTypes.UNKNOWN_ERROR]: {
|
||||
title: "Unexpected Error",
|
||||
message: "An unexpected error occurred while processing your request.",
|
||||
suggestion: "Please try again. If the problem continues, contact support."
|
||||
}
|
||||
};
|
||||
|
||||
class ErrorClassifier {
|
||||
static classifyError(error) {
|
||||
const errorString = this.getErrorString(error);
|
||||
|
||||
for (const [errorType, patterns] of Object.entries(ErrorPatterns)) {
|
||||
if (patterns.some(pattern => pattern.test(errorString))) {
|
||||
return errorType;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorTypes.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
static getErrorString(error) {
|
||||
if (typeof error === 'string') return error;
|
||||
if (error?.message) return error.message;
|
||||
if (error?.error?.message) return error.error.message;
|
||||
if (error?.response?.data?.error?.message) return error.response.data.error.message;
|
||||
if (error?.response?.statusText) return error.response.statusText;
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
|
||||
static getUserFriendlyError(error, context = {}) {
|
||||
const errorType = this.classifyError(error);
|
||||
const userMessage = UserMessages[errorType];
|
||||
const originalError = this.getErrorString(error);
|
||||
|
||||
return {
|
||||
type: errorType,
|
||||
title: userMessage.title,
|
||||
message: userMessage.message,
|
||||
suggestion: userMessage.suggestion,
|
||||
originalError: originalError,
|
||||
context: context,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
static shouldRetry(errorType) {
|
||||
return [
|
||||
ErrorTypes.THROTTLING_ERROR,
|
||||
ErrorTypes.NETWORK_ERROR
|
||||
].includes(errorType);
|
||||
}
|
||||
|
||||
static getRetryDelay(errorType, attempt = 1) {
|
||||
const delays = {
|
||||
[ErrorTypes.THROTTLING_ERROR]: Math.min(1000 * Math.pow(2, attempt), 30000), // Exponential backoff
|
||||
[ErrorTypes.NETWORK_ERROR]: 2000 * attempt, // Linear backoff
|
||||
};
|
||||
return delays[errorType] || 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ErrorTypes,
|
||||
ErrorClassifier,
|
||||
UserMessages
|
||||
};
|
144
api/server/utils/routeErrorHandlers.js
Normal file
144
api/server/utils/routeErrorHandlers.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
const { ErrorClassifier } = require('./errorClassifier');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
|
||||
/**
|
||||
* Handle errors for messages routes
|
||||
*/
|
||||
const handleMessagesError = (error, req, res, operation) => {
|
||||
const userFriendlyError = ErrorClassifier.getUserFriendlyError(error, {
|
||||
endpoint: 'messages',
|
||||
operation,
|
||||
userId: req.user?.id,
|
||||
conversationId: req.params?.conversationId,
|
||||
messageId: req.params?.messageId,
|
||||
path: req.path,
|
||||
method: req.method
|
||||
});
|
||||
|
||||
logger.error(`[Messages ${operation}] Error:`, {
|
||||
...userFriendlyError,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
// Determine status code based on error type
|
||||
let statusCode = 500;
|
||||
if (userFriendlyError.type === 'PERMISSION_ERROR') statusCode = 403;
|
||||
if (userFriendlyError.type === 'AUTHENTICATION_ERROR') statusCode = 401;
|
||||
if (userFriendlyError.type === 'VALIDATION_ERROR') statusCode = 400;
|
||||
if (userFriendlyError.type === 'DATABASE_ERROR' && error.message?.includes('not found')) statusCode = 404;
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: userFriendlyError.message,
|
||||
errorType: userFriendlyError.type,
|
||||
suggestion: userFriendlyError.suggestion,
|
||||
requestId: userFriendlyError.timestamp
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle errors for MCP routes
|
||||
*/
|
||||
const handleMCPError = (error, req, res, operation) => {
|
||||
const userFriendlyError = ErrorClassifier.getUserFriendlyError(error, {
|
||||
endpoint: 'mcp',
|
||||
operation,
|
||||
userId: req.user?.id,
|
||||
serverName: req.params?.serverName,
|
||||
flowId: req.params?.flowId || req.query?.flowId,
|
||||
path: req.path,
|
||||
method: req.method
|
||||
});
|
||||
|
||||
// Special handling for OAuth errors
|
||||
if (error.message?.includes('OAuth') || error.message?.includes('authentication')) {
|
||||
userFriendlyError.type = 'AUTHENTICATION_ERROR';
|
||||
userFriendlyError.message = 'Authentication required for this MCP server';
|
||||
userFriendlyError.suggestion = 'Please complete the OAuth flow to continue';
|
||||
}
|
||||
|
||||
logger.error(`[MCP ${operation}] Error:`, {
|
||||
...userFriendlyError,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
// Determine status code
|
||||
let statusCode = 500;
|
||||
if (userFriendlyError.type === 'AUTHENTICATION_ERROR') statusCode = 401;
|
||||
if (userFriendlyError.type === 'PERMISSION_ERROR') statusCode = 403;
|
||||
if (userFriendlyError.type === 'CONFIGURATION_ERROR') statusCode = 404;
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: userFriendlyError.message,
|
||||
errorType: userFriendlyError.type,
|
||||
suggestion: userFriendlyError.suggestion,
|
||||
context: {
|
||||
serverName: req.params?.serverName,
|
||||
operation
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Format error for abort middleware
|
||||
* This returns the formatted error object without sending a response
|
||||
*/
|
||||
const formatAbortError = (error, req, data = {}) => {
|
||||
const userFriendlyError = ErrorClassifier.getUserFriendlyError(error, {
|
||||
operation: 'abort_error',
|
||||
conversationId: data.conversationId,
|
||||
endpoint: req.body?.endpointOption?.endpoint,
|
||||
userId: req.user?.id,
|
||||
messageId: data.messageId,
|
||||
parentMessageId: data.parentMessageId
|
||||
});
|
||||
|
||||
logger.error('[Abort Middleware] Error classified:', {
|
||||
type: userFriendlyError.type,
|
||||
originalError: error?.message || error,
|
||||
...userFriendlyError
|
||||
});
|
||||
|
||||
// Return structured error data that can be stringified in handleAbortError
|
||||
return {
|
||||
type: 'classified_error',
|
||||
errorType: userFriendlyError.type,
|
||||
title: userFriendlyError.title,
|
||||
message: userFriendlyError.message,
|
||||
suggestion: userFriendlyError.suggestion,
|
||||
originalError: userFriendlyError.originalError,
|
||||
timestamp: userFriendlyError.timestamp
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic error handler for any route
|
||||
* Can be used as a fallback for routes that don't fit the above patterns
|
||||
*/
|
||||
const handleGenericError = (error, req, res, endpoint, operation) => {
|
||||
const userFriendlyError = ErrorClassifier.getUserFriendlyError(error, {
|
||||
endpoint,
|
||||
operation,
|
||||
userId: req.user?.id,
|
||||
path: req.path,
|
||||
method: req.method
|
||||
});
|
||||
|
||||
logger.error(`[${endpoint} ${operation}] Error:`, {
|
||||
...userFriendlyError,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
error: userFriendlyError.message,
|
||||
errorType: userFriendlyError.type,
|
||||
suggestion: userFriendlyError.suggestion,
|
||||
requestId: userFriendlyError.timestamp
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
handleMessagesError,
|
||||
handleMCPError,
|
||||
formatAbortError,
|
||||
handleGenericError
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue