Skip to content

storage: createReadStream raises unhandled ERR_STREAM_UNABLE_TO_PIPE when destroyed before the response arrives #9242

Description

@joshuaellis

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api: storageIssues related to the Cloud Storage API.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions