diff --git a/apps/api-service/NETWORK_ERROR_DETECTION.md b/apps/api-service/NETWORK_ERROR_DETECTION.md new file mode 100644 index 0000000..3ca9047 --- /dev/null +++ b/apps/api-service/NETWORK_ERROR_DETECTION.md @@ -0,0 +1,134 @@ +# Network Error Detection + +## Overview + +This implementation adds intelligent network error detection to the Banatie API service. It follows best practices by **only checking connectivity when errors occur** (zero overhead on successful requests) and provides clear, actionable error messages to users. + +## Features + +### ✅ Lazy Validation +- Network checks **only trigger on failures** +- Zero performance overhead on successful requests +- Follows the fail-fast pattern + +### ✅ Error Classification +Automatically detects and classifies: +- **DNS Resolution Failures** (`ENOTFOUND`, `EAI_AGAIN`) +- **Connection Timeouts** (`ETIMEDOUT`) +- **Connection Refused** (`ECONNREFUSED`) +- **Connection Resets** (`ECONNRESET`, `ENETUNREACH`) +- **Generic Network Errors** (fetch failed, etc.) + +### ✅ Clear User Messages + +**Before:** +``` +Gemini AI generation failed: exception TypeError: fetch failed sending request +``` + +**After:** +``` +Network error: Unable to connect to Gemini API. Please check your internet connection and firewall settings. +``` + +### ✅ Detailed Logging + +Logs contain both user-friendly messages and technical details: +``` +[NETWORK ERROR - DNS] DNS resolution failed: Unable to resolve Gemini API hostname | Technical: Error code: ENOTFOUND +``` + +## Implementation + +### Core Utility + +**Location:** `src/utils/NetworkErrorDetector.ts` + +**Key Methods:** +- `classifyError(error, serviceName)` - Analyzes an error and determines if it's network-related +- `formatErrorForLogging(result)` - Formats errors for logging with technical details + +### Integration Points + +1. **ImageGenService** (`src/services/ImageGenService.ts`) + - Enhanced error handling in `generateImageWithAI()` method + - Provides clear network diagnostics when image generation fails + +2. **PromptEnhancementService** (`src/services/promptEnhancement/PromptEnhancementService.ts`) + - Enhanced error handling in `enhancePrompt()` method + - Helps users diagnose connectivity issues during prompt enhancement + +## How It Works + +### Normal Operation (No Overhead) +``` +User Request → Service → Gemini API → Success ✓ +``` +No network checks performed - zero overhead. + +### Error Scenario (Smart Detection) +``` +User Request → Service → Gemini API → Error ✗ + ↓ + NetworkErrorDetector.classifyError() + ↓ + 1. Check error code/message for network patterns + 2. If network-related: Quick DNS check (2s timeout) + 3. Return classification + user-friendly message +``` + +## Error Types + +| Error Type | Trigger | User Message | +|-----------|---------|--------------| +| `dns` | ENOTFOUND, EAI_AGAIN | DNS resolution failed: Unable to resolve Gemini API hostname | +| `timeout` | ETIMEDOUT | Connection timeout: Gemini API did not respond in time | +| `refused` | ECONNREFUSED | Connection refused: Service may be down or blocked by firewall | +| `reset` | ECONNRESET, ENETUNREACH | Connection lost: Network connection was interrupted | +| `connection` | General connectivity failure | Network connection failed: Unable to reach Gemini API | +| `unknown` | Network keywords detected | Network error: Unable to connect to Gemini API | + +## Testing + +Run the manual test to see error detection in action: + +```bash +npx tsx test-network-error-detector.ts +``` + +This demonstrates: +- DNS errors +- Timeout errors +- Fetch failures (actual error from your logs) +- Non-network errors (no false positives) + +## Example Error Logs + +### Before Implementation +``` +[2025-10-09T16:56:29.228Z] [fmfnz0zp7] Text-to-image generation completed: { + success: false, + error: 'Gemini AI generation failed: exception TypeError: fetch failed sending request' +} +``` + +### After Implementation +``` +[ImageGenService] [NETWORK ERROR - UNKNOWN] Network error: Unable to connect to Gemini API. Please check your internet connection and firewall settings. | Technical: exception TypeError: fetch failed sending request +``` + +## Benefits + +1. **Better UX**: Users get actionable error messages +2. **Faster Debugging**: Developers immediately know if it's a network issue +3. **Zero Overhead**: No performance impact on successful requests +4. **Production-Ready**: Follows industry best practices (AWS SDK, Stripe, Google Cloud) +5. **Comprehensive**: Detects all major network error types + +## Future Enhancements + +Potential improvements: +- Retry logic with exponential backoff for transient network errors +- Circuit breaker pattern for repeated failures +- Metrics/alerting for network error rates +- Health check endpoint with connectivity status diff --git a/apps/api-service/src/services/ImageGenService.ts b/apps/api-service/src/services/ImageGenService.ts index 02e3ca1..3c73a91 100644 --- a/apps/api-service/src/services/ImageGenService.ts +++ b/apps/api-service/src/services/ImageGenService.ts @@ -10,6 +10,7 @@ import { } from '../types/api'; import { StorageFactory } from './StorageFactory'; import { TTILogger, TTILogEntry } from './TTILogger'; +import { NetworkErrorDetector } from '../utils/NetworkErrorDetector'; export class ImageGenService { private ai: GoogleGenAI; @@ -242,9 +243,16 @@ export class ImageGenService { geminiParams, }; } catch (error) { - // Re-throw with clear error message + // Enhanced error detection with network diagnostics if (error instanceof Error) { - throw new Error(`Gemini AI generation failed: ${error.message}`); + // Classify the error and check for network issues (only on failure) + const errorAnalysis = await NetworkErrorDetector.classifyError(error, 'Gemini API'); + + // Log the detailed error for debugging + console.error(`[ImageGenService] ${NetworkErrorDetector.formatErrorForLogging(errorAnalysis)}`); + + // Throw user-friendly error message + throw new Error(errorAnalysis.userMessage); } throw new Error('Gemini AI generation failed: Unknown error'); } diff --git a/apps/api-service/src/services/promptEnhancement/PromptEnhancementService.ts b/apps/api-service/src/services/promptEnhancement/PromptEnhancementService.ts index 0cd6268..92bf20d 100644 --- a/apps/api-service/src/services/promptEnhancement/PromptEnhancementService.ts +++ b/apps/api-service/src/services/promptEnhancement/PromptEnhancementService.ts @@ -6,6 +6,7 @@ import { import { getAgent } from './agents'; import { validatePromptLength } from './validators'; import { EnhancementLogger } from './EnhancementLogger'; +import { NetworkErrorDetector } from '../../utils/NetworkErrorDetector'; export class PromptEnhancementService { private apiKey: string; @@ -102,11 +103,28 @@ export class PromptEnhancementService { console.log(`[${timestamp}] Enhancement completed successfully`); return result; } catch (error) { + // Enhanced error detection with network diagnostics + if (error instanceof Error) { + // Classify the error and check for network issues (only on failure) + const errorAnalysis = await NetworkErrorDetector.classifyError(error, 'Gemini API'); + + // Log the detailed error for debugging + console.error( + `[${timestamp}] [PromptEnhancementService] ${NetworkErrorDetector.formatErrorForLogging(errorAnalysis)}`, + ); + + return { + success: false, + originalPrompt: rawPrompt, + error: errorAnalysis.userMessage, + }; + } + console.error(`[${timestamp}] Prompt enhancement failed:`, error); return { success: false, originalPrompt: rawPrompt, - error: error instanceof Error ? error.message : 'Enhancement failed', + error: 'Enhancement failed', }; } } diff --git a/apps/api-service/src/utils/NetworkErrorDetector.ts b/apps/api-service/src/utils/NetworkErrorDetector.ts new file mode 100644 index 0000000..4dfb1cf --- /dev/null +++ b/apps/api-service/src/utils/NetworkErrorDetector.ts @@ -0,0 +1,174 @@ +import * as dns from 'dns'; +import { promisify } from 'util'; + +const dnsLookup = promisify(dns.lookup); + +export interface NetworkErrorResult { + isNetworkError: boolean; + errorType?: 'connection' | 'dns' | 'timeout' | 'refused' | 'reset' | 'unknown'; + userMessage: string; + technicalDetails?: string; +} + +export class NetworkErrorDetector { + private static NETWORK_ERROR_CODES = [ + 'ENOTFOUND', // DNS resolution failed + 'ETIMEDOUT', // Connection timeout + 'ECONNREFUSED', // Connection refused + 'ECONNRESET', // Connection reset + 'ENETUNREACH', // Network unreachable + 'EHOSTUNREACH', // Host unreachable + 'EAI_AGAIN', // DNS temporary failure + ]; + + /** + * Classify an error and determine if it's network-related + * This is called ONLY when an error occurs (lazy validation) + */ + static async classifyError(error: Error, serviceName: string = 'API'): Promise { + const errorMessage = error.message.toLowerCase(); + const errorCode = (error as any).code?.toUpperCase(); + + // Check if it's a known network error + if (this.isNetworkErrorCode(errorCode) || this.containsNetworkKeywords(errorMessage)) { + // Perform a quick connectivity check to confirm + const isOnline = await this.quickConnectivityCheck(); + + if (!isOnline) { + return { + isNetworkError: true, + errorType: 'connection', + userMessage: `Network connection failed: Unable to reach ${serviceName}. Please check your internet connection.`, + technicalDetails: error.message, + }; + } + + // Network is up, but specific error occurred + const specificError = this.getSpecificNetworkError(errorCode, errorMessage, serviceName); + return specificError; + } + + // Not a network error - return original error + return { + isNetworkError: false, + userMessage: error.message, + technicalDetails: error.message, + }; + } + + /** + * Quick connectivity check - only called when an error occurs + * Tests DNS resolution to a reliable endpoint (Google DNS) + */ + private static async quickConnectivityCheck(): Promise { + try { + // Use Google's public DNS as a reliable test endpoint + await Promise.race([ + dnsLookup('dns.google'), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000)), + ]); + return true; + } catch { + return false; + } + } + + /** + * Check if error code is a known network error + */ + private static isNetworkErrorCode(code?: string): boolean { + if (!code) return false; + return this.NETWORK_ERROR_CODES.includes(code); + } + + /** + * Check if error message contains network-related keywords + */ + private static containsNetworkKeywords(message: string): boolean { + const keywords = [ + 'fetch failed', + 'network', + 'connection', + 'timeout', + 'dns', + 'unreachable', + 'refused', + 'reset', + 'enotfound', + 'etimedout', + 'econnrefused', + 'econnreset', + ]; + + return keywords.some((keyword) => message.includes(keyword)); + } + + /** + * Get specific error details based on error code/message + */ + private static getSpecificNetworkError( + code: string | undefined, + message: string, + serviceName: string, + ): NetworkErrorResult { + // DNS resolution failures + if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || message.includes('dns')) { + return { + isNetworkError: true, + errorType: 'dns', + userMessage: `DNS resolution failed: Unable to resolve ${serviceName} hostname. Check your DNS settings or internet connection.`, + technicalDetails: `Error code: ${code || 'unknown'}`, + }; + } + + // Connection timeout + if (code === 'ETIMEDOUT' || message.includes('timeout')) { + return { + isNetworkError: true, + errorType: 'timeout', + userMessage: `Connection timeout: ${serviceName} did not respond in time. This may be due to slow internet or firewall blocking.`, + technicalDetails: `Error code: ${code || 'timeout'}`, + }; + } + + // Connection refused + if (code === 'ECONNREFUSED' || message.includes('refused')) { + return { + isNetworkError: true, + errorType: 'refused', + userMessage: `Connection refused: ${serviceName} is not accepting connections. Service may be down or blocked by firewall.`, + technicalDetails: `Error code: ${code || 'refused'}`, + }; + } + + // Connection reset + if (code === 'ECONNRESET' || code === 'ENETUNREACH' || code === 'EHOSTUNREACH') { + return { + isNetworkError: true, + errorType: 'reset', + userMessage: `Connection lost: Network connection to ${serviceName} was interrupted. Check your internet stability.`, + technicalDetails: `Error code: ${code || 'reset'}`, + }; + } + + // Generic network error + return { + isNetworkError: true, + errorType: 'unknown', + userMessage: `Network error: Unable to connect to ${serviceName}. Please check your internet connection and firewall settings.`, + technicalDetails: message, + }; + } + + /** + * Format error for logging with both user-friendly and technical details + */ + static formatErrorForLogging(result: NetworkErrorResult): string { + if (result.isNetworkError) { + return `[NETWORK ERROR - ${result.errorType?.toUpperCase()}] ${result.userMessage}${ + result.technicalDetails ? ` | Technical: ${result.technicalDetails}` : '' + }`; + } + return result.userMessage; + } +} diff --git a/apps/api-service/test-network-error-detector.ts b/apps/api-service/test-network-error-detector.ts new file mode 100644 index 0000000..2f1cc08 --- /dev/null +++ b/apps/api-service/test-network-error-detector.ts @@ -0,0 +1,60 @@ +/** + * Manual test script for NetworkErrorDetector + * Run with: npx tsx test-network-error-detector.ts + */ + +import { NetworkErrorDetector } from './src/utils/NetworkErrorDetector'; + +async function testNetworkErrorDetector() { + console.log('🧪 Testing NetworkErrorDetector\n'); + + // Test 1: DNS Error + console.log('Test 1: DNS Resolution Error (ENOTFOUND)'); + const dnsError = new Error('getaddrinfo ENOTFOUND api.example.com'); + (dnsError as any).code = 'ENOTFOUND'; + const dnsResult = await NetworkErrorDetector.classifyError(dnsError, 'Gemini API'); + console.log(' Is Network Error:', dnsResult.isNetworkError); + console.log(' Error Type:', dnsResult.errorType); + console.log(' User Message:', dnsResult.userMessage); + console.log(' Formatted Log:', NetworkErrorDetector.formatErrorForLogging(dnsResult)); + console.log(''); + + // Test 2: Timeout Error + console.log('Test 2: Connection Timeout (ETIMEDOUT)'); + const timeoutError = new Error('connect ETIMEDOUT 142.250.185.10:443'); + (timeoutError as any).code = 'ETIMEDOUT'; + const timeoutResult = await NetworkErrorDetector.classifyError(timeoutError, 'Gemini API'); + console.log(' Is Network Error:', timeoutResult.isNetworkError); + console.log(' Error Type:', timeoutResult.errorType); + console.log(' User Message:', timeoutResult.userMessage); + console.log(' Formatted Log:', NetworkErrorDetector.formatErrorForLogging(timeoutResult)); + console.log(''); + + // Test 3: Fetch Failed (actual error from logs) + console.log('Test 3: Fetch Failed Error (from your logs)'); + const fetchError = new Error('exception TypeError: fetch failed sending request'); + const fetchResult = await NetworkErrorDetector.classifyError(fetchError, 'Gemini API'); + console.log(' Is Network Error:', fetchResult.isNetworkError); + console.log(' Error Type:', fetchResult.errorType); + console.log(' User Message:', fetchResult.userMessage); + console.log(' Formatted Log:', NetworkErrorDetector.formatErrorForLogging(fetchResult)); + console.log(''); + + // Test 4: Non-network Error + console.log('Test 4: Non-Network Error (Invalid API Key)'); + const apiError = new Error('Invalid API key provided'); + const apiResult = await NetworkErrorDetector.classifyError(apiError, 'Gemini API'); + console.log(' Is Network Error:', apiResult.isNetworkError); + console.log(' User Message:', apiResult.userMessage); + console.log(' Formatted Log:', NetworkErrorDetector.formatErrorForLogging(apiResult)); + console.log(''); + + console.log('✅ Tests completed!'); + console.log('\n📝 Summary:'); + console.log(' - Network errors are detected and classified'); + console.log(' - User-friendly messages are provided'); + console.log(' - Technical details are preserved for logging'); + console.log(' - No overhead on successful requests (only runs on failures)'); +} + +testNetworkErrorDetector().catch(console.error);