Environment
- Node.js: 24.19.0 (also 20/22)
@google-cloud/storage: 8.0.1 (also on main)
Problem
destroy()ing the stream from createReadStream() before the GCS response arrives (e.g. a client disconnects mid-download) leaves the late response to pipe into a destroyed stream. onResponse runs pipeline() with no liveness check:
The pipeline() throws ERR_STREAM_UNABLE_TO_PIPE synchronously inside retry-request's promise, so it surfaces as an unhandled rejection — never passed to the pipeline callback, never emitted as 'error', so consumers can't catch it.
Reproduce
const {Storage} = require('@google-cloud/storage')
process.on('unhandledRejection', e => { console.error('UNHANDLED:', e.code); process.exit(1) })
const stream = new Storage().bucket('my-bucket').file('some-object')
.createReadStream({validation: false})
stream.on('error', () => {}) // does NOT catch the throw
stream.resume() // fires the GCS request
setTimeout(() => stream.destroy(), 0) // aborted before the response landed
→ UNHANDLED: ERR_STREAM_UNABLE_TO_PIPE
Expected
destroy() is the documented way to abort a download (#2010); aborting must not raise an uncatchable rejection. Under Node's default --unhandled-rejections=throw this crashes the process.
Suggested fix
Guard onResponse before piping:
if (throughStream.destroyed) return
Ideally also destroy rawResponseStream there to release the socket (cf. #2313).
Environment
@google-cloud/storage: 8.0.1 (also onmain)Problem
destroy()ing the stream fromcreateReadStream()before the GCS response arrives (e.g. a client disconnects mid-download) leaves the late response to pipe into a destroyed stream.onResponserunspipeline()with no liveness check:onResponse: https://github.com/googleapis/google-cloud-node/blob/main/handwritten/storage/src/file.ts#L1705throughStreamunconditionally: https://github.com/googleapis/google-cloud-node/blob/main/handwritten/storage/src/file.ts#L1783The
pipeline()throwsERR_STREAM_UNABLE_TO_PIPEsynchronously insideretry-request's promise, so it surfaces as an unhandled rejection — never passed to the pipeline callback, never emitted as'error', so consumers can't catch it.Reproduce
→
UNHANDLED: ERR_STREAM_UNABLE_TO_PIPEExpected
destroy()is the documented way to abort a download (#2010); aborting must not raise an uncatchable rejection. Under Node's default--unhandled-rejections=throwthis crashes the process.Suggested fix
Guard
onResponsebefore piping:Ideally also destroy
rawResponseStreamthere to release the socket (cf. #2313).