Summary
Download responses can leak extent file descriptors on a client disconnect, because the response pipe does not destroy the body stream when the client goes away.
Details
The generated serializers stream a download body like this (src/blob/generated/utils/serializer.ts:373-377, and the equivalent src/queue/... and src/table/... serializers):
handlerResponse.body
.on("error", reject)
.pipe(res.getBodyStream())
.on("error", reject)
.on("close", resolve);
res.getBodyStream() is the HTTP response. When a client disconnects mid-download, Node's pipe only unpipes the source from the destination — it does not destroy handlerResponse.body. The body stream (a FSExtentStore.readExtents multistream) is therefore left alive and paused, holding its current extent's file descriptor open. Repeated aborted downloads can accumulate descriptors and reproduce the EMFILE condition from #1967.
Proposed fix
Destroy the body stream when the response closes before the body has finished, at the pipe seam (for blob, queue and table serializers), e.g.:
dest.on("close", () => {
if (body.destroy && !body.destroyed) body.destroy(); // release extent fds on disconnect
resolve();
});
Because it lives in the shared FSExtentStore, destroying the merged stream tears down the current extent (releasing its fd); the abort guard added in #2797 then covers the "extent opened just after destroy" race.
Add an HTTP-level abort regression test (start a download, abort the socket mid-stream, and assert the body/extent stream is destroyed).
Context
Summary
Download responses can leak extent file descriptors on a client disconnect, because the response pipe does not destroy the body stream when the client goes away.
Details
The generated serializers stream a download body like this (
src/blob/generated/utils/serializer.ts:373-377, and the equivalentsrc/queue/...andsrc/table/...serializers):res.getBodyStream()is the HTTP response. When a client disconnects mid-download, Node'spipeonly unpipes the source from the destination — it does not destroyhandlerResponse.body. The body stream (aFSExtentStore.readExtentsmultistream) is therefore left alive and paused, holding its current extent's file descriptor open. Repeated aborted downloads can accumulate descriptors and reproduce theEMFILEcondition from #1967.Proposed fix
Destroy the body stream when the response closes before the body has finished, at the pipe seam (for blob, queue and table serializers), e.g.:
Because it lives in the shared
FSExtentStore, destroying the merged stream tears down the current extent (releasing its fd); the abort guard added in #2797 then covers the "extent opened just after destroy" race.Add an HTTP-level abort regression test (start a download, abort the socket mid-stream, and assert the body/extent stream is destroyed).
Context
readExtentslazy (at most one extent fd open at a time) and fixes the normal-completion leak from 500 Errors - Error: EMFILE: too many open files #1967. Read blob extents lazily to avoid EMFILE on large blobs (issue #1967) #2797 already reduces a disconnect leak from N descriptors to 1, but does not fully tear down the body on a raw client disconnect — this issue tracks that remaining teardown.