@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
JavaScript
/**
* @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,
};
}