Skip to content

storage: createReadStream() never errors when a download stalls mid-body #9515

Open

Description

@nickretallack

Library name

@google-cloud/storage

What would you like to see in the library?

An idle timeout for File#createReadStream(): if no body bytes arrive for a set time, the stream emits an error.

Why

A download that stops mid-body never emits an error or end, so a pipeline that reads from the stream waits forever. In production we see occasional downloads of multi-gigabyte objects stop partway through. Each one then hung for days until we stopped the process by hand.

This worked in early versions. In 2017, a stalled createReadStream() raised ESOCKETTIMEOUT (#2317), because the library used request, whose timeout set a socket idle timeout. That protection was lost later:

googleapis/nodejs-storage#24 reports the same hang. Its last comment (storage 6.12, Node.js 20) also sees streams that hang forever.

Reproduction

storage 8.1.0, Node.js 22.22.1:

const http = require('node:http')
const { Storage } = require('@google-cloud/storage')

// The server sends the response headers and part of the body, then stops sending.
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'content-type': 'application/octet-stream' })
  res.write('first part of the file\n')
})
server.listen(0, () => {
  const storage = new Storage({
    apiEndpoint: `http://127.0.0.1:${server.address().port}`,
    projectId: 'repro',
    useAuthWithCustomEndpoint: false,
  })
  let bytes = 0
  storage.bucket('bucket').file('file.jsonl').createReadStream({ validation: false })
    .on('data', (chunk) => { bytes += chunk.length })
    .on('error', (err) => { console.log('error:', err.message); process.exit(0) })
    .on('end', () => { console.log('end'); process.exit(0) })
  setTimeout(() => {
    console.log(`after 90 s: ${bytes} bytes received, no error, no end`)
    process.exit(1)
  }, 90000)
})

Output: after 90 s: 23 bytes received, no error, no end

Alternatives considered

We now wrap the stream in our own watchdog. It aborts the pipeline when no data moves for 10 minutes. Every user who streams large objects has to build the same thing. Node's built-in fetch (undici) has bodyTimeout for this case, with a default of 5 minutes.

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