From 55464c5c5cb67b1cc4c2352130384dc6bc013f4c Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 20 Dec 2025 18:44:29 -0500 Subject: [PATCH] fix(cliproxy): improve remote proxy error messages - Add DNS_FAILED and NETWORK_UNREACHABLE error codes - Map "fetch failed" errors to NETWORK_UNREACHABLE for better UX - Extract nested error cause for more accurate error detection - Improve error messages with actionable hints Fixes #142 --- src/cliproxy/remote-proxy-client.ts | 52 +++++++++++++++++-- .../unit/cliproxy/remote-proxy-client.test.ts | 13 ++++- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/cliproxy/remote-proxy-client.ts b/src/cliproxy/remote-proxy-client.ts index 7bc3d3a3..5b0b771c 100644 --- a/src/cliproxy/remote-proxy-client.ts +++ b/src/cliproxy/remote-proxy-client.ts @@ -8,7 +8,13 @@ import * as https from 'https'; /** Error codes for remote proxy status */ -export type RemoteProxyErrorCode = 'CONNECTION_REFUSED' | 'TIMEOUT' | 'AUTH_FAILED' | 'UNKNOWN'; +export type RemoteProxyErrorCode = + | 'CONNECTION_REFUSED' + | 'TIMEOUT' + | 'AUTH_FAILED' + | 'DNS_FAILED' + | 'NETWORK_UNREACHABLE' + | 'UNKNOWN'; /** Status returned from remote proxy health check */ export interface RemoteProxyStatus { @@ -77,8 +83,8 @@ function buildProxyUrl( * Map error to RemoteProxyErrorCode * * Handles various error types including: - * - NodeJS.ErrnoException (ECONNREFUSED, ETIMEDOUT) - * - Fetch errors (AbortError, TypeError) + * - NodeJS.ErrnoException (ECONNREFUSED, ETIMEDOUT, ENOTFOUND, ENETUNREACH) + * - Fetch errors (AbortError, TypeError, "fetch failed") * - HTTP status codes (401, 403) */ function mapErrorToCode(error: Error, statusCode?: number): RemoteProxyErrorCode { @@ -87,6 +93,27 @@ function mapErrorToCode(error: Error, statusCode?: number): RemoteProxyErrorCode const rawCode = (error as NodeJS.ErrnoException).code; const code = typeof rawCode === 'string' ? rawCode.toLowerCase() : undefined; + // DNS resolution failed + if ( + code === 'enotfound' || + code === 'eai_again' || + message.includes('getaddrinfo') || + message.includes('dns') + ) { + return 'DNS_FAILED'; + } + + // Network unreachable / host unreachable + if ( + code === 'enetunreach' || + code === 'ehostunreach' || + code === 'enetdown' || + message.includes('network') || + message.includes('unreachable') + ) { + return 'NETWORK_UNREACHABLE'; + } + // Connection refused if (code === 'econnrefused' || message.includes('connection refused')) { return 'CONNECTION_REFUSED'; @@ -107,6 +134,17 @@ function mapErrorToCode(error: Error, statusCode?: number): RemoteProxyErrorCode return 'AUTH_FAILED'; } + // Generic "fetch failed" - try to extract cause + if (message.includes('fetch failed') || message.includes('failed to fetch')) { + // Check if there's a cause property (Node.js 18+) + const cause = (error as Error & { cause?: Error }).cause; + if (cause) { + return mapErrorToCode(cause); + } + // Likely network/DNS issue if no specific cause + return 'NETWORK_UNREACHABLE'; + } + return 'UNKNOWN'; } @@ -118,11 +156,15 @@ function getErrorMessage(errorCode: RemoteProxyErrorCode, rawError?: string): st case 'CONNECTION_REFUSED': return 'Connection refused - is the proxy running?'; case 'TIMEOUT': - return 'Connection timed out'; + return 'Connection timed out - server may be slow or unreachable'; case 'AUTH_FAILED': return 'Authentication failed - check auth token'; + case 'DNS_FAILED': + return 'DNS lookup failed - check hostname'; + case 'NETWORK_UNREACHABLE': + return 'Network unreachable - check if host is on same network'; default: - return rawError || 'Unknown error'; + return rawError || 'Connection failed'; } } diff --git a/tests/unit/cliproxy/remote-proxy-client.test.ts b/tests/unit/cliproxy/remote-proxy-client.test.ts index 3e7b95c8..536ff1d8 100644 --- a/tests/unit/cliproxy/remote-proxy-client.test.ts +++ b/tests/unit/cliproxy/remote-proxy-client.test.ts @@ -47,18 +47,29 @@ describe('remote-proxy-client', () => { describe('RemoteProxyErrorCode', () => { it('should define expected error codes', () => { - const validCodes = ['CONNECTION_REFUSED', 'TIMEOUT', 'AUTH_FAILED', 'UNKNOWN']; + const validCodes = [ + 'CONNECTION_REFUSED', + 'TIMEOUT', + 'AUTH_FAILED', + 'DNS_FAILED', + 'NETWORK_UNREACHABLE', + 'UNKNOWN', + ]; // Type-level test - ensure error codes can be used const status1: RemoteProxyStatus = { reachable: false, errorCode: 'CONNECTION_REFUSED' }; const status2: RemoteProxyStatus = { reachable: false, errorCode: 'TIMEOUT' }; const status3: RemoteProxyStatus = { reachable: false, errorCode: 'AUTH_FAILED' }; const status4: RemoteProxyStatus = { reachable: false, errorCode: 'UNKNOWN' }; + const status5: RemoteProxyStatus = { reachable: false, errorCode: 'DNS_FAILED' }; + const status6: RemoteProxyStatus = { reachable: false, errorCode: 'NETWORK_UNREACHABLE' }; expect(validCodes).toContain(status1.errorCode); expect(validCodes).toContain(status2.errorCode); expect(validCodes).toContain(status3.errorCode); expect(validCodes).toContain(status4.errorCode); + expect(validCodes).toContain(status5.errorCode); + expect(validCodes).toContain(status6.errorCode); }); });