This commit is contained in:
Vinnie-Singleton-NN 2025-09-19 10:07:35 -04:00 committed by GitHub
commit 753f0178c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 399 additions and 12 deletions

View file

@ -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}"}`;

View file

@ -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');
}
});

View file

@ -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');
}
});

View 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
};

View 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
};