UNPKG

atp-sdk

Version:

Official TypeScript SDK for Agent Trust Protocol™ - Build secure, verifiable, and trustworthy applications with decentralized identity, verifiable credentials, and robust access control

1,073 lines (860 loc) 27.9 kB
# Error Handling Guide This guide covers comprehensive error handling patterns and best practices for the ATP™ SDK. ## Table of Contents 1. [Error Types](#error-types) 2. [Basic Error Handling](#basic-error-handling) 3. [Advanced Error Patterns](#advanced-error-patterns) 4. [Retry Strategies](#retry-strategies) 5. [Circuit Breakers](#circuit-breakers) 6. [Error Recovery](#error-recovery) 7. [Logging and Monitoring](#logging-and-monitoring) 8. [Testing Error Scenarios](#testing-error-scenarios) ## Error Types The ATP™ SDK provides a structured error hierarchy for different types of failures: ### Base Error Classes ```javascript import { ATPError, // Base error class ATPNetworkError, // Network-related errors ATPAuthenticationError, // Authentication failures ATPAuthorizationError, // Permission/access errors ATPValidationError, // Input validation errors ATPServiceError // Service-specific errors } from '@atp/sdk'; ``` ### Error Properties All ATP errors include these properties: ```javascript try { await client.identity.resolve('invalid-did'); } catch (error) { console.log('Error type:', error.constructor.name); console.log('Message:', error.message); console.log('Error code:', error.code); console.log('Details:', error.details); console.log('Timestamp:', error.timestamp); console.log('Request ID:', error.requestId); // Network errors also include: if (error instanceof ATPNetworkError) { console.log('Status code:', error.statusCode); console.log('Response data:', error.response); } } ``` ### Common Error Codes #### Authentication Errors - `INVALID_DID` - Malformed DID - `INVALID_SIGNATURE` - Cryptographic signature verification failed - `TOKEN_EXPIRED` - JWT token has expired - `TOKEN_INVALID` - JWT token is malformed or invalid - `MFA_REQUIRED` - Multi-factor authentication required - `INSUFFICIENT_TRUST` - Trust level too low for operation #### Authorization Errors - `ACCESS_DENIED` - Insufficient permissions - `RESOURCE_NOT_FOUND` - Requested resource doesn't exist - `OPERATION_NOT_ALLOWED` - Operation not permitted - `QUOTA_EXCEEDED` - Rate limit or quota exceeded #### Network Errors - `CONNECTION_TIMEOUT` - Request timed out - `CONNECTION_REFUSED` - Cannot connect to service - `DNS_RESOLUTION_FAILED` - Cannot resolve hostname - `SSL_ERROR` - TLS/SSL certificate issue #### Validation Errors - `INVALID_INPUT` - Input validation failed - `MISSING_REQUIRED_FIELD` - Required field not provided - `INVALID_FORMAT` - Data format is incorrect - `SCHEMA_VALIDATION_FAILED` - JSON schema validation failed ## Basic Error Handling ### Try-Catch Pattern ```javascript async function basicErrorHandling() { try { const result = await client.identity.resolve('did:atp:testnet:example'); console.log('Identity resolved:', result.data); } catch (error) { if (error instanceof ATPValidationError) { console.error('Invalid DID format:', error.message); } else if (error instanceof ATPNetworkError) { console.error('Network error:', error.message); console.error('Status:', error.statusCode); } else if (error instanceof ATPAuthenticationError) { console.error('Authentication failed:', error.message); } else { console.error('Unexpected error:', error.message); } } } ``` ### Error Type Checking ```javascript function handleSpecificErrors(error) { switch (error.constructor.name) { case 'ATPAuthenticationError': if (error.code === 'MFA_REQUIRED') { return handleMFARequired(error); } else if (error.code === 'TOKEN_EXPIRED') { return handleTokenExpired(error); } break; case 'ATPAuthorizationError': if (error.code === 'ACCESS_DENIED') { return handleAccessDenied(error); } break; case 'ATPNetworkError': if (error.statusCode >= 500) { return handleServerError(error); } else if (error.statusCode === 429) { return handleRateLimit(error); } break; default: return handleGenericError(error); } } ``` ### Error Context Extract useful context from errors: ```javascript function extractErrorContext(error) { const context = { errorType: error.constructor.name, message: error.message, code: error.code, timestamp: error.timestamp || new Date().toISOString(), requestId: error.requestId }; // Add service-specific context if (error instanceof ATPServiceError) { context.service = error.service; context.operation = error.operation; } // Add network context if (error instanceof ATPNetworkError) { context.statusCode = error.statusCode; context.url = error.config?.url; context.method = error.config?.method; } // Add validation context if (error instanceof ATPValidationError) { context.field = error.field; context.value = error.value; context.constraint = error.constraint; } return context; } ``` ## Advanced Error Patterns ### Error Wrapper Class Create a unified error handling wrapper: ```javascript class ATPErrorHandler { constructor(options = {}) { this.retryAttempts = options.retryAttempts || 3; this.retryDelay = options.retryDelay || 1000; this.logger = options.logger || console; this.metrics = options.metrics; } async execute(operation, context = {}) { const startTime = Date.now(); try { const result = await this.executeWithRetry(operation, context); // Record success metrics if (this.metrics) { this.metrics.recordSuccess(context.operation, Date.now() - startTime); } return result; } catch (error) { // Record failure metrics if (this.metrics) { this.metrics.recordFailure( context.operation, error.constructor.name, Date.now() - startTime ); } // Log error with context this.logError(error, context); // Transform error if needed throw this.transformError(error, context); } } async executeWithRetry(operation, context) { let lastError; for (let attempt = 0; attempt < this.retryAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Don't retry certain error types if (!this.shouldRetry(error, attempt)) { throw error; } // Wait before retry if (attempt < this.retryAttempts - 1) { await this.delay(this.calculateRetryDelay(attempt)); } } } throw lastError; } shouldRetry(error, attempt) { // Don't retry validation or authentication errors if (error instanceof ATPValidationError || error instanceof ATPAuthenticationError) { return false; } // Retry network errors and server errors if (error instanceof ATPNetworkError) { return error.statusCode >= 500 || error.statusCode === 429; } return true; } calculateRetryDelay(attempt) { // Exponential backoff with jitter const baseDelay = this.retryDelay * Math.pow(2, attempt); const jitter = Math.random() * 0.1 * baseDelay; return baseDelay + jitter; } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } logError(error, context) { const errorContext = { ...extractErrorContext(error), ...context, stack: error.stack }; this.logger.error('ATP SDK Error:', errorContext); } transformError(error, context) { // Transform errors for specific business logic if (error instanceof ATPAuthorizationError && error.code === 'ACCESS_DENIED' && context.operation === 'sensitive_operation') { return new Error('Access to sensitive operation requires additional permissions'); } return error; } } // Usage const errorHandler = new ATPErrorHandler({ retryAttempts: 3, retryDelay: 1000, logger: winston.createLogger({...}), metrics: new MetricsCollector() }); const result = await errorHandler.execute( () => client.identity.resolve(did), { operation: 'identity.resolve', did } ); ``` ### Async Error Boundaries Implement error boundaries for async operations: ```javascript class AsyncErrorBoundary { constructor(options = {}) { this.fallbackHandlers = new Map(); this.errorReporters = []; this.circuitBreakers = new Map(); } addFallbackHandler(errorType, handler) { this.fallbackHandlers.set(errorType, handler); } addErrorReporter(reporter) { this.errorReporters.push(reporter); } async protect(operation, context = {}) { try { return await operation(); } catch (error) { // Report error to external systems await this.reportError(error, context); // Try fallback handlers const fallback = this.findFallbackHandler(error); if (fallback) { try { return await fallback(error, context); } catch (fallbackError) { // Fallback failed, continue with original error } } throw error; } } findFallbackHandler(error) { // Check for specific error type handlers for (const [errorType, handler] of this.fallbackHandlers) { if (error instanceof errorType) { return handler; } } // Check for error code handlers return this.fallbackHandlers.get(error.code); } async reportError(error, context) { const errorReport = { error: extractErrorContext(error), context, timestamp: new Date().toISOString(), stack: error.stack }; // Send to all error reporters await Promise.allSettled( this.errorReporters.map(reporter => reporter.report(errorReport)) ); } } // Setup error boundary const errorBoundary = new AsyncErrorBoundary(); // Add fallback for network errors errorBoundary.addFallbackHandler(ATPNetworkError, async (error, context) => { if (context.operation === 'identity.resolve') { // Return cached identity if available return await getCachedIdentity(context.did); } throw error; }); // Add fallback for authentication errors errorBoundary.addFallbackHandler(ATPAuthenticationError, async (error, context) => { if (error.code === 'TOKEN_EXPIRED') { // Attempt token refresh await refreshAuthToken(); return await context.retry(); } throw error; }); // Usage const identity = await errorBoundary.protect( () => client.identity.resolve(did), { operation: 'identity.resolve', did, retry: () => client.identity.resolve(did) } ); ``` ## Retry Strategies ### Exponential Backoff ```javascript class ExponentialBackoff { constructor(options = {}) { this.initialDelay = options.initialDelay || 1000; this.maxDelay = options.maxDelay || 30000; this.multiplier = options.multiplier || 2; this.jitter = options.jitter || 0.1; } async execute(operation, maxAttempts = 3) { let attempt = 0; while (attempt < maxAttempts) { try { return await operation(); } catch (error) { attempt++; if (attempt >= maxAttempts || !this.shouldRetry(error)) { throw error; } const delay = this.calculateDelay(attempt - 1); await this.sleep(delay); } } } calculateDelay(attempt) { let delay = this.initialDelay * Math.pow(this.multiplier, attempt); delay = Math.min(delay, this.maxDelay); // Add jitter to prevent thundering herd const jitterAmount = delay * this.jitter; const jitter = (Math.random() - 0.5) * 2 * jitterAmount; return Math.max(0, delay + jitter); } shouldRetry(error) { // Retry on network errors and server errors return error instanceof ATPNetworkError && (error.statusCode >= 500 || error.statusCode === 429); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } ``` ### Retry with Decorators ```javascript function retry(attempts = 3, delay = 1000) { return function(target, propertyName, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args) { let lastError; for (let attempt = 0; attempt < attempts; attempt++) { try { return await originalMethod.apply(this, args); } catch (error) { lastError = error; if (attempt < attempts - 1 && shouldRetryError(error)) { await sleep(delay * Math.pow(2, attempt)); } else { break; } } } throw lastError; }; return descriptor; }; } // Usage class IdentityService { constructor(client) { this.client = client; } @retry(3, 1000) async resolveIdentity(did) { return await this.client.identity.resolve(did); } @retry(5, 2000) async createCredential(request) { return await this.client.credentials.issue(request); } } ``` ## Circuit Breakers ### Basic Circuit Breaker ```javascript class CircuitBreaker { constructor(options = {}) { this.failureThreshold = options.failureThreshold || 5; this.resetTimeout = options.resetTimeout || 30000; this.monitoringPeriod = options.monitoringPeriod || 10000; this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN this.failureCount = 0; this.lastFailureTime = null; this.successCount = 0; } async execute(operation) { if (this.state === 'OPEN') { if (this.shouldAttemptReset()) { this.state = 'HALF_OPEN'; this.successCount = 0; } else { throw new Error('Circuit breaker is OPEN - service unavailable'); } } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failureCount = 0; if (this.state === 'HALF_OPEN') { this.successCount++; if (this.successCount >= 3) { this.state = 'CLOSED'; } } else { this.state = 'CLOSED'; } } onFailure() { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { this.state = 'OPEN'; } } shouldAttemptReset() { return Date.now() - this.lastFailureTime > this.resetTimeout; } getState() { return { state: this.state, failureCount: this.failureCount, lastFailureTime: this.lastFailureTime }; } } // Service-specific circuit breakers class ServiceClient { constructor(atpClient) { this.client = atpClient; this.circuitBreakers = { identity: new CircuitBreaker({ failureThreshold: 3, resetTimeout: 15000 }), credentials: new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000 }), audit: new CircuitBreaker({ failureThreshold: 10, resetTimeout: 60000 }) }; } async resolveIdentity(did) { return await this.circuitBreakers.identity.execute( () => this.client.identity.resolve(did) ); } async issueCredential(request) { return await this.circuitBreakers.credentials.execute( () => this.client.credentials.issue(request) ); } } ``` ## Error Recovery ### Graceful Degradation ```javascript class GracefulDegradationHandler { constructor(client, fallbackStrategies = {}) { this.client = client; this.fallbackStrategies = fallbackStrategies; this.cache = new Map(); } async resolveIdentityWithFallback(did) { try { // Try primary service const result = await this.client.identity.resolve(did); // Cache successful result this.cache.set(`identity:${did}`, { data: result, timestamp: Date.now(), ttl: 300000 // 5 minutes }); return result; } catch (error) { console.warn('Primary identity service failed:', error.message); // Try fallback strategies return await this.tryFallbackStrategies('identity.resolve', did, error); } } async tryFallbackStrategies(operation, ...args) { const strategies = this.fallbackStrategies[operation] || []; for (const strategy of strategies) { try { return await strategy.apply(this, args); } catch (fallbackError) { console.warn('Fallback strategy failed:', fallbackError.message); } } throw new Error(`All fallback strategies exhausted for ${operation}`); } // Fallback strategy: use cached data async getCachedIdentity(did) { const cached = this.cache.get(`identity:${did}`); if (cached && (Date.now() - cached.timestamp) < cached.ttl) { console.info('Using cached identity data'); return cached.data; } throw new Error('No valid cached data available'); } // Fallback strategy: use alternative service async getIdentityFromAlternativeService(did) { const alternativeUrl = process.env.ALTERNATIVE_IDENTITY_SERVICE; if (!alternativeUrl) { throw new Error('No alternative service configured'); } const response = await fetch(`${alternativeUrl}/identity/${did}`); if (!response.ok) { throw new Error('Alternative service request failed'); } return await response.json(); } // Fallback strategy: return partial data async getPartialIdentityData(did) { console.info('Returning partial identity data'); return { success: true, data: { did: did, status: 'unknown', partial: true, message: 'Limited data due to service unavailability' } }; } } // Setup fallback strategies const degradationHandler = new GracefulDegradationHandler(client, { 'identity.resolve': [ handler.getCachedIdentity.bind(handler), handler.getIdentityFromAlternativeService.bind(handler), handler.getPartialIdentityData.bind(handler) ] }); ``` ### Recovery Workflows ```javascript class RecoveryWorkflow { constructor(client) { this.client = client; this.recoveryAttempts = new Map(); } async executeWithRecovery(operation, context) { const recoveryKey = context.recoveryKey || 'default'; try { return await operation(); } catch (error) { return await this.attemptRecovery(error, operation, context, recoveryKey); } } async attemptRecovery(error, operation, context, recoveryKey) { const attempts = this.recoveryAttempts.get(recoveryKey) || 0; const maxAttempts = context.maxRecoveryAttempts || 3; if (attempts >= maxAttempts) { throw new Error(`Recovery failed after ${attempts} attempts: ${error.message}`); } this.recoveryAttempts.set(recoveryKey, attempts + 1); try { // Execute recovery strategy based on error type await this.executeRecoveryStrategy(error, context); // Retry original operation const result = await operation(); // Reset recovery counter on success this.recoveryAttempts.delete(recoveryKey); return result; } catch (recoveryError) { // Recovery failed, wait and try again await this.delay(1000 * Math.pow(2, attempts)); return await this.attemptRecovery(error, operation, context, recoveryKey); } } async executeRecoveryStrategy(error, context) { if (error instanceof ATPAuthenticationError) { await this.recoverAuthentication(error, context); } else if (error instanceof ATPNetworkError) { await this.recoverNetworkError(error, context); } else if (error instanceof ATPServiceError) { await this.recoverServiceError(error, context); } } async recoverAuthentication(error, context) { if (error.code === 'TOKEN_EXPIRED') { console.info('Attempting token refresh'); await this.refreshToken(context); } else if (error.code === 'MFA_REQUIRED') { console.info('Attempting MFA recovery'); await this.handleMFARecovery(context); } } async recoverNetworkError(error, context) { if (error.statusCode >= 500) { console.info('Server error detected, checking service health'); await this.waitForServiceHealth(context.service); } } async recoverServiceError(error, context) { console.info(`Attempting service-specific recovery for ${error.service}`); // Implement service-specific recovery logic } async refreshToken(context) { // Implement token refresh logic const newToken = await this.generateNewToken(context); this.client.setAuthentication({ token: newToken }); } async delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } ``` ## Logging and Monitoring ### Error Logging ```javascript class ErrorLogger { constructor(options = {}) { this.logLevel = options.logLevel || 'info'; this.destinations = options.destinations || [console]; this.sensitiveFields = options.sensitiveFields || ['privateKey', 'password', 'token']; } logError(error, context = {}) { const logEntry = { timestamp: new Date().toISOString(), level: 'error', message: error.message, errorType: error.constructor.name, code: error.code, requestId: error.requestId, context: this.sanitizeContext(context), stack: error.stack }; // Add service-specific information if (error instanceof ATPServiceError) { logEntry.service = error.service; logEntry.operation = error.operation; } // Add network information if (error instanceof ATPNetworkError) { logEntry.statusCode = error.statusCode; logEntry.url = error.config?.url; logEntry.method = error.config?.method; } this.writeToDestinations(logEntry); } sanitizeContext(context) { const sanitized = { ...context }; this.sensitiveFields.forEach(field => { if (sanitized[field]) { sanitized[field] = '[REDACTED]'; } }); return sanitized; } writeToDestinations(logEntry) { this.destinations.forEach(destination => { if (typeof destination.error === 'function') { destination.error(logEntry); } else { destination.log(JSON.stringify(logEntry)); } }); } } // Setup with Winston import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), new winston.transports.Console() ] }); const errorLogger = new ErrorLogger({ destinations: [logger], sensitiveFields: ['privateKey', 'token', 'password', 'secret'] }); ``` ### Error Metrics ```javascript class ErrorMetrics { constructor(metricsClient) { this.metrics = metricsClient; } recordError(error, context = {}) { const tags = { errorType: error.constructor.name, errorCode: error.code || 'unknown', service: context.service || 'unknown', operation: context.operation || 'unknown' }; // Increment error counter this.metrics.increment('atp.sdk.errors.total', 1, tags); // Record error by type this.metrics.increment(`atp.sdk.errors.${error.constructor.name.toLowerCase()}`, 1, tags); // Record response time if available if (context.duration) { this.metrics.timing('atp.sdk.errors.duration', context.duration, tags); } // Record service-specific metrics if (error instanceof ATPNetworkError) { this.metrics.increment(`atp.sdk.network_errors.status_${error.statusCode}`, 1, tags); } } recordRecovery(error, recoveryMethod, success) { const tags = { errorType: error.constructor.name, recoveryMethod: recoveryMethod, success: success.toString() }; this.metrics.increment('atp.sdk.recovery_attempts', 1, tags); } } ``` ## Testing Error Scenarios ### Error Simulation ```javascript // Test helper for simulating errors class ErrorSimulator { constructor(client) { this.client = client; this.simulationMode = false; this.errorScenarios = new Map(); } enableSimulation() { this.simulationMode = true; } disableSimulation() { this.simulationMode = false; this.errorScenarios.clear(); } addErrorScenario(operation, errorType, probability = 1.0) { this.errorScenarios.set(operation, { errorType, probability, count: 0 }); } async simulateOperation(operation, originalFunction, ...args) { if (!this.simulationMode) { return await originalFunction.apply(this.client, args); } const scenario = this.errorScenarios.get(operation); if (scenario && Math.random() < scenario.probability) { scenario.count++; switch (scenario.errorType) { case 'network_timeout': throw new ATPNetworkError('Request timeout', 'TIMEOUT'); case 'auth_failure': throw new ATPAuthenticationError('Authentication failed', 'AUTH_FAILED'); case 'server_error': throw new ATPNetworkError('Internal server error', 'SERVER_ERROR', { statusCode: 500 }); case 'rate_limit': throw new ATPNetworkError('Rate limit exceeded', 'RATE_LIMIT', { statusCode: 429 }); default: throw new ATPError('Simulated error'); } } return await originalFunction.apply(this.client, args); } } // Test cases describe('Error Handling', () => { let client, simulator; beforeEach(() => { client = new ATPClient(testConfig); simulator = new ErrorSimulator(client); }); test('should handle network timeouts', async () => { simulator.enableSimulation(); simulator.addErrorScenario('identity.resolve', 'network_timeout', 1.0); await expect( simulator.simulateOperation( 'identity.resolve', client.identity.resolve, 'did:atp:testnet:example' ) ).rejects.toThrow(ATPNetworkError); }); test('should retry on server errors', async () => { const retryHandler = new ExponentialBackoff({ maxAttempts: 3 }); simulator.enableSimulation(); simulator.addErrorScenario('identity.resolve', 'server_error', 0.8); // Should eventually succeed or fail after retries try { await retryHandler.execute(() => simulator.simulateOperation( 'identity.resolve', client.identity.resolve, 'did:atp:testnet:example' ) ); } catch (error) { expect(error).toBeInstanceOf(ATPNetworkError); } }); test('should recover from authentication failures', async () => { const recoveryWorkflow = new RecoveryWorkflow(client); simulator.enableSimulation(); simulator.addErrorScenario('identity.resolve', 'auth_failure', 1.0); // Mock token refresh jest.spyOn(recoveryWorkflow, 'refreshToken').mockResolvedValue(undefined); const result = await recoveryWorkflow.executeWithRecovery( () => simulator.simulateOperation( 'identity.resolve', client.identity.resolve, 'did:atp:testnet:example' ), { recoveryKey: 'test', maxRecoveryAttempts: 2 } ); expect(recoveryWorkflow.refreshToken).toHaveBeenCalled(); }); }); ``` This comprehensive error handling guide provides patterns and strategies for building resilient applications with the ATP™ SDK. Always implement appropriate error handling for your specific use cases and test error scenarios thoroughly.