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
This commit is contained in:
kaitranntt
2025-12-20 18:44:29 -05:00
parent c9a936166b
commit 55464c5c5c
2 changed files with 59 additions and 6 deletions
+47 -5
View File
@@ -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';
}
}
@@ -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);
});
});