Repository navigation
Description
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:
- With teeny-request 9.x / node-fetch 2 (storage ≤ 8.0.0), the request
timeoutstopped when the response headers arrived. node-fetch 2 applied its body timeout only to buffered reads (.text(),.json()), not to a piped stream. - With teeny-request ≥ 10 (storage ≥ 8.0.1), the documented
timeouthas no effect at all. See teeny-request: the timeout option is ignored since 10.0.0 (node-fetch 3 has no timeout option) #9514.
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.
Activity