Fix: serve zero-size virtual files with proper content
Fixed pillarjs/send#4944 — when stat.size is 0 (e.g. /proc files), do not clamp the read stream to byte 0. 6-line fix.

The Bug
Repo: pillarjs/send Issue: #4944 on expressjs/express PR: #311 on pillarjs/send Fix scope: 6 lines changed, 2 files
response.download() (which delegates to sendFile() via the send package) returns an empty body for files under /proc — the Linux virtual filesystem. /proc/meminfo, /proc/cpuinfo, and similar files are readable with fs.readFile() but send serves them as zero-length.
Root Cause
The bug lives in SendStream.prototype.send(), which receives a stat object from fs.stat(). For /proc virtual files, stat.size is 0 — the kernel reports zero size because these files are generated on-the-fly and have no fixed length.
The send() method then does two things that produce an empty response:
- Limits the read stream to byte 0 —
opts.end = Math.max(offset, offset + len - 1)evaluates toMath.max(0, -1) = 0, sofs.createReadStream(path, {start: 0, end: 0})reads at most 1 byte. - Sets Content-Length: 0 —
res.setHeader('Content-Length', 0)tells the client the body is zero bytes.
The client sees Content-Length: 0, reads zero bytes, and the response is effectively empty. The data is on disk but the code never reads past byte 0.
Why This Pattern Is Easy to Miss
Standard test suites for HTTP file serving use real files with real sizes. Virtual filesystems like /proc, sysfs, and tracefs are rarely tested. The stat.size assumption holds for 99.9% of files — until someone serves a /proc file through Express. [1]
Systems Affected
This pattern affects any Node.js or browser application that uses stat.size to determine content length and serve files. The send package is used by Express’s res.sendFile() and res.download(), so any Express app serving virtual filesystem paths is vulnerable.
The Fix
Two guards around the read stream options and Content-Length header:
// set read options
opts.start = offset
- opts.end = Math.max(offset, offset + len - 1)
+ if (len > 0) {
+ opts.end = Math.max(offset, offset + len - 1)
+ }
// content-length
- res.setHeader('Content-Length', len)
+ if (len > 0) {
+ res.setHeader('Content-Length', len)
+ }
When len === 0, the read stream runs until EOF (Node.js reads to end of file), and no Content-Length is set — Node.js uses chunked transfer encoding automatically. For truly empty files, the stream emits zero data and Node.js adds Content-Length: 0 on its own.
The existing test for zero-length files was updated to remove the explicit Content-Length: '0' assertion, since that’s now delegated to Node.js.
Pattern & Takeaways
Pattern: stat.size === 0 does not mean “file is empty” for virtual/pseudo filesystems. The send package assumed a file’s stat size was authoritative, but /proc, sysfs, and other virtual filesystems report size 0 while containing readable data.
Key insight: Never treat stat.size as the definitive content length for files. Always read until EOF. The Content-Length header should reflect what you actually stream, not what stat says. This is a classic leaky abstraction — fs.stat() works on real files and virtual files, but size has different semantics for each. The fix removes the assumption that stat.size is a hard limit rather than adding special-case detection for /proc.
Actionable Checklist
If your codebase serves files in Node.js, audit for these patterns:
fs.createReadStream(path, {start, end})— doesendever resolve to0whenstat.sizeis0? Guard withif (len > 0).res.setHeader('Content-Length', len)— does it set0when the file has no known size? Delegate to Node.js by omitting the header.Buffer.alloc(size)— does it usestat.sizefor allocation? This crashes on virtual files.- Range requests — do
Rangeheader calculations divide bystat.size? Guard against zero.
Transfer Potential
This pattern applies broadly: any code that uses stat.size to determine how much of a file to read will break on virtual filesystems. APIs using stat.size for allocation (Buffer.alloc(size)), range calculations, or content-length headers should fall back to reading until EOF when size === 0.
When to Use This Fix
Apply this guard pattern in any codebase that serves files through HTTP in Node.js:
- Static file servers — if they use
send,express.static, or customfs.createReadStreamwrappers - API endpoints that serve file downloads — protect against zero-size files from
/procorsysfs - Proxy servers — avoid forwarding
Content-Length: 0for files that might be virtual
Rule of Thumb
If a file’s stat.size is 0, don’t assume it’s empty. Read until EOF and let Node.js determine the actual length. This single rule protects against all virtual filesystem edge cases without needing to detect which filesystem a file lives on.
Auto-generated from PR #4944. View all patches on GitHub.
References
- [1] (citation needed)