mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
fix(remote-proxy): fix TypeError and make port optional with protocol defaults
- Fix TypeError: error.code?.toLowerCase - add type guard for non-string error codes - Make remote proxy port optional - defaults to 443 (HTTPS) or 80 (HTTP) - Smart URL building - omits port when using protocol default - UI improvements - show default port hint, allow empty port field - Better validation - only host is required, port uses smart defaults Closes #142
This commit is contained in:
@@ -490,9 +490,13 @@ export function ensureProviderSettings(provider: CLIProxyProvider): void {
|
|||||||
*/
|
*/
|
||||||
export function getRemoteEnvVars(
|
export function getRemoteEnvVars(
|
||||||
provider: CLIProxyProvider,
|
provider: CLIProxyProvider,
|
||||||
remoteConfig: { host: string; port: number; protocol: 'http' | 'https'; authToken?: string }
|
remoteConfig: { host: string; port?: number; protocol: 'http' | 'https'; authToken?: string }
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const baseUrl = `${remoteConfig.protocol}://${remoteConfig.host}:${remoteConfig.port}/api/provider/${provider}`;
|
// Build URL with smart port handling - omit if using protocol default
|
||||||
|
const defaultPort = remoteConfig.protocol === 'https' ? 443 : 80;
|
||||||
|
const effectivePort = remoteConfig.port ?? defaultPort;
|
||||||
|
const portSuffix = effectivePort === defaultPort ? '' : `:${effectivePort}`;
|
||||||
|
const baseUrl = `${remoteConfig.protocol}://${remoteConfig.host}${portSuffix}/api/provider/${provider}`;
|
||||||
const models = getModelMapping(provider);
|
const models = getModelMapping(provider);
|
||||||
|
|
||||||
// Get global env vars (DISABLE_TELEMETRY, etc.)
|
// Get global env vars (DISABLE_TELEMETRY, etc.)
|
||||||
|
|||||||
@@ -26,8 +26,13 @@ export interface RemoteProxyStatus {
|
|||||||
export interface RemoteProxyClientConfig {
|
export interface RemoteProxyClientConfig {
|
||||||
/** Remote proxy host (IP or hostname) */
|
/** Remote proxy host (IP or hostname) */
|
||||||
host: string;
|
host: string;
|
||||||
/** Remote proxy port */
|
/**
|
||||||
port: number;
|
* Remote proxy port.
|
||||||
|
* Optional - defaults based on protocol:
|
||||||
|
* - HTTPS: 443
|
||||||
|
* - HTTP: 80
|
||||||
|
*/
|
||||||
|
port?: number;
|
||||||
/** Protocol to use (http or https) */
|
/** Protocol to use (http or https) */
|
||||||
protocol: 'http' | 'https';
|
protocol: 'http' | 'https';
|
||||||
/** Optional auth token for Authorization header */
|
/** Optional auth token for Authorization header */
|
||||||
@@ -41,12 +46,45 @@ export interface RemoteProxyClientConfig {
|
|||||||
/** Default timeout for remote proxy requests (aggressive for CLI UX) */
|
/** Default timeout for remote proxy requests (aggressive for CLI UX) */
|
||||||
const DEFAULT_TIMEOUT_MS = 2000;
|
const DEFAULT_TIMEOUT_MS = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default port for protocol
|
||||||
|
*/
|
||||||
|
function getDefaultPort(protocol: 'http' | 'https'): number {
|
||||||
|
return protocol === 'https' ? 443 : 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build URL for remote proxy, intelligently omitting default ports
|
||||||
|
*/
|
||||||
|
function buildProxyUrl(
|
||||||
|
host: string,
|
||||||
|
port: number | undefined,
|
||||||
|
protocol: 'http' | 'https',
|
||||||
|
path: string
|
||||||
|
): string {
|
||||||
|
const defaultPort = getDefaultPort(protocol);
|
||||||
|
const effectivePort = port ?? defaultPort;
|
||||||
|
|
||||||
|
// Omit port from URL if it matches the default for the protocol
|
||||||
|
if (effectivePort === defaultPort) {
|
||||||
|
return `${protocol}://${host}${path}`;
|
||||||
|
}
|
||||||
|
return `${protocol}://${host}:${effectivePort}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map error to RemoteProxyErrorCode
|
* Map error to RemoteProxyErrorCode
|
||||||
|
*
|
||||||
|
* Handles various error types including:
|
||||||
|
* - NodeJS.ErrnoException (ECONNREFUSED, ETIMEDOUT)
|
||||||
|
* - Fetch errors (AbortError, TypeError)
|
||||||
|
* - HTTP status codes (401, 403)
|
||||||
*/
|
*/
|
||||||
function mapErrorToCode(error: Error, statusCode?: number): RemoteProxyErrorCode {
|
function mapErrorToCode(error: Error, statusCode?: number): RemoteProxyErrorCode {
|
||||||
const message = error.message.toLowerCase();
|
const message = error.message.toLowerCase();
|
||||||
const code = (error as NodeJS.ErrnoException).code?.toLowerCase();
|
// Handle error.code safely - it may be string, number, or undefined
|
||||||
|
const rawCode = (error as NodeJS.ErrnoException).code;
|
||||||
|
const code = typeof rawCode === 'string' ? rawCode.toLowerCase() : undefined;
|
||||||
|
|
||||||
// Connection refused
|
// Connection refused
|
||||||
if (code === 'econnrefused' || message.includes('connection refused')) {
|
if (code === 'econnrefused' || message.includes('connection refused')) {
|
||||||
@@ -110,7 +148,17 @@ export async function checkRemoteProxy(
|
|||||||
const { host, port, protocol, authToken, allowSelfSigned = false } = config;
|
const { host, port, protocol, authToken, allowSelfSigned = false } = config;
|
||||||
const timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
const timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
||||||
|
|
||||||
const url = `${protocol}://${host}:${port}/health`;
|
// Validate host is provided
|
||||||
|
if (!host || host.trim() === '') {
|
||||||
|
return {
|
||||||
|
reachable: false,
|
||||||
|
error: 'Host is required',
|
||||||
|
errorCode: 'UNKNOWN',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use smart URL building - omit port if it's the default for the protocol
|
||||||
|
const url = buildProxyUrl(host, port, protocol, '/health');
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
|||||||
enabled:
|
enabled:
|
||||||
partial.cliproxy_server?.remote?.enabled ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.enabled,
|
partial.cliproxy_server?.remote?.enabled ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.enabled,
|
||||||
host: partial.cliproxy_server?.remote?.host ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.host,
|
host: partial.cliproxy_server?.remote?.host ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.host,
|
||||||
port: partial.cliproxy_server?.remote?.port ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.port,
|
// Port is optional - undefined means use protocol default (443/80)
|
||||||
|
port: partial.cliproxy_server?.remote?.port,
|
||||||
protocol:
|
protocol:
|
||||||
partial.cliproxy_server?.remote?.protocol ??
|
partial.cliproxy_server?.remote?.protocol ??
|
||||||
DEFAULT_CLIPROXY_SERVER_CONFIG.remote.protocol,
|
DEFAULT_CLIPROXY_SERVER_CONFIG.remote.protocol,
|
||||||
|
|||||||
@@ -195,8 +195,14 @@ export interface ProxyRemoteConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/** Remote proxy hostname or IP (empty = not configured) */
|
/** Remote proxy hostname or IP (empty = not configured) */
|
||||||
host: string;
|
host: string;
|
||||||
/** Remote proxy port (default: 8317) */
|
/**
|
||||||
port: number;
|
* Remote proxy port.
|
||||||
|
* Optional - defaults based on protocol:
|
||||||
|
* - HTTPS: 443
|
||||||
|
* - HTTP: 80
|
||||||
|
* When empty/undefined, uses protocol default.
|
||||||
|
*/
|
||||||
|
port?: number;
|
||||||
/** Protocol for remote connection */
|
/** Protocol for remote connection */
|
||||||
protocol: 'http' | 'https';
|
protocol: 'http' | 'https';
|
||||||
/** Auth token for remote proxy (optional, sent as header) */
|
/** Auth token for remote proxy (optional, sent as header) */
|
||||||
@@ -345,12 +351,13 @@ export const DEFAULT_COPILOT_CONFIG: CopilotConfig = {
|
|||||||
/**
|
/**
|
||||||
* Default CLIProxy server configuration.
|
* Default CLIProxy server configuration.
|
||||||
* Local mode by default - remote must be explicitly enabled.
|
* Local mode by default - remote must be explicitly enabled.
|
||||||
|
* Port is optional for remote - defaults based on protocol.
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = {
|
export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = {
|
||||||
remote: {
|
remote: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
host: '',
|
host: '',
|
||||||
port: 8317,
|
// port is intentionally omitted - will use protocol default (443 for HTTPS, 80 for HTTP)
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
auth_token: '',
|
auth_token: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,14 +72,20 @@ router.post('/test', async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { host, port, protocol, authToken, allowSelfSigned } = req.body;
|
const { host, port, protocol, authToken, allowSelfSigned } = req.body;
|
||||||
|
|
||||||
if (!host || !port) {
|
// Host is required, port is optional (uses protocol defaults)
|
||||||
res.status(400).json({ error: 'Host and port are required' });
|
if (!host) {
|
||||||
|
res.status(400).json({ error: 'Host is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse port - treat empty string, 0, null as "use default"
|
||||||
|
const parsedPort = port && port !== '' ? parseInt(String(port), 10) : undefined;
|
||||||
|
const effectivePort =
|
||||||
|
parsedPort && !isNaN(parsedPort) && parsedPort > 0 ? parsedPort : undefined;
|
||||||
|
|
||||||
const status = await testConnection({
|
const status = await testConnection({
|
||||||
host,
|
host,
|
||||||
port: typeof port === 'number' ? port : parseInt(port, 10),
|
port: effectivePort,
|
||||||
protocol: protocol || 'http',
|
protocol: protocol || 'http',
|
||||||
authToken,
|
authToken,
|
||||||
allowSelfSigned: allowSelfSigned || false,
|
allowSelfSigned: allowSelfSigned || false,
|
||||||
|
|||||||
@@ -166,7 +166,8 @@ export interface RemoteProxyStatus {
|
|||||||
export interface ProxyRemoteConfig {
|
export interface ProxyRemoteConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
/** Port is optional - uses protocol default (443 for HTTPS, 80 for HTTP) */
|
||||||
|
port?: number;
|
||||||
protocol: 'http' | 'https';
|
protocol: 'http' | 'https';
|
||||||
auth_token: string;
|
auth_token: string;
|
||||||
}
|
}
|
||||||
@@ -402,7 +403,8 @@ export const api = {
|
|||||||
/** Test remote proxy connection */
|
/** Test remote proxy connection */
|
||||||
test: (params: {
|
test: (params: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
/** Port is optional - uses protocol default (443 for HTTPS, 80 for HTTP) */
|
||||||
|
port?: number;
|
||||||
protocol: 'http' | 'https';
|
protocol: 'http' | 'https';
|
||||||
authToken?: string;
|
authToken?: string;
|
||||||
allowSelfSigned?: boolean;
|
allowSelfSigned?: boolean;
|
||||||
|
|||||||
+28
-14
@@ -463,8 +463,8 @@ export function SettingsPage() {
|
|||||||
if (!proxyConfig) return;
|
if (!proxyConfig) return;
|
||||||
|
|
||||||
const { host, port, protocol, auth_token } = proxyConfig.remote;
|
const { host, port, protocol, auth_token } = proxyConfig.remote;
|
||||||
if (!host || !port) {
|
if (!host) {
|
||||||
setProxyError('Host and port are required');
|
setProxyError('Host is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +475,7 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
const result = await api.cliproxyServer.test({
|
const result = await api.cliproxyServer.test({
|
||||||
host,
|
host,
|
||||||
port,
|
port: port || undefined, // Empty/0 means use protocol default
|
||||||
protocol,
|
protocol,
|
||||||
authToken: auth_token || undefined,
|
authToken: auth_token || undefined,
|
||||||
});
|
});
|
||||||
@@ -1310,20 +1310,24 @@ function ProxyContent({
|
|||||||
fetchCliproxyServerConfig,
|
fetchCliproxyServerConfig,
|
||||||
fetchRawConfig,
|
fetchRawConfig,
|
||||||
}: ProxyContentProps) {
|
}: ProxyContentProps) {
|
||||||
// Memoized default config to avoid recreation
|
// Default configs for fallback
|
||||||
const defaultRemote = {
|
const defaultRemote = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
host: '',
|
host: '',
|
||||||
port: 8317,
|
port: undefined as number | undefined,
|
||||||
protocol: 'http' as const,
|
protocol: 'http' as const,
|
||||||
auth_token: '',
|
auth_token: '',
|
||||||
};
|
};
|
||||||
const defaultFallback = { enabled: true, auto_start: true };
|
const defaultFallback = { enabled: true, auto_start: true };
|
||||||
const defaultLocal = { port: 8317, auto_start: true };
|
const defaultLocal = { port: 8317, auto_start: true };
|
||||||
|
|
||||||
|
// Helper to get default port based on protocol
|
||||||
|
const getDefaultPort = (protocol: 'http' | 'https') => (protocol === 'https' ? 443 : 80);
|
||||||
|
|
||||||
// Sync local state with config (using refs to avoid lint warnings)
|
// Sync local state with config (using refs to avoid lint warnings)
|
||||||
const hostInput = config?.remote.host ?? '';
|
const hostInput = config?.remote.host ?? '';
|
||||||
const portInput = (config?.remote.port ?? 8317).toString();
|
// Show empty string for port if undefined (will use protocol default)
|
||||||
|
const portInput = config?.remote.port !== undefined ? config.remote.port.toString() : '';
|
||||||
const authTokenInput = config?.remote.auth_token ?? '';
|
const authTokenInput = config?.remote.auth_token ?? '';
|
||||||
const localPortInput = (config?.local.port ?? 8317).toString();
|
const localPortInput = (config?.local.port ?? 8317).toString();
|
||||||
|
|
||||||
@@ -1365,9 +1369,13 @@ function ProxyContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const savePort = () => {
|
const savePort = () => {
|
||||||
const port = parseInt(editedPort ?? displayPort, 10);
|
const portStr = editedPort ?? displayPort;
|
||||||
if (!isNaN(port) && port !== config?.remote.port) {
|
// Empty string means use protocol default (undefined)
|
||||||
saveCliproxyServerConfig({ remote: { ...remoteConfig, port } });
|
const port = portStr === '' ? undefined : parseInt(portStr, 10);
|
||||||
|
const effectivePort = port && !isNaN(port) && port > 0 ? port : undefined;
|
||||||
|
|
||||||
|
if (effectivePort !== config?.remote.port) {
|
||||||
|
saveCliproxyServerConfig({ remote: { ...remoteConfig, port: effectivePort } });
|
||||||
}
|
}
|
||||||
setEditedPort(null);
|
setEditedPort(null);
|
||||||
};
|
};
|
||||||
@@ -1496,13 +1504,19 @@ function ProxyContent({
|
|||||||
{/* Port and Protocol */}
|
{/* Port and Protocol */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-sm text-muted-foreground">Port</label>
|
<label className="text-sm text-muted-foreground">
|
||||||
|
Port{' '}
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
(default: {getDefaultPort(config?.remote.protocol || 'http')})
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
value={displayPort}
|
value={displayPort}
|
||||||
onChange={(e) => setEditedPort(e.target.value)}
|
onChange={(e) => setEditedPort(e.target.value.replace(/\D/g, ''))}
|
||||||
onBlur={savePort}
|
onBlur={savePort}
|
||||||
placeholder="8317"
|
placeholder={`Leave empty for ${getDefaultPort(config?.remote.protocol || 'http')}`}
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
@@ -1545,7 +1559,7 @@ function ProxyContent({
|
|||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
disabled={testing || !displayHost || !displayPort}
|
disabled={testing || !displayHost}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user