mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
* fix: create new flows on invalid_grant errors * chore: fix failing test * chore: keep isOAuthError test function in sync with implementation * test: add tests for OAuth error detection on invalid grant errors * test: add tests for creating new flows when token expires * test: add test for flow clean up prior to creation * refactor: consolidate token expiration handling in FlowStateManager - Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic. - Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds. - Updated tests to ensure proper functionality of flow management with token expiration scenarios. * fix: conditionally setup cleanup handlers in FlowStateManager - Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management. * chore: enhance OAuth token refresh logging - Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server. - Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated. - Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management. * chore: enhance logging for MCP server reinitialization - Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
190 lines
6.7 KiB
TypeScript
190 lines
6.7 KiB
TypeScript
/**
|
|
* Tests for MCPConnection error detection methods.
|
|
*
|
|
* These tests use standalone implementations that mirror the private methods in MCPConnection.
|
|
* This approach was chosen because MCPConnection requires complex dependencies (Client, transport)
|
|
* that are difficult to mock properly. The standalone implementations are kept in sync with
|
|
* the actual implementation in connection.ts.
|
|
*
|
|
* Alternative approaches considered:
|
|
* 1. Reflection/type casting - fragile and breaks with refactoring
|
|
* 2. Protected methods with test subclass - changes public API for testing
|
|
* 3. Integration tests - tested separately in the full MCP test suite
|
|
*/
|
|
describe('MCPConnection Error Detection', () => {
|
|
/**
|
|
* Standalone implementation of isRateLimitError for testing.
|
|
* This mirrors the private method in MCPConnection (connection.ts).
|
|
* Keep in sync with the actual implementation.
|
|
*/
|
|
function isRateLimitError(error: unknown): boolean {
|
|
if (!error || typeof error !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
// Check for error code
|
|
if ('code' in error) {
|
|
const code = (error as { code?: number }).code;
|
|
if (code === 429) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check message for rate limit indicators
|
|
if ('message' in error && typeof error.message === 'string') {
|
|
const message = error.message.toLowerCase();
|
|
if (
|
|
message.includes('429') ||
|
|
message.includes('rate limit') ||
|
|
message.includes('too many requests')
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Standalone implementation of isOAuthError for testing.
|
|
* This mirrors the private method in MCPConnection (connection.ts).
|
|
* Keep in sync with the actual implementation.
|
|
*/
|
|
function isOAuthError(error: unknown): boolean {
|
|
if (!error || typeof error !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
// Check for error code
|
|
if ('code' in error) {
|
|
const code = (error as { code?: number }).code;
|
|
if (code === 401 || code === 403) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check message for various auth error indicators
|
|
if ('message' in error && typeof error.message === 'string') {
|
|
const message = error.message.toLowerCase();
|
|
// Check for 401 status
|
|
if (message.includes('401') || message.includes('non-200 status code (401)')) {
|
|
return true;
|
|
}
|
|
// Check for invalid_grant (OAuth servers return this for expired/revoked grants)
|
|
if (message.includes('invalid_grant')) {
|
|
return true;
|
|
}
|
|
// Check for invalid_token (OAuth servers return this for expired/revoked tokens)
|
|
if (message.includes('invalid_token')) {
|
|
return true;
|
|
}
|
|
// Check for authentication required
|
|
if (message.includes('authentication required') || message.includes('unauthorized')) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
describe('isRateLimitError', () => {
|
|
it('should detect rate limit error by code 429', () => {
|
|
const error = { code: 429, message: 'Too many requests' };
|
|
expect(isRateLimitError(error)).toBe(true);
|
|
});
|
|
|
|
it('should detect rate limit error by message containing 429', () => {
|
|
const error = { message: 'Error POSTing to endpoint (HTTP 429): Too many requests' };
|
|
expect(isRateLimitError(error)).toBe(true);
|
|
});
|
|
|
|
it('should detect rate limit error by message containing "rate limit"', () => {
|
|
const error = { message: 'Rate limit exceeded, please try again later' };
|
|
expect(isRateLimitError(error)).toBe(true);
|
|
});
|
|
|
|
it('should detect rate limit error by message containing "too many requests"', () => {
|
|
const error = { message: 'Too many requests - slow down!' };
|
|
expect(isRateLimitError(error)).toBe(true);
|
|
});
|
|
|
|
it('should not detect rate limit for 401 errors', () => {
|
|
const error = { code: 401, message: 'Unauthorized' };
|
|
expect(isRateLimitError(error)).toBe(false);
|
|
});
|
|
|
|
it('should not detect rate limit for 500 errors', () => {
|
|
const error = { code: 500, message: 'Internal server error' };
|
|
expect(isRateLimitError(error)).toBe(false);
|
|
});
|
|
|
|
it('should not detect rate limit for null/undefined', () => {
|
|
expect(isRateLimitError(null)).toBe(false);
|
|
expect(isRateLimitError(undefined)).toBe(false);
|
|
});
|
|
|
|
it('should not detect rate limit for non-object errors', () => {
|
|
expect(isRateLimitError('string error')).toBe(false);
|
|
expect(isRateLimitError(123)).toBe(false);
|
|
});
|
|
|
|
it('should handle real-world StackOverflow rate limit error', () => {
|
|
const error = {
|
|
code: 429,
|
|
message:
|
|
'Streamable HTTP error: Error POSTing to endpoint: <!DOCTYPE html><html>Too Many Requests</html>',
|
|
};
|
|
expect(isRateLimitError(error)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isOAuthError', () => {
|
|
it('should detect OAuth error by code 401', () => {
|
|
const error = { code: 401, message: 'Unauthorized' };
|
|
expect(isOAuthError(error)).toBe(true);
|
|
});
|
|
|
|
it('should detect OAuth error by code 403', () => {
|
|
const error = { code: 403, message: 'Forbidden' };
|
|
expect(isOAuthError(error)).toBe(true);
|
|
});
|
|
|
|
it('should detect OAuth error by message containing 401', () => {
|
|
const error = { message: 'Error POSTing to endpoint (HTTP 401): Unauthorized' };
|
|
expect(isOAuthError(error)).toBe(true);
|
|
});
|
|
|
|
it('should not detect OAuth error for 429 rate limit', () => {
|
|
const error = { code: 429, message: 'Too many requests' };
|
|
expect(isOAuthError(error)).toBe(false);
|
|
});
|
|
|
|
it('should detect OAuth error for invalid_token', () => {
|
|
const error = { message: 'The access token is invalid_token or expired' };
|
|
expect(isOAuthError(error)).toBe(true);
|
|
});
|
|
|
|
it('should detect OAuth error for invalid_grant', () => {
|
|
const error = {
|
|
message:
|
|
'Streamable HTTP error: Error POSTing to endpoint: {"error":"invalid_grant","error_description":"The provided authorization grant is invalid, expired, or revoked"}',
|
|
};
|
|
expect(isOAuthError(error)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('error type differentiation', () => {
|
|
it('should correctly differentiate between rate limit and OAuth errors', () => {
|
|
const rateLimitError = { code: 429, message: 'Too many requests' };
|
|
const oauthError = { code: 401, message: 'Unauthorized' };
|
|
|
|
// Rate limit error should be detected as rate limit, not OAuth
|
|
expect(isRateLimitError(rateLimitError)).toBe(true);
|
|
expect(isOAuthError(rateLimitError)).toBe(false);
|
|
|
|
// OAuth error should be detected as OAuth, not rate limit
|
|
expect(isOAuthError(oauthError)).toBe(true);
|
|
expect(isRateLimitError(oauthError)).toBe(false);
|
|
});
|
|
});
|
|
});
|