envd: GET /files returns an empty body for procfs/sysfs files unless gzip is negotiated
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 1.6k
- Forks
- 438
- PR merge metrics
- No merged PRs in 30d
Description
Summary
GET /files (envd's file download endpoint) returns an empty 200 response for procfs/sysfs files (e.g. /proc/sys/kernel/random/boot_id) whenever the client does not negotiate gzip. The same request with Accept-Encoding: gzip returns the full file content. So the response body depends on content negotiation, and non-gzip clients silently read virtual files as empty.
The failure is silent: Content-Length: 0 on a 200 looks exactly like a legitimately empty file, and the SDKs treat it as one (files.read() returns '').
Root cause
Virtual files report size 0 from stat/Seek(0, SeekEnd) even though reading them yields content:
$ stat -c 'size=%s' /proc/sys/kernel/random/boot_id && cat /proc/sys/kernel/random/boot_id
size=0
459d0c2e-ee00-49cd-b3ff-71d4ce0b3cde
The identity path of the handler delegates to http.ServeContent, which determines the response size by seeking to the end of the file → gets 0 → sets Content-Length: 0 and copies exactly 0 bytes:
The gzip path doesn't size the response up front — it streams the file through gzip.NewWriter with a plain io.Copy, so it serves the real content:
Reproduction (envd 0.6.10, prod e2b.app, base template, 2026-07-23)
Raw HTTP/1.1 to envd on localhost:49983 from inside the sandbox (no edge proxy involved; X-Access-Token header set):
Identity — empty body:
GET /files?path=/proc/sys/kernel/random/boot_id&username=user
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Disposition: inline; filename=boot_id
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Last-Modified: Mon, 13 Jul 2026 10:52:37 GMT
Vary: Accept-Encoding
<0 body bytes>
Same request + Accept-Encoding: gzip — full content:
HTTP/1.1 200 OK
Content-Disposition: inline; filename=boot_id
Content-Encoding: gzip
Content-Type: application/octet-stream
Vary: Accept-Encoding
Content-Length: 61
<61 gzip bytes, decode to "459d0c2e-ee00-49cd-b3ff-71d4ce0b3cde\n">
Control — regular file /tmp/regular.txt works fine on the identity path:
HTTP/1.1 200 OK
Content-Length: 21
regular file content
The same behavior is observable externally through the edge (https://49983-<sandboxId>.e2b.app/files?...): identity → Content-Length: 0, empty; gzip → chunked, full content.
Impact
- Cloudflare Workers / workerd users:
fetchthere doesn't negotiate gzip the way undici does, sosandbox.files.read('/proc/...')(and any sysfs read) returns''. Found while running the js-sdk test suite under workerd (e2b-dev/E2B#1586) — the two filesystem pause tests fail exactly this way. - Anyone using
downloadUrl()with plaincurl/wget(no--compressed): empty download for proc/sysfs files. - Node (undici) and Python (httpx) SDKs are unaffected only by accident — their HTTP clients send
Accept-Encoding: gzipby default and land on the working gzip path.
Quick check from a shell, using a signed download URL:
curl "$URL" # → empty
curl --compressed "$URL" # → 459d0c2e-ee00-49cd-b3ff-71d4ce0b3cde
Suggested fix
Don't trust the seek-derived size when stat.Size() == 0. Since a genuinely empty file and a virtual file are indistinguishable without reading, one option: when stat.Size() == 0, read the file (optionally through an io.LimitReader cap) into memory and serve it via http.ServeContent with a bytes.Reader — an actually-empty file still yields a correct Content-Length: 0, and virtual files get their real content and a correct length. Range/conditional semantics are preserved.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/envd/internal/api/download.go, especially the identity path around the linked ServeContent call and the gzip path for comparison. Reproduce the request against a procfs or sysfs file without gzip, then verify that the identity response contains the virtual file content and correct length while regular files and genuinely empty files still behave correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100