UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

297 lines (296 loc) 9.49 kB
/** * @fileoverview Error handling utilities for Sanity CMS operations * * This module provides comprehensive error handling capabilities including * specific error types, context information, and utilities for determining * whether errors are retryable. * * @example * ```typescript * import { SanityError, isRetryableError, createSanityError } from '@ai-growth/nextjs/utils'; * * try { * const result = await sanityClient.fetch(query); * } catch (error) { * const sanityError = createSanityError(error, 'FETCH_FAILED', { * query, * timestamp: new Date().toISOString() * }); * * if (isRetryableError(sanityError)) { * // Retry the operation * } else { * // Handle non-retryable error * } * } * ``` */ /** * Standard error codes for Sanity operations */ export const SANITY_ERROR_CODES = { // Network and connection errors NETWORK_ERROR: 'NETWORK_ERROR', CONNECTION_TIMEOUT: 'CONNECTION_TIMEOUT', DNS_RESOLUTION_FAILED: 'DNS_RESOLUTION_FAILED', // HTTP status code errors RATE_LIMITED: 'RATE_LIMITED', SERVER_ERROR: 'SERVER_ERROR', SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', GATEWAY_TIMEOUT: 'GATEWAY_TIMEOUT', // Authentication and authorization UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', TOKEN_EXPIRED: 'TOKEN_EXPIRED', // Client errors BAD_REQUEST: 'BAD_REQUEST', NOT_FOUND: 'NOT_FOUND', INVALID_QUERY: 'INVALID_QUERY', VALIDATION_ERROR: 'VALIDATION_ERROR', // Operation-specific errors FETCH_FAILED: 'FETCH_FAILED', MUTATION_FAILED: 'MUTATION_FAILED', CONNECTION_VERIFICATION_FAILED: 'CONNECTION_VERIFICATION_FAILED', CLIENT_CREATION_FAILED: 'CLIENT_CREATION_FAILED', // Configuration errors CONFIGURATION_ERROR: 'CONFIGURATION_ERROR', MISSING_CREDENTIALS: 'MISSING_CREDENTIALS', INVALID_PROJECT_CONFIG: 'INVALID_PROJECT_CONFIG', // Unknown errors UNKNOWN_ERROR: 'UNKNOWN_ERROR', }; /** * Enhanced error class for Sanity operations with detailed context */ export class SanityError extends Error { constructor(message, code, context = {}, retryable = false, originalError) { super(message); this.code = code; this.context = context; this.retryable = retryable; this.originalError = originalError; this.name = 'SanityError'; // Maintain proper stack trace if (Error.captureStackTrace) { Error.captureStackTrace(this, SanityError); } // Include original error stack if available if (originalError?.stack) { this.stack = `${this.stack}\nCaused by: ${originalError.stack}`; } } /** * Convert error to JSON for logging or serialization */ toJSON() { return { name: this.name, message: this.message, code: this.code, context: this.context, retryable: this.retryable, stack: this.stack, originalError: this.originalError ? { name: this.originalError.name, message: this.originalError.message, stack: this.originalError.stack, } : undefined, }; } /** * Get a human-readable description of the error */ getDescription() { const parts = [this.message]; if (this.context.operation) { parts.push(`Operation: ${this.context.operation}`); } if (this.context.statusCode) { parts.push(`Status: ${this.context.statusCode}`); } if (this.context.retryAttempt) { parts.push(`Retry attempt: ${this.context.retryAttempt}`); } return parts.join(' | '); } } /** * Determine if an error is retryable based on its type and status code */ export function isRetryableError(error) { if (error instanceof SanityError) { return error.retryable; } // Check for common retryable error patterns const message = error.message.toLowerCase(); // Network errors are generally retryable if (message.includes('network') || message.includes('timeout') || message.includes('connection') || message.includes('econnreset') || message.includes('enotfound') || message.includes('econnrefused')) { return true; } // Check for HTTP status codes in error message const statusMatch = message.match(/(?:status:?\s*|http\s+)(\d{3})/); if (statusMatch) { const status = parseInt(statusMatch[1], 10); return isRetryableStatusCode(status); } return false; } /** * Determine if an HTTP status code indicates a retryable error */ export function isRetryableStatusCode(statusCode) { // 429 (Rate Limited) - retryable with backoff if (statusCode === 429) return true; // 5xx server errors - generally retryable if (statusCode >= 500 && statusCode < 600) return true; // 408 (Request Timeout) - retryable if (statusCode === 408) return true; // All other status codes are not retryable return false; } /** * Create a SanityError from various error types */ export function createSanityError(error, code, context = {}) { let message; let originalError; if (error instanceof Error) { message = error.message; originalError = error; } else if (typeof error === 'string') { message = error; } else { message = 'Unknown error occurred'; } // Enhance context with timestamp if not provided const enhancedContext = { timestamp: new Date().toISOString(), ...context, }; // Determine if error is retryable const retryable = isRetryableError(originalError || new Error(message)); return new SanityError(message, code, enhancedContext, retryable, originalError); } /** * Create a SanityError from an HTTP response */ export function createHttpError(statusCode, statusText, context = {}) { let code; let message; switch (statusCode) { case 400: code = SANITY_ERROR_CODES.BAD_REQUEST; message = `Bad request: ${statusText}`; break; case 401: code = SANITY_ERROR_CODES.UNAUTHORIZED; message = `Unauthorized: ${statusText}`; break; case 403: code = SANITY_ERROR_CODES.FORBIDDEN; message = `Forbidden: ${statusText}`; break; case 404: code = SANITY_ERROR_CODES.NOT_FOUND; message = `Not found: ${statusText}`; break; case 408: code = SANITY_ERROR_CODES.CONNECTION_TIMEOUT; message = `Request timeout: ${statusText}`; break; case 429: code = SANITY_ERROR_CODES.RATE_LIMITED; message = `Rate limited: ${statusText}`; break; case 500: code = SANITY_ERROR_CODES.SERVER_ERROR; message = `Server error: ${statusText}`; break; case 502: case 503: code = SANITY_ERROR_CODES.SERVICE_UNAVAILABLE; message = `Service unavailable: ${statusText}`; break; case 504: code = SANITY_ERROR_CODES.GATEWAY_TIMEOUT; message = `Gateway timeout: ${statusText}`; break; default: if (statusCode >= 500) { code = SANITY_ERROR_CODES.SERVER_ERROR; message = `Server error (${statusCode}): ${statusText}`; } else { code = SANITY_ERROR_CODES.BAD_REQUEST; message = `Client error (${statusCode}): ${statusText}`; } } const enhancedContext = { statusCode, timestamp: new Date().toISOString(), ...context, }; const retryable = isRetryableStatusCode(statusCode); return new SanityError(message, code, enhancedContext, retryable); } /** * Wrap an async operation with error handling */ export async function withErrorHandling(operation, operationName, context = {}) { const startTime = Date.now(); try { const result = await operation(); return result; } catch (error) { const duration = Date.now() - startTime; const enhancedContext = { operation: operationName, duration, ...context, }; // If it's already a SanityError, enhance it with additional context if (error instanceof SanityError) { throw new SanityError(error.message, error.code, { ...error.context, ...enhancedContext }, error.retryable, error.originalError); } // Create new SanityError for unknown errors throw createSanityError(error, SANITY_ERROR_CODES.UNKNOWN_ERROR, enhancedContext); } } /** * Type guard to check if an error is a SanityError */ export function isSanityError(error) { return error instanceof SanityError; } /** * Extract error details for logging */ export function getErrorDetails(error) { if (error instanceof SanityError) { return error.toJSON(); } if (error instanceof Error) { return { name: error.name, message: error.message, stack: error.stack, }; } return { error: String(error), type: typeof error, }; }