Add request body file storage while receiving - #1233
Conversation
Routes can opt in with body_file() so the parser writes the request body to a unique file as bytes arrive, instead of appending them to req.body. That keeps large uploads off the heap. The file is deleted after the response unless keep_body_file() is called. A size cap (max_body_file_size) discards the partial file, consumes the rest of the body, and answers 413 without running the handler. Incomplete connections clean up the partial file. Addresses CrowCpp#1064.
|
Reviewed at First, the good news: the branch is healthy. It builds clean with My overall read is rework rather than merge, for two reasons: the patch does not close the issue it cites, and its failure policy makes the size limit cost more than having no limit at all. Details below, then a concrete proposal. Blocking1. The limit is enforced after the bytes, so 413 is an amplifier
With One detail that matters for the fix: 2. The knob guards only the opted-in route, so #1064 stays open
The spool also runs before any request validation. Crow already ships the idiom worth copying here: 3. The temporary file loses its exclusive handle
A second pathname race survives even after fixing the first: the parser closes the file at The Windows branch ( 4. A failed final flush is reported as a successful upload
5. Neither middleware nor after-handlers run for a rejected body
Non-blocking, but worth fixing
API surfaceLoad-bearing, and worth keeping: The rest reads as internals in public headers:
For comparison, the response side solves the same problem and keeps it private: Two smaller idiom points: What I would merge insteadStep 1, its own PR: a general request body limitThis is the actual fix for #1064, and it is small. // app.h, next to websocket_max_payload (app.h:340)
self_t& max_body_size(uint64_t bytes); // UINT64_MAX = unlimited, matching max_payload_
uint64_t max_body_size() const;
// routing.h, on RuleParameterTraits, mirroring WebSocketRule::max_payload (routing.h:478)
self_t& max_body_size(uint64_t bytes); // per-route overrideMechanism, in
One plumbing note: Step 2: the spool, on an internal sink seamKeep // routing.h, on RuleParameterTraits
self_t& body_file(); // app directory
self_t& body_file(std::string directory); // optional, if a case for it turns up
// http_request.h
std::string body_file_path; // empty => the body is in `body`
bool has_body_file() const;
std::string take_body_file(); // transfers ownership; the parser will not delete itRequired properties, each answering one of the issues above:
That lets Step 3, separate proposal: a public streaming sinkA user callable invoked from TestsThe four cases pass and cover real ground: memory vs file, 64 KiB across multiple reads, binary, empty, chunked,
Three smaller test-side notes: Thanks for the patch, and for the care that clearly went into the routing and lifetime plumbing. The core of it (route opts in, the parser diverts the body, the file is cleaned up on every termination path) is sound. My argument is only that the size limit belongs on every request rather than on this one route, that the limit has to be decided at headers-complete and close the connection instead of draining, and that the spool should own its descriptor. |
gittiver
left a comment
There was a problem hiding this comment.
wouldn't it be better to use a stream interface instead of a file (there is not always a file system available especially on embedded systems or otherwise limited systems)?
|
Superseded by two PRs, per the reviews below.
This follows @ssubbotin's review here (#1233 (comment)), which asked for the limit and the spool to be split into their own PRs, and @gittiver's comment here (#1233 (review)), which asked for a stream interface instead of a file-only approach — |
Addresses #1064.
Why this is needed
Crow currently appends every request body into
req.body(std::string). That is the right default for JSON and form fields, but a GB-class upload then occupies the same amount of RAM as the file. There is no existing pull request that writes the request body to disk while it is received (searched open/closed PRs for body-to-file, streaming upload, and large request body). The closest issue is #1064.Receiving a body already has a read loop; the only change is to divert each decoded body span to a file instead of
string::insert. Writes are still synchronous and run on the connection executor, same as today's string append.API
.body_file(<directory>)overrides the app directory for that route. Crow always generates a unique name, so concurrent requests do not share a path.req.keep_body_file()keeps the file after the response. Otherwise it is deleted once the connection is done with the request.Behaviour
Expect: 100-continuestill works; the file is opened after the 100 response.max_body_file_sizewould be exceeded, the partial file is discarded, remaining body bytes are consumed so keep-alive stays in sync, and the client gets413without the handler running.req.body.This stores the raw body.
multipart/form-datais still parsed fromreq.body. Saving individual multipart parts to disk as they arrive is a separate feature.Coverage and documentation
Tests cover in-memory vs file routes, a 64 KiB payload received in multiple reads, binary data, empty bodies, chunked request bodies,
100-continue,keep_body_file(), a route-level directory, keep-alive reuse,413, and disconnect cleanup.The guide is
docs/guides/body-file.md.examples/example_body_file.cppshows a raw PUT/POST upload.