Skip to content

envd: GET /files returns an empty body for procfs/sysfs files unless gzip is negotiated #3363

Description

@mishushakov

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:

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.NewWriter with a plain io.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, 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:

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions