| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """ |
| Network error classification and user-friendly message formatting. |
| |
| This module provides a centralized system for classifying network errors |
| and converting them into actionable, user-friendly messages with troubleshooting steps. |
| |
| Architecture: |
| - ErrorCategory: Enum of all possible network error types |
| - NetworkErrorInfo: Structured information about an error |
| - classify_network_error(): Analyzes exceptions and returns NetworkErrorInfo |
| - format_error_for_user(): Formats errors for API responses (OpenAI/Anthropic) |
| """ |
|
|
| import socket |
| from dataclasses import dataclass |
| from enum import Enum |
| from typing import List, Dict, Any, Optional |
|
|
| import httpx |
| from loguru import logger |
|
|
|
|
| class ErrorCategory(str, Enum): |
| """ |
| Categories of network errors. |
| |
| Each category represents a distinct type of network failure |
| with specific troubleshooting steps. |
| """ |
| DNS_RESOLUTION = "dns_resolution" |
| CONNECTION_REFUSED = "connection_refused" |
| CONNECTION_RESET = "connection_reset" |
| NETWORK_UNREACHABLE = "network_unreachable" |
| TIMEOUT_CONNECT = "timeout_connect" |
| TIMEOUT_READ = "timeout_read" |
| SSL_ERROR = "ssl_error" |
| PROXY_ERROR = "proxy_error" |
| TOO_MANY_REDIRECTS = "too_many_redirects" |
| UNKNOWN = "unknown" |
|
|
|
|
| @dataclass |
| class NetworkErrorInfo: |
| """ |
| Structured information about a network error. |
| |
| Attributes: |
| category: Error category for classification |
| user_message: Clear, non-technical message for end users |
| troubleshooting_steps: List of actionable steps to resolve the issue |
| technical_details: Technical error details for logging and debugging |
| is_retryable: Whether retrying the request might succeed |
| suggested_http_code: Appropriate HTTP status code (502, 504, etc.) |
| """ |
| category: ErrorCategory |
| user_message: str |
| troubleshooting_steps: List[str] |
| technical_details: str |
| is_retryable: bool |
| suggested_http_code: int |
|
|
|
|
| def classify_network_error(error: Exception) -> NetworkErrorInfo: |
| """ |
| Classifies a network error and returns structured information. |
| |
| Analyzes the exception type, error message, and underlying cause |
| to determine the specific type of network failure and provide |
| appropriate user-facing messages and troubleshooting steps. |
| |
| Args: |
| error: The exception that occurred (typically httpx.RequestError) |
| |
| Returns: |
| NetworkErrorInfo with classification and user-friendly details |
| |
| Example: |
| >>> try: |
| ... response = await client.get("https://example.com") |
| ... except httpx.RequestError as e: |
| ... error_info = classify_network_error(e) |
| ... logger.error(f"[{error_info.category}] {error_info.user_message}") |
| """ |
| error_type = type(error).__name__ |
| error_str = str(error) |
| |
| |
| technical_details = f"{error_type}: {error_str}" |
| |
| |
| if isinstance(error, httpx.ConnectError): |
| return _classify_connect_error(error, technical_details) |
| |
| |
| if isinstance(error, httpx.TimeoutException): |
| return _classify_timeout_error(error, technical_details) |
| |
| |
| if isinstance(error, httpx.TooManyRedirects): |
| return NetworkErrorInfo( |
| category=ErrorCategory.TOO_MANY_REDIRECTS, |
| user_message="Too many redirects - the server is redirecting in a loop.", |
| troubleshooting_steps=[ |
| "This is likely a server-side configuration issue", |
| "Try accessing the service directly without the gateway", |
| "Contact the service provider if the issue persists" |
| ], |
| technical_details=technical_details, |
| is_retryable=False, |
| suggested_http_code=502 |
| ) |
| |
| |
| if isinstance(error, httpx.ProxyError): |
| return NetworkErrorInfo( |
| category=ErrorCategory.PROXY_ERROR, |
| user_message="Proxy connection failed - cannot connect through the configured proxy.", |
| troubleshooting_steps=[ |
| "Check proxy configuration (HTTP_PROXY, HTTPS_PROXY environment variables)", |
| "Verify proxy server is accessible", |
| "Try disabling proxy temporarily", |
| "Check proxy authentication credentials if required" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=502 |
| ) |
| |
| |
| if isinstance(error, httpx.RequestError): |
| return NetworkErrorInfo( |
| category=ErrorCategory.UNKNOWN, |
| user_message="Network request failed due to an unexpected error.", |
| troubleshooting_steps=[ |
| "Check your internet connection", |
| "Verify firewall/antivirus settings", |
| "Try again in a few moments", |
| "Check the debug logs for more details" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=502 |
| ) |
| |
| |
| return NetworkErrorInfo( |
| category=ErrorCategory.UNKNOWN, |
| user_message="An unexpected error occurred.", |
| troubleshooting_steps=[ |
| "Check the debug logs for details", |
| "Try again in a few moments", |
| "Report this issue if it persists" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=500 |
| ) |
|
|
|
|
| def _classify_connect_error(error: httpx.ConnectError, technical_details: str) -> NetworkErrorInfo: |
| """ |
| Classifies httpx.ConnectError into specific subcategories. |
| |
| Args: |
| error: The ConnectError exception |
| technical_details: Technical error string for logging |
| |
| Returns: |
| NetworkErrorInfo with specific classification |
| """ |
| error_str = str(error) |
| |
| |
| cause = error.__cause__ |
| |
| |
| if cause and isinstance(cause, socket.gaierror): |
| |
| |
| |
| |
| errno = getattr(cause, 'errno', None) |
| |
| return NetworkErrorInfo( |
| category=ErrorCategory.DNS_RESOLUTION, |
| user_message="DNS resolution failed - cannot resolve the provider's domain name.", |
| troubleshooting_steps=[ |
| "Check your internet connection", |
| "Try changing DNS servers to Google DNS (8.8.8.8, 8.8.4.4) or Cloudflare (1.1.1.1, 1.0.0.1)", |
| "Temporarily disable VPN if you're using one", |
| "Check if firewall/antivirus is blocking DNS requests", |
| "Verify the domain name is correct and the service is operational" |
| ], |
| technical_details=f"{technical_details} (errno: {errno})", |
| is_retryable=True, |
| suggested_http_code=502 |
| ) |
| |
| |
| if "Connection refused" in error_str or "ECONNREFUSED" in error_str: |
| return NetworkErrorInfo( |
| category=ErrorCategory.CONNECTION_REFUSED, |
| user_message="Connection refused - the server is not accepting connections.", |
| troubleshooting_steps=[ |
| "The service may be temporarily down", |
| "Check if the service is running and accessible", |
| "Verify firewall is not blocking the connection", |
| "Try again in a few moments" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=502 |
| ) |
| |
| |
| if "Connection reset" in error_str or "ECONNRESET" in error_str: |
| return NetworkErrorInfo( |
| category=ErrorCategory.CONNECTION_RESET, |
| user_message="Connection reset - the server closed the connection unexpectedly.", |
| troubleshooting_steps=[ |
| "This is usually a temporary server issue", |
| "Try again in a few moments", |
| "Check if VPN/proxy is interfering with the connection", |
| "Verify network stability" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=502 |
| ) |
| |
| |
| if "Network is unreachable" in error_str or "No route to host" in error_str or "ENETUNREACH" in error_str: |
| return NetworkErrorInfo( |
| category=ErrorCategory.NETWORK_UNREACHABLE, |
| user_message="Network unreachable - cannot reach the server's network.", |
| troubleshooting_steps=[ |
| "Check your internet connection", |
| "Verify network adapter is enabled and working", |
| "Check routing table if using VPN", |
| "Try disabling VPN temporarily", |
| "Restart network adapter or router" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=502 |
| ) |
| |
| |
| if "SSL" in error_str or "TLS" in error_str or "certificate" in error_str.lower(): |
| return NetworkErrorInfo( |
| category=ErrorCategory.SSL_ERROR, |
| user_message="SSL/TLS error - secure connection could not be established.", |
| troubleshooting_steps=[ |
| "Check system date and time (incorrect time causes SSL errors)", |
| "Update SSL certificates on your system", |
| "Check if antivirus/firewall is intercepting HTTPS traffic", |
| "Verify the server's SSL certificate is valid" |
| ], |
| technical_details=technical_details, |
| is_retryable=False, |
| suggested_http_code=502 |
| ) |
| |
| |
| return NetworkErrorInfo( |
| category=ErrorCategory.UNKNOWN, |
| user_message="Connection failed - unable to establish connection to the server.", |
| troubleshooting_steps=[ |
| "Check your internet connection", |
| "Verify firewall/antivirus settings", |
| "Try disabling VPN temporarily", |
| "Check if the service is accessible from other devices" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=502 |
| ) |
|
|
|
|
| def _classify_timeout_error(error: httpx.TimeoutException, technical_details: str) -> NetworkErrorInfo: |
| """ |
| Classifies httpx.TimeoutException into specific subcategories. |
| |
| Args: |
| error: The TimeoutException |
| technical_details: Technical error string for logging |
| |
| Returns: |
| NetworkErrorInfo with specific classification |
| """ |
| |
| if isinstance(error, httpx.ConnectTimeout): |
| return NetworkErrorInfo( |
| category=ErrorCategory.TIMEOUT_CONNECT, |
| user_message="Connection timeout - server did not respond to connection attempt.", |
| troubleshooting_steps=[ |
| "Check your internet connection speed", |
| "The server may be overloaded or slow to respond", |
| "Try again in a few moments", |
| "Check if firewall is delaying connections" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=504 |
| ) |
| |
| |
| if isinstance(error, httpx.ReadTimeout): |
| return NetworkErrorInfo( |
| category=ErrorCategory.TIMEOUT_READ, |
| user_message="Read timeout - server stopped responding during data transfer.", |
| troubleshooting_steps=[ |
| "The server may be processing a complex request", |
| "Check your internet connection stability", |
| "Try again with a simpler request", |
| "The service may be experiencing high load" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=504 |
| ) |
| |
| |
| return NetworkErrorInfo( |
| category=ErrorCategory.TIMEOUT_READ, |
| user_message="Request timeout - operation took too long to complete.", |
| troubleshooting_steps=[ |
| "Check your internet connection", |
| "The server may be slow or overloaded", |
| "Try again in a few moments" |
| ], |
| technical_details=technical_details, |
| is_retryable=True, |
| suggested_http_code=504 |
| ) |
|
|
|
|
| def format_error_for_user( |
| error_info: NetworkErrorInfo, |
| format_type: str = "openai", |
| include_troubleshooting: bool = True |
| ) -> Dict[str, Any]: |
| """ |
| Formats NetworkErrorInfo for API response. |
| |
| Converts structured error information into the appropriate format |
| for OpenAI or Anthropic API responses. |
| |
| Args: |
| error_info: The classified error information |
| format_type: "openai" or "anthropic" format |
| include_troubleshooting: Whether to include troubleshooting steps |
| |
| Returns: |
| Dictionary formatted for API response |
| |
| Example: |
| >>> error_info = classify_network_error(exception) |
| >>> response = format_error_for_user(error_info, format_type="openai") |
| >>> return JSONResponse(status_code=502, content=response) |
| """ |
| |
| message = error_info.user_message |
| |
| if include_troubleshooting and error_info.troubleshooting_steps: |
| message += "\n\nTroubleshooting steps:\n" |
| for i, step in enumerate(error_info.troubleshooting_steps, 1): |
| message += f"{i}. {step}\n" |
| |
| |
| if format_type == "openai": |
| return { |
| "error": { |
| "message": message.strip(), |
| "type": "connectivity_error", |
| "code": error_info.category.value, |
| "param": None |
| } |
| } |
| |
| |
| elif format_type == "anthropic": |
| return { |
| "type": "error", |
| "error": { |
| "type": "connectivity_error", |
| "message": message.strip() |
| } |
| } |
| |
| |
| else: |
| return { |
| "error": { |
| "type": "connectivity_error", |
| "category": error_info.category.value, |
| "message": message.strip(), |
| "technical_details": error_info.technical_details |
| } |
| } |
|
|
|
|
| def get_short_error_message(error_info: NetworkErrorInfo) -> str: |
| """ |
| Returns a short, single-line error message for logging. |
| |
| Args: |
| error_info: The classified error information |
| |
| Returns: |
| Short error message suitable for log files |
| |
| Example: |
| >>> error_info = classify_network_error(exception) |
| >>> logger.warning(get_short_error_message(error_info)) |
| """ |
| return error_info.user_message |
|
|