Static file server reads whole files into memory: no streaming, no Range, no conditional requests #36

Open
opened 2026-10-20 08:25:45 +00:00 by james.oates · 0 comments
Owner

serve_from_dir (proxy.rs:1037) reads each file fully into a Vec<u8> before responding:

if let Ok(bytes) = tokio::fs::read(candidate).await {
    …
    .body(Body::from(bytes))

So peak memory is file size × concurrent requests. A few-hundred-MB asset served to several clients at once is multiple GB of RSS, and a HEAD costs the same read even though hyper discards the body.

Also absent, all consequences of the same shape:

  • No Range support — a client that asks for a byte range gets the whole file with 200, and a resumed download restarts.
  • No ETag / Last-Modified, so no conditional requests: every fetch is a full transfer and re-read.
  • No Content-Length before the read — it is only known once the file is buffered.

The fix is to stream: open the file and wrap it in a tokio_util::io::ReaderStream as the body, with Content-Length from the metadata, and handle Range for large assets. tower-http's ServeDir does all of this and is already an indirect dependency, though it would need the access-control and fallback semantics in serve_static layered around it rather than replacing them.

This is fine for the small HTML/CSS/image sites the static server was written for, and a blocker for anything large. Found while assessing ER for serving netboot images (an initrd of a few hundred MB); not fixed inline per the standing rule.

`serve_from_dir` (`proxy.rs:1037`) reads each file fully into a `Vec<u8>` before responding: ```rust if let Ok(bytes) = tokio::fs::read(candidate).await { … .body(Body::from(bytes)) ``` So peak memory is file size × concurrent requests. A few-hundred-MB asset served to several clients at once is multiple GB of RSS, and a `HEAD` costs the same read even though hyper discards the body. Also absent, all consequences of the same shape: - **No `Range` support** — a client that asks for a byte range gets the whole file with `200`, and a resumed download restarts. - **No `ETag` / `Last-Modified`**, so no conditional requests: every fetch is a full transfer and re-read. - **No `Content-Length` before the read** — it is only known once the file is buffered. The fix is to stream: open the file and wrap it in a `tokio_util::io::ReaderStream` as the body, with `Content-Length` from the metadata, and handle `Range` for large assets. `tower-http`'s `ServeDir` does all of this and is already an indirect dependency, though it would need the access-control and fallback semantics in `serve_static` layered around it rather than replacing them. This is fine for the small HTML/CSS/image sites the static server was written for, and a blocker for anything large. Found while assessing ER for serving netboot images (an initrd of a few hundred MB); not fixed inline per the standing rule.
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
IsoHex/edge-router#36
No description provided.