# Running RustFS behind a reverse proxy RustFS speaks plain S3 over HTTP/1.1 and HTTP/2 and works behind reverse proxies (Caddy, Nginx, HAProxy) and CDNs (Cloudflare). Most proxy problems are **not** RustFS storage bugs — the same request sent directly to `:9000` succeeds, while the proxied request fails. This page documents the request semantics RustFS expects from the proxy layer and gives known-good configurations. > Rule of thumb: if a request works against `http://:9000` directly but > fails through the proxy, the fault is in the proxy/CDN request forwarding, not > in RustFS object handling. Use the checklist below to find which forwarding > behavior broke. ## What RustFS requires from the proxy S3 clients sign requests with AWS SigV4. RustFS (via `s3s`) re-derives the signature from the forwarded request, and streams the request body to storage. For this to succeed the proxy must forward the request **byte-for-byte** with respect to the signed material and the body: 1. **Do not alter the body.** No transparent compression, no re-encoding, no truncation. If the client sent `Content-Length: N`, exactly `N` body bytes must reach RustFS. If fewer bytes arrive, RustFS waits for the rest per the HTTP spec and the request appears to hang until the client aborts. 2. **Do not rewrite signed headers.** `Host` and any `x-amz-*` / signed headers must reach RustFS unchanged. Rewriting `Host` is fine only if the client signed with that same host. 3. **Preserve `Content-Length`; avoid re-chunking large bodies.** Some CDNs drop `Content-Length` and switch to `Transfer-Encoding: chunked`, or buffer the whole request body before forwarding — both change the timing and framing RustFS sees. 4. **Keep upstream idle keep-alive shorter than RustFS's, or vice-versa** (see next section) so the proxy never reuses a connection RustFS has already closed. 5. **Do not strip `ETag`** from responses (breaks multipart completion). ## Idle keep-alive: the #1 cause of `socket hang up` on writes RustFS closes **idle** upstream HTTP/1.1 keep-alive connections after `RUSTFS_HTTP1_HEADER_READ_TIMEOUT` seconds (default **75s**; see `crates/config/src/constants/tls.rs`). Reverse proxies keep a pool of upstream connections and reuse them. If the proxy's upstream idle-keepalive window is **longer** than RustFS's timeout, the proxy can pick a connection that RustFS has already FIN'd, write a request onto the dead socket, and the client sees: ``` TimeoutError: socket hang up # ECONNRESET AbortError: Request aborted ``` This is most visible on large `PutObject` uploads because: - `PUT` is non-idempotent, so proxies will **not** transparently retry it; and - a larger body keeps the connection in use longer, widening the race window, so small uploads on the same path often succeed. ### Fix — make the two windows agree Pick **either** side; doing both is safest: - **RustFS side:** keep `RUSTFS_HTTP1_HEADER_READ_TIMEOUT` (default 75s) *above* the proxy's upstream idle-keepalive. To harden slowloris protection on a directly-exposed node instead, lower it — but then also lower the proxy keepalive below it. - **Proxy side:** lower the proxy's upstream idle-keepalive below RustFS's timeout, or disable upstream keep-alive entirely. ## Known-good Caddy configuration ```caddy your-domain.example.com { reverse_proxy http://127.0.0.1:9000 { transport http { # Talk HTTP/1.1 to RustFS. versions 1.1 # Keep the proxy's upstream idle-keepalive BELOW RustFS's # RUSTFS_HTTP1_HEADER_READ_TIMEOUT (default 75s) so Caddy never # reuses a connection RustFS already closed. Set to 0 to disable # upstream keep-alive entirely (simplest, slightly less efficient). keepalive 30s keepalive_idle_conns_per_host 0 # Never let the proxy compress/transform the request body. compression off # Generous timeouts for multi-MB single-request PUTs. dial_timeout 30s read_timeout 300s write_timeout 300s } # Forward the body untouched; do not negotiate compression upstream. header_up Accept-Encoding identity # Preserve the host the client signed with. header_up Host {upstream_hostport} # Stream immediately instead of buffering. flush_interval -1 } } ``` ## Nginx equivalent (essentials) ```nginx location / { proxy_pass http://127.0.0.1:9000; proxy_http_version 1.1; # Nginx default upstream keepalive is 60s; keep it under RustFS's 75s. # (set `keepalive` in the matching `upstream {}` block) proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header Accept-Encoding "identity"; # Do not buffer/limit large uploads. proxy_request_buffering off; client_max_body_size 0; proxy_read_timeout 300s; proxy_send_timeout 300s; } ``` ## Cloudflare (orange-cloud) caveats Cloudflare's proxy (orange cloud) may **buffer the entire request body** before forwarding, and can rewrite requests to `Transfer-Encoding: chunked`, dropping the client's `Content-Length`. Symptoms match this pattern exactly: tiny uploads succeed, larger uploads fail with `socket hang up`. - For large object writes, prefer **DNS-only (grey cloud)** for the S3 endpoint, or a Cloudflare plan/tunnel configuration that does not buffer/re-chunk the request body. - Force `Accept-Encoding: identity` so nothing in the path negotiates compression (see issues #609, #1492). - Ensure `Content-Length` reaches RustFS; disable chunked re-encoding in tunnel settings (see issue #934). ## Diagnosis checklist Run each step and note where behavior diverges: 1. **Bypass the proxy.** Send the failing request to `http://:9000` directly. Success here confirms the fault is in the proxy/CDN path. 2. **Bypass the CDN, keep the proxy.** Point the proxy straight at the origin (Cloudflare grey cloud / direct DNS). If it now works, the CDN was buffering/re-chunking the body. 3. **Check idle reuse.** If failures are intermittent and correlate with upload size, it is almost always the keep-alive mismatch above. Lower the proxy keepalive (or disable it) and retry. - If instead the upload **hangs indefinitely** (rather than resetting), the proxy is likely forwarding a *partial* body and then going silent without closing the connection. RustFS bounds this wait with `RUSTFS_HTTP_REQUEST_BODY_READ_TIMEOUT` (default 300s; `0` disables) and, on timeout, logs a `put_object_body_read_stalled` event with the received/expected byte counts — grep the server log for it to confirm a truncated-body forwarding problem. 4. **Compare bytes.** Confirm the proxy forwards exactly `Content-Length` body bytes with no compression/transformation. 5. **Confirm signed headers survive.** `Host` and `x-amz-*` headers must reach RustFS unchanged; a `SignatureDoesNotMatch` (rather than a hang) points here. ## Related issues - #3076 — Large single-request PutObject fails behind Caddy (this document) - #609 — Bucket inaccessible via Cloudflare proxied DNS (`Accept-Encoding`) - #1492 — SigV4 `SignatureDoesNotMatch` on Cloudflare tunnel (`Accept-Encoding`) - #934 — Console fails behind Cloudflare tunnels (chunked / `Content-Length`) - #1766 — Large multipart upload fails through Nginx (`ETag` stripping)