Skip to main content
Glama
errors.js8.89 kB
const { logger } = require('./logger'); class StockSparkError extends Error { constructor(message, statusCode = 500, errorCode = 'UNKNOWN_ERROR', details = null) { super(message); this.name = 'StockSparkError'; this.statusCode = statusCode; this.errorCode = errorCode; this.details = details; this.timestamp = new Date().toISOString(); } } class AuthenticationError extends StockSparkError { constructor(message = 'Authentication failed', details = null) { super(message, 401, 'AUTH_FAILED', details); this.name = 'AuthenticationError'; } } class NotFoundError extends StockSparkError { constructor(resource = 'Resource', details = null) { super(`${resource} not found`, 404, 'NOT_FOUND', details); this.name = 'NotFoundError'; } } class ValidationError extends StockSparkError { constructor(message = 'Validation failed', details = null) { super(message, 400, 'VALIDATION_ERROR', details); this.name = 'ValidationError'; } } class NetworkError extends StockSparkError { constructor(message = 'Network request failed', details = null) { super(message, 503, 'NETWORK_ERROR', details); this.name = 'NetworkError'; } } class RateLimitError extends StockSparkError { constructor(message = 'Rate limit exceeded', details = null) { super(message, 429, 'RATE_LIMIT', details); this.name = 'RateLimitError'; } } function handleApiError(error, context = {}) { const { operation = 'API request', resource = 'resource', vehicleId = null } = context; // Log the original error logger.error(`Error in ${operation}:`, { message: error.message, stack: error.stack, context, timestamp: new Date().toISOString() }); // Handle fetch/network errors if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') { throw new NetworkError(`Unable to connect to StockSpark API during ${operation}`, { originalError: error.message, operation, resource }); } // Handle abort errors (request cancelled) if (error.name === 'AbortError' || error.message?.includes('aborted')) { throw new NetworkError(`Request was cancelled during ${operation}`, { originalError: error.message, operation, resource }); } // Handle HTTP response errors if (error.response) { const status = error.response.status; const statusText = error.response.statusText; switch (status) { case 400: throw new ValidationError(`Invalid request for ${operation}`, { statusCode: status, statusText, operation, resource, vehicleId }); case 401: throw new AuthenticationError(`Authentication failed during ${operation}`, { statusCode: status, statusText, operation, hint: 'Check your credentials and token expiry' }); case 403: throw new AuthenticationError(`Access forbidden for ${operation}`, { statusCode: status, statusText, operation, resource, hint: 'You may not have permission to access this resource' }); case 404: const resourceName = vehicleId ? `Vehicle ${vehicleId}` : resource; throw new NotFoundError(resourceName, { statusCode: status, statusText, operation, vehicleId, hint: vehicleId ? 'Check if the vehicle ID exists in your inventory' : 'Check if the endpoint URL is correct' }); case 409: throw new ValidationError(`Conflict in ${operation}`, { statusCode: status, statusText, operation, resource, hint: 'The resource may already exist or be in an incompatible state' }); case 422: throw new ValidationError(`Invalid data provided for ${operation}`, { statusCode: status, statusText, operation, resource, hint: 'Check the data format and required fields' }); case 429: throw new RateLimitError(`Too many requests for ${operation}`, { statusCode: status, statusText, operation, hint: 'Wait a moment before retrying' }); case 500: case 502: case 503: case 504: throw new StockSparkError(`StockSpark API server error during ${operation}`, status, 'SERVER_ERROR', { statusCode: status, statusText, operation, resource, hint: 'This is a server-side issue. Try again in a few minutes.' }); default: throw new StockSparkError(`Unexpected error during ${operation}`, status, 'HTTP_ERROR', { statusCode: status, statusText, operation, resource }); } } // Handle JSON parsing errors if (error.message?.includes('JSON') || error.name === 'SyntaxError') { throw new StockSparkError(`Invalid response format from ${operation}`, 502, 'INVALID_RESPONSE', { originalError: error.message, operation, resource, hint: 'The API returned malformed data' }); } // Handle validation errors from our own code if (error instanceof ValidationError || error instanceof AuthenticationError || error instanceof NotFoundError || error instanceof NetworkError) { throw error; // Re-throw our custom errors } // Default fallback for unknown errors throw new StockSparkError(`Unexpected error during ${operation}: ${error.message}`, 500, 'UNKNOWN_ERROR', { originalError: error.message, operation, resource, stack: error.stack }); } function formatErrorForUser(error) { if (error instanceof StockSparkError) { let message = error.message; // Add helpful context if (error.details?.hint) { message += `\n💡 Tip: ${error.details.hint}`; } if (error.details?.vehicleId) { message += `\n🚗 Vehicle ID: ${error.details.vehicleId}`; } // Add error code for debugging if (error.errorCode !== 'UNKNOWN_ERROR') { message += `\n🔍 Error Code: ${error.errorCode}`; } return message; } // Fallback for non-StockSpark errors return `An unexpected error occurred: ${error.message}`; } function isRetryableError(error) { if (error instanceof NetworkError) return true; if (error instanceof RateLimitError) return true; if (error instanceof StockSparkError && error.statusCode >= 500) return true; return false; } async function withRetry(operation, maxRetries = 3, backoffMs = 1000) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; if (!isRetryableError(error) || attempt === maxRetries) { throw error; } const delay = backoffMs * Math.pow(2, attempt - 1); // Exponential backoff logger.warn(`Attempt ${attempt} failed, retrying in ${delay}ms:`, { error: error.message, attempt, maxRetries }); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } function validateVehicleId(vehicleId) { if (!vehicleId || typeof vehicleId !== 'number' || vehicleId <= 0) { throw new ValidationError('Invalid vehicle ID provided', { vehicleId, hint: 'Vehicle ID must be a positive number' }); } } function validateRequired(value, fieldName) { if (value === null || value === undefined || value === '') { throw new ValidationError(`${fieldName} is required`, { field: fieldName, hint: `Please provide a valid ${fieldName}` }); } } function validatePrice(price) { if (typeof price !== 'number' || price < 0) { throw new ValidationError('Invalid price provided', { price, hint: 'Price must be a positive number' }); } } function validateImagePath(imagePath) { if (!imagePath || typeof imagePath !== 'string') { throw new ValidationError('Invalid image path provided', { imagePath, hint: 'Image path must be a non-empty string' }); } // Check if it's a URL or file path if (!imagePath.startsWith('http') && !imagePath.startsWith('/') && !imagePath.includes('/')) { throw new ValidationError('Invalid image path format', { imagePath, hint: 'Image path must be a valid URL or file path' }); } } module.exports = { StockSparkError, AuthenticationError, NotFoundError, ValidationError, NetworkError, RateLimitError, handleApiError, formatErrorForUser, isRetryableError, withRetry, validateVehicleId, validateRequired, validatePrice, validateImagePath };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/loukach/stockspark-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server