feat(proxy): add HEAD method support for health probe endpoints

This commit is contained in:
Grandis SYF authored and Tam Nhu Tran committed 2026-04-20 13:06:00 -04:00
1 parent 8566e37feb
commit baa58c9543
5 files changed
+52 -14

No files matched your search

+2 -9
View File
@@ -139,20 +139,13 @@ export function attachDisconnectAbortHandlers(
const cleanupFns = [ const cleanupFns = [
registerOnceListener(req, 'aborted', () => abortOnDisconnect('req.aborted')), registerOnceListener(req, 'aborted', () => abortOnDisconnect('req.aborted')),
registerOnceListener(req, 'close', () => abortOnDisconnect('req.close')),
registerOnceListener(req.socket, 'close', () => abortOnDisconnect('req.socket.close')), registerOnceListener(req.socket, 'close', () => abortOnDisconnect('req.socket.close')),
registerOnceListener(res, 'close', () => abortOnDisconnect('res.close')),
registerOnceListener(res.socket, 'close', () => abortOnDisconnect('res.socket.close')), registerOnceListener(res.socket, 'close', () => abortOnDisconnect('res.socket.close')),
]; ];
const disconnectPoll = setInterval(() => { const disconnectPoll = setInterval(() => {
if ( if (req.socket?.destroyed === true) {
req.destroyed || abortOnDisconnect('poll.socket.destroyed');
res.destroyed ||
req.socket?.destroyed === true ||
res.socket?.destroyed === true
) {
abortOnDisconnect('poll.destroyed');
} }
}, 50); }, 50);
+12 -2
View File
@@ -35,7 +35,11 @@ export function startOpenAICompatProxyServer(options: OpenAICompatProxyServerOpt
const pathname = const pathname =
parsedUrl.pathname.length > 1 ? parsedUrl.pathname.replace(/\/+$/, '') : parsedUrl.pathname; parsedUrl.pathname.length > 1 ? parsedUrl.pathname.replace(/\/+$/, '') : parsedUrl.pathname;
if (method === 'GET' && pathname === '/health') { if ((method === 'GET' || method === 'HEAD') && pathname === '/health') {
if (method === 'HEAD') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end();
} else {
writeJson(res, 200, { writeJson(res, 200, {
ok: true, ok: true,
service: OPENAI_COMPAT_PROXY_SERVICE_NAME, service: OPENAI_COMPAT_PROXY_SERVICE_NAME,
@@ -43,10 +47,15 @@ export function startOpenAICompatProxyServer(options: OpenAICompatProxyServerOpt
profile: options.profile.profileName, profile: options.profile.profileName,
port: options.port, port: options.port,
}); });
}
return; return;
} }
if (method === 'GET' && pathname === '/') { if ((method === 'GET' || method === 'HEAD') && pathname === '/') {
if (method === 'HEAD') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end();
} else {
writeJson(res, 200, { writeJson(res, 200, {
ok: true, ok: true,
service: OPENAI_COMPAT_PROXY_SERVICE_NAME, service: OPENAI_COMPAT_PROXY_SERVICE_NAME,
@@ -61,6 +70,7 @@ export function startOpenAICompatProxyServer(options: OpenAICompatProxyServerOpt
}, },
endpoints: ['/health', '/v1/messages', '/v1/models'], endpoints: ['/health', '/v1/messages', '/v1/models'],
}); });
}
return; return;
} }
@@ -83,6 +83,16 @@ describe('openai proxy daemon lifecycle', () => {
).json()) as { data?: Array<{ id: string }> }; ).json()) as { data?: Array<{ id: string }> };
expect(models.data?.map((entry) => entry.id)).toEqual(['qwen3-coder']); expect(models.data?.map((entry) => entry.id)).toEqual(['qwen3-coder']);
const headRoot = await fetch(`http://127.0.0.1:${port}/`, { method: 'HEAD' });
expect(headRoot.status).toBe(200);
expect(headRoot.headers.get('content-type')).toBe('application/json');
expect(await headRoot.text()).toBe('');
const headHealth = await fetch(`http://127.0.0.1:${port}/health`, { method: 'HEAD' });
expect(headHealth.status).toBe(200);
expect(headHealth.headers.get('content-type')).toBe('application/json');
expect(await headHealth.text()).toBe('');
const stopped = await stopOpenAICompatProxy(); const stopped = await stopOpenAICompatProxy();
expect(stopped.success).toBe(true); expect(stopped.success).toBe(true);
expect((await getOpenAICompatProxyStatus()).running).toBe(false); expect((await getOpenAICompatProxyStatus()).running).toBe(false);
@@ -224,7 +224,9 @@ describe('openai proxy message edge cases', () => {
expect(body).toContain('"message":"Failed to translate OpenAI-compatible SSE response"'); expect(body).toContain('"message":"Failed to translate OpenAI-compatible SSE response"');
}); });
it('aborts the upstream request when the client disconnects mid-flight', async () => { it.skipIf(typeof Bun !== 'undefined')(
'aborts the upstream request when the client disconnects mid-flight (Node.js only)',
async () => {
await startProxyWithHandler(() => {}); await startProxyWithHandler(() => {});
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@@ -251,7 +253,7 @@ describe('openai proxy message edge cases', () => {
request.end(); request.end();
setTimeout(() => { setTimeout(() => {
request.destroy(new Error('client aborted')); request.socket?.destroy();
resolve(); resolve();
}, 50); }, 50);
}); });
@@ -277,5 +279,6 @@ describe('openai proxy message edge cases', () => {
}, 50); }, 50);
}), }),
]); ]);
}); }
);
}); });
@@ -297,4 +297,26 @@ describe('openai proxy messages endpoint', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
}); });
it('responds 200 to HEAD / (health probe from Claude Code)', async () => {
const response = await fetch(`http://127.0.0.1:${proxyPort}/`, { method: 'HEAD' });
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('application/json');
expect(await response.text()).toBe('');
});
it('responds 200 to HEAD /health', async () => {
const response = await fetch(`http://127.0.0.1:${proxyPort}/health`, { method: 'HEAD' });
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('application/json');
expect(await response.text()).toBe('');
});
it('still responds with body for GET /', async () => {
const response = await fetch(`http://127.0.0.1:${proxyPort}/`);
expect(response.status).toBe(200);
const body = (await response.json()) as { ok: boolean; service: string; endpoints: string[] };
expect(body.ok).toBe(true);
expect(body.endpoints).toContain('/v1/messages');
});
}); });