diff --git a/src/cli-agent.ts b/src/cli-agent.ts index 58069048..dca89882 100644 --- a/src/cli-agent.ts +++ b/src/cli-agent.ts @@ -302,6 +302,9 @@ export const httpRequest: ApiRequest = (ctx, options) => { (res) => { const chunks: Buffer[] = []; res.on('data', (chunk: Buffer) => chunks.push(chunk)); + // A connection dropped mid-body emits 'error' and never 'end'; without this the + // promise never settles (and a long-lived `codeman mcp` never answers the call). + res.on('error', reject); res.on('end', () => { const text = Buffer.concat(chunks).toString('utf-8'); let json: ApiEnvelope | undefined; diff --git a/test/cli-agent.test.ts b/test/cli-agent.test.ts index 84c635be..977e064c 100644 --- a/test/cli-agent.test.ts +++ b/test/cli-agent.test.ts @@ -660,6 +660,13 @@ describe('httpRequest', () => { res.writeHead(401, { 'Content-Type': 'text/plain' }).end('Unauthorized'); return; } + if (req.url?.startsWith('/reset')) { + // Headers and half a body, then the connection drops (a restart mid-answer). + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.write('{"success":true,"data":'); + setTimeout(() => req.socket.destroy(), 50); + return; + } res .writeHead(200, { 'Content-Type': 'application/json' }) .end(JSON.stringify({ success: true, data: { echo: true } })); @@ -702,6 +709,10 @@ describe('httpRequest', () => { expect(last.headers.authorization).toBeUndefined(); }); + it('rejects, rather than hanging, when the connection drops mid-body', async () => { + await expect(httpRequest(ctx({ apiUrl }), { method: 'GET', path: '/reset', timeoutMs: 5000 })).rejects.toThrow(); + }); + it('keeps a plain-text body when the answer is not JSON', async () => { const res = await httpRequest(ctx({ apiUrl }), { method: 'GET', path: '/plain' }); expect(res.status).toBe(401);