Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
fa21b7f
Add a request body size limit that closes over-limit connections
anton-n-petrov Aug 31, 2026
87adfe6
Add a request body sink, with body_file() as a file-backed wrapper
anton-n-petrov Aug 31, 2026
225034f
Don't allocate from untrusted Content-Length when body size is unlimited
anton-n-petrov Sep 1, 2026
71c9f86
Merge feature/max-body-size into request-body-sink
anton-n-petrov Sep 1, 2026
7767c4c
Make body_file/body_sink last-call-wins and cover sink failure paths
anton-n-petrov Sep 1, 2026
cffeec8
Rework the request body sink per review: shared handle, opt-in file h…
anton-n-petrov Sep 2, 2026
0b3a6dc
Address code review: portable file sink, drain doc, comments
anton-n-petrov Sep 3, 2026
a0e6733
Drop the Content-Length reserve() unconditionally, clear req.body on …
anton-n-petrov Sep 4, 2026
7dca1ef
Keep file_body_sink.h out of the amalgamated crow_all.h
anton-n-petrov Sep 4, 2026
ec10ad8
Fix stale mkostemp reference in body-file guide
anton-n-petrov Sep 4, 2026
2779929
Fix CIFuzz build: DummyHandler out of sync with HTTPParser handler in…
anton-n-petrov Sep 4, 2026
300378a
Address PR review: scope drain wording, make send_file test cwd-indep…
anton-n-petrov Sep 5, 2026
e4e4a26
Explicit skip_body on body_error_status_ responses, matching max-body…
anton-n-petrov Sep 5, 2026
16fa250
Address PR review: fix inaccurate drain wording, init rule_index
anton-n-petrov Sep 6, 2026
99d7368
Address PR review: linger-drain on body errors, dedupe test helpers
anton-n-petrov Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Crow is a C++ framework for creating HTTP or Websocket web services. It uses rou
- Middleware support for extensions.
- HTTP/1.1 and Websocket support.
- Multi-part request and response support.
- Request body size limit (`max_body_size`) and optional body-to-file / body-sink uploads.
- Uses modern C++ (11/14)

### Still in development
Expand Down
19 changes: 19 additions & 0 deletions docs/guides/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ app.tcp_nodelay(true)
.run();
```

## Request body size
<span class="tag">[:octicons-feed-tag-16: master](https://github.com/CrowCpp/Crow)</span>

By default Crow accepts a request body of any size. `#!cpp app.max_body_size(bytes)` sets an app-wide limit (`UINT64_MAX` is unlimited). A route can override it with `#!cpp .max_body_size(bytes)`.

The advertised `Content-Length` is checked when headers complete, before `100 Continue`. Chunked bodies are counted as they arrive. An over-limit request is answered `413 Payload Too Large`; the route handler does not run. Whether the rejection happens on the advertised `Content-Length` alone or partway through a streaming (chunked) body, the connection drains and discards whatever the client still sends rather than closing on unread bytes, which could reset a peer that is still mid-upload; the drain is bounded by the same deadline timer (`app.timeout()`, default 5s) a slow client already gets. The same cap applies to 404, 405, and slash-redirects.

```cpp
crow::SimpleApp app;
app.max_body_size(64ull * 1024 * 1024);

CROW_ROUTE(app, "/upload")
.methods(crow::HTTPMethod::Post)
.max_body_size(8ull * 1024 * 1024)
([](const crow::request& req) {
return crow::response(200);
});
```

<br><br>

For more info on middlewares, check out [this page](middleware.md).<br><br>
Expand Down
122 changes: 122 additions & 0 deletions docs/guides/body-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<span class="tag">[:octicons-feed-tag-16: master](https://github.com/CrowCpp/Crow)</span>

By default Crow stores a request body in `req.body` (`std::string`). That is
the right default for JSON and form fields, but a large upload then occupies
the same amount of RAM as the file.

A route can divert body bytes **while they arrive** to a sink instead, via
`#!cpp .body_sink(factory)`. The built-in sink is a unique file
(`crow::FileBodySink`, an opt-in header — see below); you can also supply
your own (flash, SD, a bounded buffer) when there is no writable filesystem.

The handler still runs only after the full body has been received. Writes run
synchronously on the connection's io thread, before the `Host` header check,
before middleware, and before `100 Continue`. A sink that throws, or returns
`false` from `write()`/`finish()`, ends the request as a 500 with the
connection closed; the route handler never runs.

This is the request-side counterpart of returning a file with
`response.set_static_file_info()`: the payload is never held as one string.

Size limits are `app.max_body_size()` / `.max_body_size()` — see
[Request body size](app.md#request-body-size). Over-limit requests are 413
and the connection closes; nothing the sink wrote is kept.

## File convenience

```cpp
#include "crow.h"
#include "crow/file_body_sink.h" // opt-in: not pulled in by crow.h

crow::SimpleApp app;
app.max_body_size(64ull * 1024 * 1024);

CROW_ROUTE(app, "/upload")
.methods(crow::HTTPMethod::Put, crow::HTTPMethod::Post)
.body_sink(crow::FileBodySink::factory("uploads")) // empty: system temp directory
([](const crow::request& req) {
auto* file = crow::FileBodySink::from(req);
if (!file)
return crow::response(200); // e.g. an empty PUT: see "Empty bodies" below
// req.body is empty; the bytes are in file->path()
std::ifstream in(file->path(), std::ios::binary);
// ...
return crow::response(200);
});
```

`crow::FileBodySink::factory(directory)` resolves `directory` to an absolute
path once, at the point you call it, and throws
`std::filesystem::filesystem_error` if it does not already exist — Crow does
not create it for you, and does not fail silently. Crow always generates a
unique file name (POSIX: a retried `open(O_CREAT | O_EXCL)`; Windows: a
retried `CREATE_NEW`), so concurrent requests never share a path. The
descriptor is kept open until the body is complete; the handler then reads
`file->path()`.

The file is deleted when the last `crow::request` copy that reached the
handler is destroyed — normally right after the response has been fully
sent. Call `file->keep()` if the application will use the file after that
(for example after renaming it into place); the destructor then only closes
the descriptor.

A client that disconnects before the body is complete never reaches the
handler; the partial file is removed. Bodyless GET/HEAD (and
`Content-Length: 0`) do not create a file — `crow::FileBodySink::from(req)`
returns `nullptr` in that case, same as for any route where the body didn't
go to this sink.

`crow::FileBodySink::from()` uses `dynamic_cast` and so needs RTTI; that is
why this header is opt-in and never pulled in by `crow.h` or
`crow/parser.h`. A build with `-fno-rtti` (or asio's `-DASIO_NO_TYPEID`) can
use `crow::BodySink` directly and simply not include
`crow/file_body_sink.h`.

## Custom sink

```cpp
struct FlashSink : crow::BodySink {
bool write(const char* data, std::size_t length) override;
bool finish() override;
};

CROW_ROUTE(app, "/upload")
.methods(crow::HTTPMethod::Post)
.body_sink([](const crow::request& req) -> std::unique_ptr<crow::BodySink> {
return std::make_unique<FlashSink>(req);
})
([](const crow::request&) {
return crow::response(200);
});
```

The factory runs once per request, after headers and before the body; `req`
carries the method, url, headers and `remote_ip_address` (already set, so a
factory can apply a per-peer policy), but not `body` or `body_sink` yet.
Returning `nullptr` from the factory means "no sink for this request" — the
body goes to `req.body` as usual, it is not an error. Throwing from the
factory, or from `write()`/`finish()`, is a 500.

Calling `.body_sink(...)` again on the same route replaces the factory
(last-call-wins).

## Empty bodies

A `.body_sink(...)` route only opens a sink when a body is actually coming
(`Content-Length` > 0, or chunked framing); a bodyless request never calls
the factory, so `req.body_sink` (and `crow::FileBodySink::from(req)`) is
`nullptr`. Handle that case explicitly — see the example above. Note that an
empty **chunked** body (`0\r\n\r\n`) still counts as "a body is coming" and
does open the sink (into an empty file, for the built-in one), unlike
`Content-Length: 0`.

## What this is not

`.body_sink(...)` stores the **raw** request body. `multipart/form-data` is
parsed from `req.body` by `crow::multipart::message`; a route with a sink
diverts every body regardless of `Content-Type`, so `req.body` is empty and
`crow::multipart::message` cannot be used on it. Saving individual multipart
parts as they arrive is a separate feature.

The route handler is not invoked per chunk. Incremental processing belongs in
`BodySink::write`.
3 changes: 3 additions & 0 deletions docs/guides/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ Handlers can also use information from the request by adding it as a parameter `

You can also access the URL parameters in the handler using `#!cpp req.url_params.get("param_name");`. If the parameter doesn't exist, `nullptr` is returned.<br><br>

To cap the request body for one route, use `#!cpp .max_body_size(bytes)` (see [Request body size](app.md#request-body-size)). Without a per-route value, `#!cpp app.max_body_size()` applies, including on 404 and 405.

To write a large request body to a file (or a custom sink) while it is received instead of filling `req.body`, use `#!cpp .body_sink(...)`. See [Request body files](body-file.md).

!!! note "Note &nbsp;&nbsp;&nbsp;&nbsp; <span class="tag">[:octicons-feed-tag-16: master](https://github.com/CrowCpp/Crow)</span>"

Expand Down
4 changes: 4 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ add_executable(example_file_upload example_file_upload.cpp)
add_warnings_optimizations(example_file_upload)
target_link_libraries(example_file_upload PUBLIC Crow::Crow)

add_executable(example_body_file example_body_file.cpp)
add_warnings_optimizations(example_body_file)
target_link_libraries(example_body_file PUBLIC Crow::Crow)

add_executable(example_unix_socket example_unix_socket.cpp)
add_warnings_optimizations(example_unix_socket)
target_link_libraries(example_unix_socket PUBLIC Crow::Crow)
Expand Down
38 changes: 38 additions & 0 deletions examples/example_body_file.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "crow.h"
#include "crow/file_body_sink.h"

#include <fstream>
#include <iterator>
#include <string>

int main()
{
crow::SimpleApp app;
app.max_body_size(64ull * 1024 * 1024);

// PUT or POST the raw body; Crow writes it to disk as the bytes arrive.
CROW_ROUTE(app, "/upload")
.methods(crow::HTTPMethod::Put, crow::HTTPMethod::Post)
.body_sink(crow::FileBodySink::factory("uploads")) // requires ./uploads to exist
([](const crow::request& req) {
auto* file = crow::FileBodySink::from(req);
if (!file)
{
// No incoming body (e.g. an empty PUT): the sink never opened.
crow::json::wvalue reply;
reply["bytes"] = 0;
reply["preview"] = "";
return crow::response(reply);
}

std::ifstream in(file->path(), std::ios::binary);
const std::string contents((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());

crow::json::wvalue reply;
reply["bytes"] = contents.size();
reply["preview"] = contents.substr(0, 32);
return crow::response(reply);
});

app.port(18080).multithreaded().run();
}
1 change: 1 addition & 0 deletions include/crow.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "crow/utility.h"
#include "crow/common.h"
#include "crow/http_request.h"
#include "crow/body_sink.h"
#include "crow/websocket.h"
#include "crow/parser.h"
#include "crow/http_response.h"
Expand Down
36 changes: 36 additions & 0 deletions include/crow/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <memory>
#include <future>
#include <cstdint>
#include <system_error>
#include <type_traits>
#include <thread>
#include <condition_variable>
Expand Down Expand Up @@ -349,6 +350,30 @@ namespace crow
return max_payload_;
}

/// \brief Set the maximum request body size in bytes (`UINT64_MAX` = unlimited, the default).
///
/// Applied to every request, including 404, 405, and slash-redirects.
/// Advertised `Content-Length` is checked at headers-complete, before `100 Continue`.
/// Chunked bodies are counted as they arrive. An over-limit request is answered 413
/// and the connection is closed without reading (or draining) the body.
self_t& max_body_size(uint64_t bytes)
{
max_body_size_ = bytes;
return *this;
}

/// \brief Get the app-wide request body size limit
uint64_t max_body_size() const
{
return max_body_size_;
}

/// \brief Effective body size limit for a routing result (used by the connection).
uint64_t effective_max_body_size(const routing_handle_result& found) const
{
return router_.effective_max_body_size(found, max_body_size_);
}

self_t& signal_clear()
{
signals_.clear();
Expand Down Expand Up @@ -520,6 +545,16 @@ namespace crow
return res_stream_threshold_;
}

/// \brief Create a body sink for the matched route, or nullptr for in-memory `req.body`.
std::unique_ptr<BodySink> make_body_sink(const routing_handle_result& found, const request& req) const
{
return router_.make_body_sink(found, req);
}

bool uses_body_sink(const routing_handle_result& found) const
{
return router_.uses_body_sink(found);
}

self_t& register_blueprint(Blueprint& blueprint)
{
Expand Down Expand Up @@ -914,6 +949,7 @@ namespace crow
unsigned int concurrency_ = 2;
std::atomic_bool is_bound_ = false;
uint64_t max_payload_{UINT64_MAX};
uint64_t max_body_size_{UINT64_MAX};
std::string server_name_ = std::string("Crow/") + VERSION;
std::string bindaddr_ = "0.0.0.0";
bool use_unix_ = false;
Expand Down
29 changes: 29 additions & 0 deletions include/crow/body_sink.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include <cstddef>
#include <functional>
#include <memory>

namespace crow
{
struct request;

/// Destination for request body bytes as they arrive from the parser.
///
/// `write()`/`finish()` run on the connection's io thread, at headers-complete,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] The timing contract is incorrect. Only the factory runs at headers-complete before 100 Continue; write() runs after the body starts arriving, so an Expect: 100-continue request receives the interim 100 before a later write() failure can produce 500, and finish() runs at message-complete. Please distinguish factory timing from write()/finish() here and in docs/guides/body-file.md.

/// before the `Host` check, before middleware, and before `100 Continue`. A
/// thrown exception or a `false` return is a 500 with the connection closed;
/// the route handler never runs. If `finish()` was never called, the body did
/// not arrive in full (client disconnect, over-limit body, or a write failure).
struct BodySink
{
virtual ~BodySink() = default;
virtual bool write(const char* data, std::size_t length) = 0;
virtual bool finish() = 0;
};

/// `req` carries method, url, headers and `remote_ip_address` (set before the
/// factory runs); `req.body` and `req.body_sink` are not yet populated.
/// Returning `nullptr` keeps the body in `req.body` instead of diverting it.
using BodySinkFactory = std::function<std::unique_ptr<BodySink>(const request&)>;
} // namespace crow
4 changes: 2 additions & 2 deletions include/crow/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,10 @@ namespace crow
struct routing_handle_result
{
bool catch_all{false};
size_t rule_index;
size_t rule_index{0};
std::vector<size_t> blueprint_indices;
routing_params r_params;
HTTPMethod method;
HTTPMethod method{};

routing_handle_result() {}

Expand Down
Loading
Loading