Fix: response.download does not return /proc/* files
Fixed pillarjs/send#311 — 6 line bug-fix.

The Bug
Repo: pillarjs/send Issue: #311 Status: PR-submitted PR: https://github.com/pillarjs/send/pull/311
Description: response.download does not return /proc/* files
Fix scope: 6 lines changed in index.js
PR body: When stat.size === 0 (e.g. /proc virtual filesystem files),
send() was setting opts.end = 0 and Content-Length: 0, causing the read
stream to stop at byte 0 and the response to be empty.
Root Cause
The send() method in index.js works by calling fs.stat() on the requested
path, then using the returned stat object to configure an
fs.createReadStream() call. The key fields are stat.size (used as len in
the send() method) and the derived opts.start and opts.end values that
define the byte range for the read stream. This is the standard pattern for
serving static files: stat the file, get the size, and stream exactly that many
bytes.
The /proc/* problem: Files under /proc are virtual — the kernel’s procfs
generates their content on-the-fly when read. The stat syscall returns
st_size = 0 for many of these files because the kernel does not pre-compute
the output size; it doesn’t know how many bytes read() will produce until the
read actually happens. Standard filesystems (ext4, xfs, btrfs) store the
exact byte count in the inode, so stat.size is always accurate. Procfs has no
such guarantee — it reports zero as a sentinel meaning “unknown ahead of time.”
When send() receives stat.size = 0, it sets len = 0. The original code
then executes:
opts.end = Math.max(offset, offset + len - 1)
res.setHeader('Content-Length', len)
With len = 0, opts.end evaluates to Math.max(0, -1) = 0, so the read
stream’s range is start: 0, end: 0 — exactly zero bytes. The response
header Content-Length: 0 tells the client the body is empty. The
fs.createReadStream call opens the file but the end-at-zero range means it
reads nothing, pipes nothing, and the response arrives with an empty body even
though the file is readable via fs.readFile().
Impact: The bug silently returns empty responses for any file where
stat.size is zero but the file has readable content. Beyond /proc/*, this
affects other pseudo-filesystems (/sys, /dev) and any scenario where a
file’s reported stat size does not reflect its actual readable content.
The Fix
The fix is 6 lines in index.js, wrapping two statements in len > 0 guards:
// BEFORE (broken)
opts.end = Math.max(offset, offset + len - 1)
res.setHeader('Content-Length', len)
// AFTER (fixed)
if (len > 0) {
opts.end = Math.max(offset, offset + len - 1)
}
if (len > 0) {
res.setHeader('Content-Length', len)
}
When len === 0, opts.end is left undefined, so fs.createReadStream() reads
until end-of-file (EOF). Without a Content-Length header, Node.js falls back
to chunked transfer encoding, which correctly streams the dynamic content.
The Content-Range header path (earlier in the same function) already handled
len === 0 correctly, so only these two lines needed adjustment.
The test suite also updated the zero-length file test — it no longer asserts
Content-Length: 0 for empty files, since the fix makes the behavior
context-dependent.
Pattern & Takeaways
Pattern: An implicit invariant (stat.size reliably reflects readable
content size) that holds for regular files but breaks on pseudo-filesystems.
The code assumed a non-zero size implied content existence, but the reciprocal
(size === 0 implies no content) does not hold for virtual files. This is a
classic boundary condition at the zero-value edge of a numeric parameter.
Key insight: The most predictable bugs are edge cases at input boundaries.
Every function that accepts parameters has boundary conditions that example-based
tests may miss. Code review should focus on: (1) What happens with empty/null input?
(2) What happens at iteration boundaries? (3) What happens with unexpected types?
(4) What assumptions does the type system leave implicit? In this case, the
unwritten assumption was “stat.size is the number of readable bytes” — true
for real filesystems, false for procfs.
Deeper lesson about fs.createReadStream: When end is omitted from the
options, Node.js reads until the file descriptor returns EOF. When end is
explicitly set (even to 0), the read range is clamped to [start, end]. This
subtle difference — setting end = 0 vs. omitting end — is the mechanical
heart of the bug. The fix leverages this by conditionally omitting the option
entirely, letting Node.js do the right thing.
Transfer Potential
High — the specific trigger (virtual filesystem stat reporting) is niche, but the pattern repeats across ecosystems. Any library that pre-computes stream boundaries from source metadata (stat, HTTP Content-Length, database row counts) is vulnerable when the metadata reports zero for a non-empty source. The minimal-change principle and boundary-condition thinking transfer to any codebase. Reading this post helps recognize similar patterns in your own projects: watch for places where a zero-valued length/limit/size controls a read operation without checking whether the source genuinely has zero content.
Auto-generated from PR #311. View all patches on GitHub.
References
[1] pillarjs/send [2] #311 [3] https://github.com/pillarjs/send/pull/311