fix(cliproxy): prevent false remote timeout on reachable proxy

This commit is contained in:
Tam Nhu Tran
2026-02-22 22:38:50 +07:00
parent dcdb2f6284
commit 34292ca7f8
2 changed files with 41 additions and 31 deletions
+32 -22
View File
@@ -196,8 +196,8 @@ function createHttpsAgent(allowSelfSigned: boolean): https.Agent | undefined {
/**
* Check health of remote CLIProxyAPI instance
*
* Uses /v1/models endpoint for health check since CLIProxyAPI doesn't expose /health.
* This endpoint is always available and returns 200 when the server is operational.
* Uses root endpoint (/) for health check since CLIProxyAPI doesn't expose /health.
* Root is cheap and avoids false negatives from slower model-list endpoints.
*
* @param config Remote proxy client configuration
* @returns RemoteProxyStatus with reachability and latency
@@ -217,14 +217,13 @@ export async function checkRemoteProxy(
};
}
// Use /v1/models as health check - CLIProxyAPI doesn't have /health endpoint
const url = buildProxyUrl(host, port, protocol, '/v1/models');
// Use root endpoint for liveness check - cheap and available across deployments
const url = buildProxyUrl(host, port, protocol, '/');
const startTime = Date.now();
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
// Build request options
const headers: Record<string, string> = {
Accept: 'application/json',
@@ -245,8 +244,19 @@ export async function checkRemoteProxy(
// Use native https module for self-signed cert support
response = await new Promise<Response>((resolve, reject) => {
const agent = createHttpsAgent(true);
let settled = false;
const settle = (callback: () => void) => {
if (settled) return;
settled = true;
clearTimeout(reqTimeout);
callback();
};
const reqTimeout = setTimeout(() => {
reject(new Error('Request timeout'));
const timeoutError = new Error('Request timeout');
req.destroy(timeoutError);
settle(() => reject(timeoutError));
}, timeout);
const req = https.request(
@@ -258,28 +268,28 @@ export async function checkRemoteProxy(
timeout,
},
(res) => {
clearTimeout(reqTimeout);
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
// Health check only needs response headers; don't wait for full body.
// This avoids timeout false negatives when servers stream slower payloads.
res.resume();
settle(() =>
resolve(
new Response(data, {
new Response(null, {
status: res.statusCode || 500,
statusText: res.statusMessage,
statusText: res.statusMessage ?? '',
})
);
});
)
);
}
);
req.on('error', (err) => {
clearTimeout(reqTimeout);
reject(err);
settle(() => reject(err));
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
const timeoutError = new Error('Request timeout');
req.destroy(timeoutError);
settle(() => reject(timeoutError));
});
req.end();
@@ -292,8 +302,6 @@ export async function checkRemoteProxy(
});
}
clearTimeout(timeoutId);
const latencyMs = Date.now() - startTime;
// Check for auth failure
@@ -328,6 +336,8 @@ export async function checkRemoteProxy(
error: getErrorMessage(errorCode, err.message),
errorCode,
};
} finally {
clearTimeout(timeoutId);
}
}
@@ -2,9 +2,9 @@
* Unit tests for remote-proxy-client module
*/
import { describe, it, expect } from 'bun:test';
import type {
RemoteProxyClientConfig,
RemoteProxyStatus,
import {
type RemoteProxyClientConfig,
type RemoteProxyStatus,
} from '../../../src/cliproxy/remote-proxy-client';
// We test the module's type exports and error handling logic
@@ -119,15 +119,15 @@ describe('remote-proxy-client', () => {
});
describe('health check URL construction', () => {
// CLIProxyAPI uses /v1/models for health checks (no /health endpoint)
it('should construct correct health check URL pattern using /v1/models', () => {
// CLIProxyAPI uses root endpoint for liveness checks (no /health endpoint)
it('should construct correct health check URL pattern using /', () => {
const config: RemoteProxyClientConfig = {
host: '192.168.1.100',
port: 8317,
protocol: 'http',
};
const expectedUrl = `${config.protocol}://${config.host}:${config.port}/v1/models`;
expect(expectedUrl).toBe('http://192.168.1.100:8317/v1/models');
const expectedUrl = `${config.protocol}://${config.host}:${config.port}/`;
expect(expectedUrl).toBe('http://192.168.1.100:8317/');
});
it('should construct HTTPS URL when protocol is https', () => {
@@ -136,8 +136,8 @@ describe('remote-proxy-client', () => {
port: 443,
protocol: 'https',
};
const expectedUrl = `${config.protocol}://${config.host}:${config.port}/v1/models`;
expect(expectedUrl).toBe('https://secure.example.com:443/v1/models');
const expectedUrl = `${config.protocol}://${config.host}:${config.port}/`;
expect(expectedUrl).toBe('https://secure.example.com:443/');
});
});