fix(core): Report chat attachment streams that close before they end

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian Gößler 2026-10-19 10:28:51 +02:00
parent 8c7176929f
commit 43ffef4e72
No known key found for this signature in database
GPG Key ID: D197A1D77570F4A9
2 changed files with 25 additions and 0 deletions

View File

@ -297,6 +297,23 @@ describe('ChatHubController', () => {
expect(await handlerSettled).toEqual({ error: undefined });
});
it('should reject when the file stream closes before it ends', async () => {
const stream = await openFileStream();
chatAttachmentService.getAttachment.mockResolvedValue(attachmentResult(stream));
const { port, handlerSettled } = await startAttachmentServer();
const clientReq = request(port);
clientReq.on('response', (clientRes) => {
clientRes.once('data', () => stream.destroy());
clientRes.on('error', () => {});
});
expect(await handlerSettled).toEqual({
error: expect.objectContaining({ code: 'ERR_STREAM_PREMATURE_CLOSE' }),
});
await vi.waitFor(() => expect(stream.closed).toBe(true), { timeout: CLOSE_TIMEOUT_MS });
});
it('should close the file stream and settle when the client disconnects before the response starts', async () => {
const stream = await openFileStream();
const requestReceived = createDeferredPromise();

View File

@ -164,12 +164,20 @@ export class ChatHubController {
return;
}
// pipeline also rejects with ERR_STREAM_PREMATURE_CLOSE when the source closes early,
// so only treat it as a disconnect if the response closed while the source was open.
let clientDisconnected = false;
res.once('close', () => {
clientDisconnected = !res.writableFinished && !stream.destroyed;
});
// pipeline destroys the source when the client disconnects during the download,
// so aborted downloads don't leak file descriptors.
try {
await pipeline(stream, res);
} catch (error) {
if (
clientDisconnected &&
error instanceof Error &&
'code' in error &&
error.code === 'ERR_STREAM_PREMATURE_CLOSE'