Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fix(api-proxy): address review feedback on key validation
- httpProbe: add settle-once guard with resolveOnce/rejectOnce to
  prevent hanging if response stream errors or socket closes early;
  also handle res 'error' and 'close' events
- Startup latch: clarify comment that only validation-participating
  listeners are counted (no-key Gemini 503 handler excluded)
- Test: replace hard-coded port 19999 with dynamic port allocation
  to prevent flakiness when something listens on that port

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
lpcox and Copilot committed Apr 24, 2026
commit 53cdaebf8488c46b50c405249da0a514967d756c
24 changes: 20 additions & 4 deletions containers/api-proxy/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@
const anthropicTarget = ov('anthropicTarget', ANTHROPIC_API_TARGET);
const copilotGithubToken = ov('copilotGithubToken', COPILOT_GITHUB_TOKEN);
const copilotApiKey = ov('copilotApiKey', COPILOT_API_KEY);
const copilotAuthToken = ov('copilotAuthToken', COPILOT_AUTH_TOKEN);
const copilotTarget = ov('copilotTarget', COPILOT_API_TARGET);
const copilotIntegrationId = ov('copilotIntegrationId', COPILOT_INTEGRATION_ID);
const geminiKey = ov('geminiKey', GEMINI_API_KEY);
Expand Down Expand Up @@ -1111,16 +1111,30 @@
timeout: timeoutMs,
};

let settled = false;
const resolveOnce = (statusCode) => {
if (settled) return;
settled = true;
resolve(statusCode);
};
const rejectOnce = (err) => {
if (settled) return;
settled = true;
reject(err);
};

const req = mod.request(reqOpts, (res) => {
// Consume body to free the socket
res.resume();
res.on('end', () => resolve(res.statusCode));
res.on('end', () => resolveOnce(res.statusCode));
res.on('error', rejectOnce);
res.on('close', () => resolveOnce(res.statusCode));
});

req.on('timeout', () => {
req.destroy(new Error(`Probe timed out after ${timeoutMs}ms`));
});
req.on('error', reject);
req.on('error', rejectOnce);

if (opts.body) {
req.write(opts.body);
Expand Down Expand Up @@ -1172,7 +1186,9 @@
// Health port is always 10000 — this is what Docker healthcheck hits
const HEALTH_PORT = 10000;

// Startup latch: count expected listeners, run validation when all are ready
// Startup latch: count listeners that participate in key validation.
// The no-key Gemini 503 handler binds port 10003 but doesn't participate
// in validation, so it's intentionally excluded from the count.
let expectedListeners = 1; // port 10000 (always)
if (ANTHROPIC_API_KEY) expectedListeners++;
if (COPILOT_AUTH_TOKEN) expectedListeners++;
Expand All @@ -1182,7 +1198,7 @@
function onListenerReady() {
readyListeners++;
if (readyListeners === expectedListeners) {
logRequest('info', 'startup_complete', { message: `All ${expectedListeners} listeners ready, starting key validation` });
logRequest('info', 'startup_complete', { message: `All ${expectedListeners} validation-participating listeners ready, starting key validation` });
validateApiKeys().catch((err) => {
logRequest('error', 'key_validation_error', { message: 'Unexpected error during key validation', error: String(err) });
keyValidationComplete = true;
Expand Down
10 changes: 9 additions & 1 deletion containers/api-proxy/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1036,8 +1036,16 @@ describe('httpProbe', () => {
});

it('should reject on connection refused', async () => {
// Allocate a port, then close it — guarantees nothing is listening
const tmpServer = http.createServer();
const refusedPort = await new Promise((resolve) => {
tmpServer.listen(0, '127.0.0.1', () => {
resolve(tmpServer.address().port);
tmpServer.close();
});
});
await expect(
httpProbe('http://127.0.0.1:19999/health', {
httpProbe(`http://127.0.0.1:${refusedPort}/health`, {
method: 'GET',
headers: {},
}, 5000)
Expand Down
Loading