Skip to content

Commit 7628878

Browse files
committed
fix(openai-bridge): avoid duplicate error headers
1 parent 005cbdb commit 7628878

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

cli/openai-bridge.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,20 @@ function createOpenaiBridgeHttpHandler(options = {}) {
514514
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
515515
res.end(JSON.stringify(ensureResponseMetadata(responsesPayload)));
516516
} catch (e) {
517+
if (res.writableEnded || res.destroyed) {
518+
return;
519+
}
520+
const message = e && e.message ? e.message : 'Internal Error';
521+
if (res.headersSent) {
522+
try {
523+
res.end();
524+
} catch (_) {
525+
// Headers are already committed; avoid surfacing a secondary write failure.
526+
}
527+
return;
528+
}
517529
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
518-
res.end(JSON.stringify({ error: e && e.message ? e.message : 'Internal Error' }));
530+
res.end(JSON.stringify({ error: message }));
519531
}
520532
})();
521533

tests/unit/openai-bridge-upstream-responses.test.mjs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,3 +1609,77 @@ test('openai-bridge maps reasoning effort when converting to upstream chat compl
16091609
await upstream.close();
16101610
await rm(tmpDir, { recursive: true, force: true });
16111611
});
1612+
1613+
test('openai-bridge does not write 500 headers after response headers were already sent', async () => {
1614+
const upstream = http.createServer((req, res) => {
1615+
if (req.url === '/v1/chat/completions' && req.method === 'POST') {
1616+
res.writeHead(200, { 'Content-Type': 'application/json' });
1617+
res.end(JSON.stringify({
1618+
id: 'chatcmpl_headers_sent_regression',
1619+
model: 'gpt-test',
1620+
choices: [{ message: { role: 'assistant', content: 'ok' } }]
1621+
}));
1622+
return;
1623+
}
1624+
res.writeHead(404, { 'Content-Type': 'application/json' });
1625+
res.end(JSON.stringify({ error: 'not found' }));
1626+
});
1627+
const { port: upstreamPort } = await listen(upstream);
1628+
1629+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'codexmate-bridge-test-'));
1630+
const settingsFile = path.join(tmpDir, 'bridge.json');
1631+
await writeFile(settingsFile, JSON.stringify({
1632+
version: 1,
1633+
providers: {
1634+
test: { baseUrl: `http://127.0.0.1:${upstreamPort}/v1`, apiKey: 'sk-upstream' }
1635+
}
1636+
}), 'utf-8');
1637+
1638+
let uncaught = null;
1639+
const onUncaught = (err) => { uncaught = err; };
1640+
process.once('uncaughtException', onUncaught);
1641+
1642+
const handler = createOpenaiBridgeHttpHandler({ settingsFile, expectedToken: 'codexmate' });
1643+
const bridge = http.createServer((req, res) => {
1644+
const originalWriteHead = res.writeHead.bind(res);
1645+
let wroteSuccessHeader = false;
1646+
res.writeHead = function patchedWriteHead(statusCode, headers) {
1647+
if (statusCode === 200) wroteSuccessHeader = true;
1648+
if (wroteSuccessHeader && statusCode === 500) {
1649+
throw new Error('regression: attempted second writeHead after headers sent');
1650+
}
1651+
return originalWriteHead(statusCode, headers);
1652+
};
1653+
const originalEnd = res.end.bind(res);
1654+
res.end = function patchedEnd(chunk, encoding, cb) {
1655+
if (wroteSuccessHeader && !res.writableEnded) {
1656+
originalEnd(chunk, encoding, cb);
1657+
throw new Error('simulated post-header write failure');
1658+
}
1659+
return originalEnd(chunk, encoding, cb);
1660+
};
1661+
if (!handler(req, res)) {
1662+
res.statusCode = 404;
1663+
res.end('not handled');
1664+
}
1665+
});
1666+
const { port: bridgePort } = await listen(bridge);
1667+
1668+
const resp = await requestText(`http://127.0.0.1:${bridgePort}/bridge/openai/test/v1/responses`, {
1669+
method: 'POST',
1670+
headers: {
1671+
'Content-Type': 'application/json',
1672+
'Accept': 'application/json',
1673+
'Authorization': 'Bearer codexmate'
1674+
},
1675+
body: { model: 'gpt-test', input: 'ping' }
1676+
});
1677+
1678+
assert.equal(resp.status, 200);
1679+
assert.equal(uncaught, null);
1680+
1681+
process.removeListener('uncaughtException', onUncaught);
1682+
await bridge.close();
1683+
await upstream.close();
1684+
await rm(tmpDir, { recursive: true, force: true });
1685+
});

0 commit comments

Comments
 (0)