mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
88645c9169
* fix(server): raise default HTTP/1.1 idle keep-alive timeout for proxies In hyper's HTTP/1.1 stack `header_read_timeout` is armed as soon as the connection is ready to read the next request head, so on a keep-alive connection it also bounds how long an idle upstream connection may sit before RustFS closes it. The previous 5s default meant RustFS FIN'd pooled upstream connections while reverse proxies (Caddy ~2min, Nginx 60s) still believed they were alive. The proxy then reused a dead socket and clients saw `socket hang up` / connection reset, most visibly on non-idempotent `PUT` uploads that proxies will not transparently retry (issue #3076). Raise DEFAULT_HTTP1_HEADER_READ_TIMEOUT to 75s (above common proxy idle-keepalive windows) and document the coupling. Still overridable via RUSTFS_HTTP1_HEADER_READ_TIMEOUT for deployments that expose RustFS directly to untrusted slow clients and want tighter slowloris protection. Co-Authored-By: heihutu <heihutu@gmail.com> * docs(operations): add reverse-proxy deployment guide Document the request/body semantics RustFS expects from a proxy layer, the idle keep-alive mismatch that causes `socket hang up` on large PutObject writes, and known-good Caddy/Nginx configs plus Cloudflare caveats. Closes the documentation gap called out in issue #3076 and consolidates the scattered findings from #609/#934/#1492/#1766. Co-Authored-By: heihutu <heihutu@gmail.com> * feat(object): bound stalled PutObject request-body reads with diagnostics When a reverse proxy or CDN forwards a partial request body and then goes silent without closing the connection, the inner body stream neither yields more bytes nor reports EOF, so RustFS waited forever for bytes that never arrive and the client saw a silent hang/abort (issue #3076). A short body that ends with a proper EOF was already rejected promptly; the gap was the no-EOF stall case. Add a single-point request-body guard on the PutObject path that wraps the incoming StreamingBlob with an inter-chunk read timeout. The timer resets on every chunk, so slow-but-progressing uploads are unaffected; it only fires after RUSTFS_HTTP_REQUEST_BODY_READ_TIMEOUT (default 300s, 0 disables) of complete silence. On timeout it logs a structured `put_object_body_read_stalled` event with received/expected byte counts and fails the read with ErrorKind::TimedOut instead of hanging. remaining_length/size_hint are forwarded so wrapping is transparent to downstream length handling. Covered by unit tests for the stall path, length-preserving pass-through, and the disabled (timeout=0) pass-through. Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
174 lines
7.2 KiB
Markdown
174 lines
7.2 KiB
Markdown
# 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://<host>: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://<host>: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)
|