mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 00:17:09 +00:00
Merge pull request #162 from kaitranntt/kai/fix/remote-proxy-fetch-error
fix(cliproxy): improve remote proxy error messages
This commit is contained in:
3 files changed
+114
-20
No files matched your search
@@ -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
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user