You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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: fetch there doesn't negotiate gzip the way undici does, so sandbox.files.read('/proc/...') (and any sysfs read) returns ''. Found while running the js-sdk test suite under workerd (test(js-sdk): Cloudflare Workers smoke tests (workerd pool + real deploy) E2B#1586) — the two filesystem pause tests fail exactly this way.
Anyone using downloadUrl() with plain curl/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: gzip by default and land on the working gzip path.
Quick check from a shell, using a signed download URL:
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.
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 withAccept-Encoding: gzipreturns 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: 0on 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: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 → setsContent-Length: 0and copies exactly 0 bytes:https://github.com/e2b-dev/infra/blob/afaa636b1eb7e4508998f08b2d0a887f368ad0d6/packages/envd/internal/api/download.go#L172
The gzip path doesn't size the response up front — it streams the file through
gzip.NewWriterwith a plainio.Copy, so it serves the real content:https://github.com/e2b-dev/infra/blob/afaa636b1eb7e4508998f08b2d0a887f368ad0d6/packages/envd/internal/api/download.go#L151-L169
Reproduction (envd 0.6.10, prod
e2b.app,basetemplate, 2026-07-23)Raw HTTP/1.1 to envd on
localhost:49983from inside the sandbox (no edge proxy involved;X-Access-Tokenheader set):Identity — empty body:
Same request +
Accept-Encoding: gzip— full content:Control — regular file
/tmp/regular.txtworks fine on the identity path: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
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 (test(js-sdk): Cloudflare Workers smoke tests (workerd pool + real deploy) E2B#1586) — the two filesystem pause tests fail exactly this way.downloadUrl()with plaincurl/wget(no--compressed): empty download for proc/sysfs files.Accept-Encoding: gzipby default and land on the working gzip path.Quick check from a shell, using a signed download URL:
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: whenstat.Size() == 0, read the file (optionally through anio.LimitReadercap) into memory and serve it viahttp.ServeContentwith abytes.Reader— an actually-empty file still yields a correctContent-Length: 0, and virtual files get their real content and a correct length. Range/conditional semantics are preserved.