From d9788169f22e4c31638c31198cfd910e2c12c9eb Mon Sep 17 00:00:00 2001 From: Martyn Date: Tue, 28 Jul 2026 07:08:51 +0100 Subject: [PATCH] Adds documentation for behaviour of capped uploads in the event of the byte limit being reached AI-Tool: Claude Code --- CHANGELOG.md | 2 + website/docs/04-requests.md | 70 ++++++++++++++++++++++++++++++++ website/docs/10-configuration.md | 5 +++ 3 files changed, 77 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18e72a89..5e3d6cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 original client address. Only enable this when all connections arrive through a trusted proxy that always sends the preamble; connections without a valid preamble are rejected, as the protocol specification requires. +- Documentation for capped uploads explaining expected behavior for HTTP/1.1 + connections in the event of the upload byte limit being reached. [PROXY protocol]: https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt diff --git a/website/docs/04-requests.md b/website/docs/04-requests.md index 7ca6d14d..dc86a13d 100644 --- a/website/docs/04-requests.md +++ b/website/docs/04-requests.md @@ -907,6 +907,76 @@ willing to accept from the client when `open`ing a data stream. The such a value as idiomatic as `128.kibibytes()`. ::: +### Capped Uploads + +A guard that reaches its limit doesn't have to fail outright. +[`Capped`](https://docs.rs/rkt/latest/rkt/data/struct.Capped.html) records how many bytes were +read and whether the value is complete, so a route can decide for itself what an +over-sized upload means: + +```rust +# #[macro_use] extern crate rkt; + +use rkt::data::Capped; +use rkt::fs::TempFile; +use rkt::http::Status; + +#[post("/upload", data = "")] +async fn upload(mut file: Capped>) -> std::io::Result { + if !file.is_complete() { + return Ok(Status::PayloadTooLarge); + } + + # let permanent_location = "/tmp/perm.txt"; + file.persist_to(permanent_location).await?; + Ok(Status::Ok) +} +``` + +Responding this way means responding early. The guard stopped reading at the +limit, so the rest of the body is still on its way. HTTP/1.1 has no way to +cancel a single request, so the only way to stop the client sending is to close +the connection. [RFC 9110 §15.5.14] says as much about `413`: + +> The server MAY terminate the request, if the protocol version in use allows +> it; otherwise, the server MAY close the connection. + +Closing mid-upload risks losing the response, as [RFC 9112 §9.6] describes: + +> If a server performs an immediate close of a TCP connection, there is a +> significant risk that the client will not be able to read the last HTTP +> response. [...] the reset packet might erase the client's unacknowledged input +> buffers before they can be read and interpreted by the client's HTTP parser. + +So the `413` your application produced and logged may never reach the client. +Firefox reports the reset as `NS_ERROR_NET_RESET`, and some browsers retry the +upload. Whether it happens depends on how much body is left and how quickly it +arrives, so a route that reliably returns `413` on `localhost` can still fail +for users on slower connections. The staged close RFC 9112 §9.6 recommends — +half-close, then keep reading — only helps if the client finishes uploading +quickly, so it is not a general fix. + +If clients must see the rejection, don't respond mid-body: + + * **Serve over HTTP/2.** It cancels one request without disturbing the + connection or the response. Per [RFC 9113 §8.1], a server may send + `RST_STREAM` with `NO_ERROR` after a complete response, and clients "MUST + NOT discard responses" because of it. rkt enables HTTP/2 by default; + browsers use it only over TLS. + * **Reject before the body arrives.** A [request guard](#request-guards) that + checks `Content-Length` runs before any data guard. A client that sent + `Expect: 100-continue` ([RFC 9110 §10.1.1]) then gets the final status + instead of a `100 (Continue)` and never sends the body. Browsers don't send + `Expect`, so this helps API clients like `curl`. + * **Check the size in the browser** before starting the upload. + * **Raise the limit and reject after reading the whole body.** A response sent + once the body is consumed is delivered normally. + +[RFC 9110 §10.1.1]: https://www.rfc-editor.org/rfc/rfc9110.html#section-10.1.1 +[RFC 9110 §15.5.14]: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.14 +[RFC 9112 §9.6]: https://www.rfc-editor.org/rfc/rfc9112.html#section-9.6 +[RFC 9113 §8.1]: https://www.rfc-editor.org/rfc/rfc9113.html#section-8.1 + ## Forms Forms are one of the most common types of data handled in web applications, and diff --git a/website/docs/10-configuration.md b/website/docs/10-configuration.md index 9f3979e4..d92b25ea 100644 --- a/website/docs/10-configuration.md +++ b/website/docs/10-configuration.md @@ -245,6 +245,11 @@ also choose to have a configure limit via the `limits` parameter. The [`Json`](https://docs.rs/rkt/latest/rkt/serde/json/struct.Json.html) type, for instance, uses the `limits.json` parameter. +A guard that hits its limit stops reading: the client keeps sending the rest of +the body, but the server never reads it. See +[capped uploads](./requests/#capped-uploads) for what that means for the +response the client receives. + ### TLS Rocket includes built-in, native support for TLS >= 1.2 (Transport Layer