feat: detect network issues
This commit is contained in:
parent
83303f8890
commit
6944e6b750
|
|
@ -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
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
Loading…
Reference in New Issue