Improve streamToBuffer error handling and memory safety (#17255)

## Description

This PR improves error handling and memory safety for the streamToBuffer
utility function.

### Changes
- Add proper cleanup of event listeners to prevent memory leaks
- Add checks for already-ended streams to handle edge cases gracefully
- Add protection against multiple resolve/reject calls with isResolved
flag
- Add handling for close events with appropriate error messages
- Improve robustness when streams are destroyed or closed externally

### Impact
This fixes potential memory leaks and race conditions when streams are
cancelled, destroyed, or closed before completion.

### Code Statistics
- 1 file changed
- ~53 lines added, 9 lines modified
- ~95% code changes (functional improvements)

---------

Co-authored-by: GitTensor Miner <miner@gittensor.io>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
Angel98518
2026-01-21 00:19:47 +08:00
committed by GitHub
parent 351e2030ff
commit 9cf88df67b
2 changed files with 138 additions and 9 deletions
@@ -0,0 +1,79 @@
import { PassThrough, Readable } from 'stream';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
describe('streamToBuffer', () => {
describe('successful scenarios', () => {
it('should convert a stream with single chunk to buffer', async () => {
const testData = 'Hello, World!';
const stream = Readable.from([Buffer.from(testData)]);
const result = await streamToBuffer(stream);
expect(result.toString()).toBe(testData);
});
it('should convert a stream with multiple chunks to buffer', async () => {
const chunks = ['Hello, ', 'World', '!'];
const stream = Readable.from(chunks.map((chunk) => Buffer.from(chunk)));
const result = await streamToBuffer(stream);
expect(result.toString()).toBe('Hello, World!');
});
it('should handle empty stream', async () => {
const stream = Readable.from([]);
const result = await streamToBuffer(stream);
expect(result.length).toBe(0);
expect(result.toString()).toBe('');
});
});
describe('error scenarios', () => {
it('should reject when stream is already ended', async () => {
const stream = Readable.from([Buffer.from('test')]);
await streamToBuffer(stream);
await expect(streamToBuffer(stream)).rejects.toThrow(
'Stream has already ended',
);
});
it('should reject when stream is not readable (destroyed)', async () => {
const stream = new PassThrough();
stream.destroy();
await expect(streamToBuffer(stream)).rejects.toThrow(
'Stream is not readable',
);
});
it('should reject when stream emits an error', async () => {
const stream = new PassThrough();
const testError = new Error('Stream error');
const promise = streamToBuffer(stream);
stream.write(Buffer.from('partial'));
stream.emit('error', testError);
await expect(promise).rejects.toThrow('Stream error');
});
it('should reject when stream closes before end', async () => {
const stream = new PassThrough();
const promise = streamToBuffer(stream);
stream.write(Buffer.from('data'));
stream.destroy();
await expect(promise).rejects.toThrow('Stream closed before end');
});
});
});