nice-grpc icon indicating copy to clipboard operation
nice-grpc copied to clipboard

Posible memory leak

Open polRk opened this issue 9 months ago • 1 comments

https://github.com/deeplay-io/nice-grpc/blob/223fb38b0078b32ae6cd242dbfffb077675c337b/packages/nice-grpc/src/client/createServerStreamingMethod.ts#L110

If I call the throw method from the iterator, then the next wait will not stop.

Example:

let serverStream = ...

once(AbortSignal.timeout(10), 'abort').then(() => {
  return serverStream.throw!(new Error("Timeout."))
})

let result = await serverStream.next() // the first message will be received in 1 second. The timeout is 10ms, but we are still waiting for the first message...

polRk avatar May 30 '25 07:05 polRk

Posible solution

async function testWithRace() {
    // Create a slow generator similar to the previous test
    const slowGenerator = () => ({
        [Symbol.asyncIterator]() {
            let yielded = false;
            console.log('Generator started');

            return {
                // async next() {
                //     if (!yielded) {
                //         await new Promise(resolve => setTimeout(resolve, 5000));
                //         yielded = true;
                //         console.log('Generator yielded first value');
                //         return { value: { status: 200 }, done: false };
                //     }
                //     return { value: undefined, done: true };
                // },
                async return() {
                    return { value: undefined, done: true };
                },
                async throw(error) {
                    if (this._abortController) {
                        this._abortController.abort();
                    }
                    throw error;
                },
                _abortController: (null as unknown) as AbortController, // Initialize abort controller
                async next() {
                    if (!yielded) {
                        this._abortController = new AbortController();
                        try {
                            await new Promise((resolve, reject) => {
                                const timeout = setTimeout(resolve, 5000);
                                this._abortController.signal.addEventListener('abort', () => {
                                    clearTimeout(timeout);
                                    reject(new Error('Operation aborted'));
                                });
                            });
                            yielded = true;
                            console.log('Generator yielded first value');
                            return { value: { status: 200 }, done: false };
                        } catch (err) {
                            if (err.message === 'Operation aborted') {
                                throw error; // Re-throw the original error from throw()
                            }
                            throw err;
                        }
                    }
                    return { value: undefined, done: true };
                }
            };
        }
    });

    // Create the iterator
    const iterator = slowGenerator()[Symbol.asyncIterator]()

    const timeout = 2000 // 2 seconds timeout

    try {
        // Race between the iterator's next result and a timeout promise
        const result = await Promise.race([
            iterator.next(),
            new Promise((_, reject) =>
                setTimeout(() => iterator.throw!(new Error("Operation timed out after " + timeout + "ms")), timeout)
            )
        ])

        console.log('Result:', result)
        return result
    } catch (error) {
        console.error('Error occurred:', error.message)
        throw error
    }
}

// Run the improved test
testWithRace().catch(err => console.error('Test failed with race pattern:', err))

polRk avatar May 30 '25 07:05 polRk

Hi, sorry for the late response

From what I can see, the code is relying on calling iterator.throw(...) to inject an error into a server-streaming handler. However, throw is part of the generator protocol and is not something that's meant to be used in typical application code — certainly not in the context of gRPC streams. See MDN docs for context.

This kind of usage falls outside of what nice-grpc is designed to support. Internally, nice-grpc uses async generators as a convenient abstraction, but they're not intended to be interacted with directly from user code in this way.

If you can share what you're trying to achieve at a higher level, maybe there's a more idiomatic solution. But as it stands, this looks like fragile and unsupported behavior.

aikoven avatar Jul 11 '25 09:07 aikoven