Merge pull request #162 from kaitranntt/kai/fix/remote-proxy-fetch-error

fix(cliproxy): improve remote proxy error messages
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2025-12-20 19:14:41 -05:00
commit d7ab635020
3 files changed
+114 -20

No files matched your search

+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);
});
});
+55 -14
View File
@@ -124,6 +124,11 @@ export function SettingsPage() {
const [proxySuccess, setProxySuccess] = useState(false);
const [testResult, setTestResult] = useState<RemoteProxyStatus | null>(null);
const [testing, setTesting] = useState(false);
// Proxy input state (lifted from ProxyContent to persist across tab switches)
const [proxyEditedHost, setProxyEditedHost] = useState<string | null>(null);
const [proxyEditedPort, setProxyEditedPort] = useState<string | null>(null);
const [proxyEditedAuthToken, setProxyEditedAuthToken] = useState<string | null>(null);
const [proxyEditedLocalPort, setProxyEditedLocalPort] = useState<string | null>(null);
// Load config and status on mount
useEffect(() => {
@@ -459,10 +464,13 @@ export function SettingsPage() {
}
};
const handleTestConnection = async () => {
if (!proxyConfig) return;
const { host, port, protocol, auth_token } = proxyConfig.remote;
const handleTestConnection = async (params: {
host: string;
port: string;
protocol: 'http' | 'https';
authToken: string;
}) => {
const { host, port, protocol, authToken } = params;
if (!host) {
setProxyError('Host is required');
return;
@@ -473,11 +481,13 @@ export function SettingsPage() {
setProxyError(null);
setTestResult(null);
// Parse port - empty string means use protocol default
const portNum = port ? parseInt(port, 10) : undefined;
const result = await api.cliproxyServer.test({
host,
port: port || undefined, // Empty/0 means use protocol default
port: portNum || undefined, // Empty/0 means use protocol default
protocol,
authToken: auth_token || undefined,
authToken: authToken || undefined,
});
setTestResult(result);
} catch (err) {
@@ -590,6 +600,14 @@ export function SettingsPage() {
handleTestConnection={handleTestConnection}
fetchCliproxyServerConfig={fetchCliproxyServerConfig}
fetchRawConfig={fetchRawConfig}
editedHost={proxyEditedHost}
setEditedHost={setProxyEditedHost}
editedPort={proxyEditedPort}
setEditedPort={setProxyEditedPort}
editedAuthToken={proxyEditedAuthToken}
setEditedAuthToken={setProxyEditedAuthToken}
editedLocalPort={proxyEditedLocalPort}
setEditedLocalPort={setProxyEditedLocalPort}
/>
)}
</div>
@@ -1292,9 +1310,23 @@ interface ProxyContentProps {
testResult: RemoteProxyStatus | null;
testing: boolean;
saveCliproxyServerConfig: (updates: Partial<CliproxyServerConfig>) => void;
handleTestConnection: () => void;
handleTestConnection: (params: {
host: string;
port: string;
protocol: 'http' | 'https';
authToken: string;
}) => void;
fetchCliproxyServerConfig: () => void;
fetchRawConfig: () => void;
// Lifted input state for persistence across tab switches
editedHost: string | null;
setEditedHost: (value: string | null) => void;
editedPort: string | null;
setEditedPort: (value: string | null) => void;
editedAuthToken: string | null;
setEditedAuthToken: (value: string | null) => void;
editedLocalPort: string | null;
setEditedLocalPort: (value: string | null) => void;
}
function ProxyContent({
@@ -1309,6 +1341,14 @@ function ProxyContent({
handleTestConnection,
fetchCliproxyServerConfig,
fetchRawConfig,
editedHost,
setEditedHost,
editedPort,
setEditedPort,
editedAuthToken,
setEditedAuthToken,
editedLocalPort,
setEditedLocalPort,
}: ProxyContentProps) {
// Default configs for fallback
const defaultRemote = {
@@ -1332,12 +1372,6 @@ function ProxyContent({
const authTokenInput = config?.remote.auth_token ?? '';
const localPortInput = (config?.local.port ?? 8317).toString();
// Track edited values separately
const [editedHost, setEditedHost] = useState<string | null>(null);
const [editedPort, setEditedPort] = useState<string | null>(null);
const [editedAuthToken, setEditedAuthToken] = useState<string | null>(null);
const [editedLocalPort, setEditedLocalPort] = useState<string | null>(null);
// Get display values (edited or from config)
const displayHost = editedHost ?? hostInput;
const displayPort = editedPort ?? portInput;
@@ -1559,7 +1593,14 @@ function ProxyContent({
{/* Test Connection */}
<div className="space-y-3 pt-2">
<Button
onClick={handleTestConnection}
onClick={() =>
handleTestConnection({
host: displayHost,
port: displayPort,
protocol: config?.remote.protocol || 'http',
authToken: displayAuthToken,
})
}
disabled={testing || !displayHost}
variant="outline"
className="w-full"