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 is 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 combine to 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 in the file but the code never reads past byte 0.
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 an example of a leaky abstraction — fs.stat() works on real files and virtual files, but size has different semantics for each. The fix doesn’t add special-case detection for /proc. Instead, it removes the assumption that stat.size is a hard limit on how much to read.
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.
Auto-generated from PR #4944. View all patches on GitHub.