feat: detect network issues

This commit is contained in:
Oleg Proskurin 2025-10-10 00:17:29 +07:00
parent 83303f8890
commit 6944e6b750
5 changed files with 397 additions and 3 deletions

View File

@ -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

View File

@ -10,6 +10,7 @@ import {
} from '../types/api'; } from '../types/api';
import { StorageFactory } from './StorageFactory'; import { StorageFactory } from './StorageFactory';
import { TTILogger, TTILogEntry } from './TTILogger'; import { TTILogger, TTILogEntry } from './TTILogger';
import { NetworkErrorDetector } from '../utils/NetworkErrorDetector';
export class ImageGenService { export class ImageGenService {
private ai: GoogleGenAI; private ai: GoogleGenAI;
@ -242,9 +243,16 @@ export class ImageGenService {
geminiParams, geminiParams,
}; };
} catch (error) { } catch (error) {
// Re-throw with clear error message // Enhanced error detection with network diagnostics
if (error instanceof Error) { 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'); throw new Error('Gemini AI generation failed: Unknown error');
} }

View File

@ -6,6 +6,7 @@ import {
import { getAgent } from './agents'; import { getAgent } from './agents';
import { validatePromptLength } from './validators'; import { validatePromptLength } from './validators';
import { EnhancementLogger } from './EnhancementLogger'; import { EnhancementLogger } from './EnhancementLogger';
import { NetworkErrorDetector } from '../../utils/NetworkErrorDetector';
export class PromptEnhancementService { export class PromptEnhancementService {
private apiKey: string; private apiKey: string;
@ -102,11 +103,28 @@ export class PromptEnhancementService {
console.log(`[${timestamp}] Enhancement completed successfully`); console.log(`[${timestamp}] Enhancement completed successfully`);
return result; return result;
} catch (error) { } 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); console.error(`[${timestamp}] Prompt enhancement failed:`, error);
return { return {
success: false, success: false,
originalPrompt: rawPrompt, originalPrompt: rawPrompt,
error: error instanceof Error ? error.message : 'Enhancement failed', error: 'Enhancement failed',
}; };
} }
} }

View File

@ -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<NetworkErrorResult> {
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<boolean> {
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;
}
}

View File

@ -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);