diff --git a/docs/guides/app.md b/docs/guides/app.md
index daa17217a2..afb2906062 100644
--- a/docs/guides/app.md
+++ b/docs/guides/app.md
@@ -52,6 +52,25 @@ app.tcp_nodelay(true)
.run();
```
+## Request body size
+[:octicons-feed-tag-16: master](https://github.com/CrowCpp/Crow)
+
+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`; no body bytes are read at all if it is already over the cap. Chunked bodies, and bodies with neither `Content-Length` nor `Transfer-Encoding` (read until the connection closes), are counted as they arrive instead, since their size isn't known upfront. An over-limit request is answered `413 Payload Too Large`; the body is not stored and the route handler does not run. If the body was still arriving when the cap was hit, the connection lingers rather than closing outright: the write side is shut down once the 413 is sent, and whatever the client keeps sending is drained and discarded, so a client still mid-upload isn't reset before it can read the response. The cap applies to every request that reaches this check, including matched routes, 404, 405, and slash-redirects — but not an unmatched `HEAD` or `OPTIONS` request, which is answered before headers complete and reads no body either way. Only global after-handlers run on the 413; route-local middleware and before-handlers do not.
+
+```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);
+ });
+```
+
For more info on middlewares, check out [this page](middleware.md).
diff --git a/docs/guides/routes.md b/docs/guides/routes.md
index 4552905fac..781cfc6c4f 100644
--- a/docs/guides/routes.md
+++ b/docs/guides/routes.md
@@ -82,6 +82,7 @@ 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.
+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.
!!! note "Note [:octicons-feed-tag-16: master](https://github.com/CrowCpp/Crow)"
diff --git a/include/crow/app.h b/include/crow/app.h
index 7aa60522db..4180f896aa 100644
--- a/include/crow/app.h
+++ b/include/crow/app.h
@@ -349,6 +349,31 @@ 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; the body is not stored, but leftover bytes are
+ /// drained and discarded rather than left unread.
+ 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();
@@ -914,6 +939,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;
diff --git a/include/crow/common.h b/include/crow/common.h
index a8e58f4abc..8af19afb59 100644
--- a/include/crow/common.h
+++ b/include/crow/common.h
@@ -291,7 +291,7 @@ namespace crow
size_t rule_index;
std::vector blueprint_indices;
routing_params r_params;
- HTTPMethod method;
+ HTTPMethod method{HTTPMethod::InternalMethodCount};
routing_handle_result() {}
diff --git a/include/crow/http_connection.h b/include/crow/http_connection.h
index 2ebf72a956..e500b36ed6 100644
--- a/include/crow/http_connection.h
+++ b/include/crow/http_connection.h
@@ -121,8 +121,21 @@ namespace crow
}
}
- void handle_header()
+ /// Apply `max_body_size` before 100-continue. Return 1 to skip the body (F_SKIPBODY).
+ int handle_header()
{
+ payload_too_large_ = false;
+ const uint64_t limit = handler_->effective_max_body_size(*routing_handle_result_);
+ parser_.set_max_body_size(limit);
+
+ // limit == UINT64_MAX means unlimited, so this never rejects in the default case.
+ if (parser_.content_length != CROW_ULLONG_MAX &&
+ limit != UINT64_MAX && parser_.content_length > limit)
+ {
+ payload_too_large_ = true;
+ return 1;
+ }
+
// HTTP 1.1 Expect: 100-continue
if (req_.http_ver_major == 1 && req_.http_ver_minor == 1 && get_header_value(req_.headers, "expect") == "100-continue")
{
@@ -142,6 +155,18 @@ namespace crow
need_to_call_after_handlers_ = true;
complete_request();
}
+ return 0;
+ }
+
+ void reject_payload_too_large()
+ {
+ payload_too_large_ = true;
+ handle();
+ }
+
+ bool parser_should_abort() const
+ {
+ return payload_too_large_;
}
void handle()
@@ -160,6 +185,21 @@ namespace crow
add_keep_alive_ = req_.keep_alive;
close_connection_ = req_.close_connection;
+ if (payload_too_large_)
+ {
+ req_.body.clear();
+ res = response(status::PAYLOAD_TOO_LARGE);
+ // Explicit, rather than relying on response::operator=(&&) happening to
+ // omit skip_body: a HEAD request must never get a body, even on 413.
+ res.skip_body = (req_.method == HTTPMethod::Head);
+ res.set_header("Connection", "close");
+ res.end();
+ close_connection_ = true;
+ add_keep_alive_ = false;
+ need_to_call_after_handlers_ = true;
+ complete_request();
+ return;
+ }
if (req_.check_version(1, 1)) // HTTP/1.1
{
if (!req_.headers.count("host"))
@@ -408,9 +448,20 @@ namespace crow
{
self->cancel_deadline_timer();
self->parser_.done();
- self->adaptor_.shutdown_read();
- self->adaptor_.close();
CROW_LOG_DEBUG << self << " from read(1) with description: \"" << http_errno_description(static_cast(self->parser_.http_errno)) << '\"';
+ if (self->payload_too_large_)
+ {
+ // The rejection response is already written; shut down the
+ // write side and drain whatever the client still sends
+ // instead of closing on unread bytes, which can RST a peer
+ // that is still mid-upload before it reads the response.
+ self->linger_close();
+ }
+ else
+ {
+ self->adaptor_.shutdown_read();
+ self->adaptor_.close();
+ }
}
else if (self->close_connection_)
{
@@ -431,6 +482,37 @@ namespace crow
});
}
+ /// Shut down the write side (FIN, not RST) after a rejection response and
+ /// discard whatever the client still sends, instead of closing on unread
+ /// bytes. Bounded by the same deadline timer a slow client already gets.
+ void linger_close()
+ {
+ adaptor_.shutdown_write();
+ start_deadline();
+ do_linger_read();
+ }
+
+ // Discards whatever bytes come in (the count doesn't matter - this is
+ // draining, not parsing) and keeps re-reading until the peer closes,
+ // errors, or the deadline timer set by linger_close() fires.
+ void do_linger_read()
+ {
+ auto self = this->shared_from_this();
+ adaptor_.socket().async_read_some(
+ asio::buffer(buffer_),
+ [self](const error_code& ec, std::size_t /*bytes_transferred*/) {
+ if (!ec && self->adaptor_.is_open())
+ {
+ self->do_linger_read();
+ }
+ else
+ {
+ self->cancel_deadline_timer();
+ self->adaptor_.close();
+ }
+ });
+ }
+
void do_write()
{
auto self = this->shared_from_this();
@@ -480,10 +562,17 @@ namespace crow
{
this->continue_requested = false;
}
- else
+ else if (!this->payload_too_large_)
{
this->parser_.clear();
}
+ // else: this response is the 413 itself, written while the llhttp
+ // callback that triggered the reject is still on the stack
+ // (http_parser_execute has not returned yet), so clearing here would
+ // reenter the parser mid-message. The connection is always closed
+ // afterwards (never reused for another request), so there is no
+ // keep-alive state left to reset; skip the clear instead of doing it
+ // unsafely.
return ec;
}
@@ -536,6 +625,7 @@ namespace crow
bool need_to_call_after_handlers_{};
bool need_to_start_read_after_complete_{};
bool add_keep_alive_{};
+ bool payload_too_large_{false};
std::tuple* middlewares_;
detail::context ctx_;
diff --git a/include/crow/http_response.h b/include/crow/http_response.h
index 71a9e0c7a9..61cd83625e 100644
--- a/include/crow/http_response.h
+++ b/include/crow/http_response.h
@@ -412,7 +412,7 @@ namespace crow
auto& status = statusCodes.find(code)->second;
buffers.emplace_back(status.data(), status.size());
- if (code >= 400 && body.empty())
+ if (code >= 400 && body.empty() && !skip_body)
body = statusCodes[code].substr(9);
for (auto& kv : headers)
diff --git a/include/crow/parser.h b/include/crow/parser.h
index 1417d6cb60..acf253d634 100644
--- a/include/crow/parser.h
+++ b/include/crow/parser.h
@@ -1,8 +1,9 @@
#pragma once
+#include
+#include
#include
#include
-#include
#include "crow/http_request.h"
#include "crow/http_parser_merged.h"
@@ -82,13 +83,20 @@ namespace crow
self->set_connection_parameters();
- self->process_header();
- return 0;
+ return self->process_header();
}
static int on_body(http_parser* self_, const char* at, size_t length)
{
HTTPParser* self = static_cast(self_);
+ if (self->max_body_size_ != UINT64_MAX &&
+ (length > self->max_body_size_ ||
+ self->body_bytes_ > self->max_body_size_ - length))
+ {
+ self->handler_->reject_payload_too_large();
+ return 1;
+ }
self->req.body.insert(self->req.body.end(), at, at + length);
+ self->body_bytes_ += length;
return 0;
}
static int on_message_complete(http_parser* self_)
@@ -97,7 +105,8 @@ namespace crow
self->message_complete = true;
self->process_message();
- return 0;
+ // Stop so leftover skipped-body bytes are not parsed as the next request.
+ return self->handler_->parser_should_abort() ? 1 : 0;
}
HTTPParser(Handler* handler):
http_parser(),
@@ -145,17 +154,24 @@ namespace crow
header_building_state = 0;
qs_point = 0;
message_complete = false;
+ body_bytes_ = 0;
+ max_body_size_ = UINT64_MAX;
state = CROW_NEW_MESSAGE();
}
+ void set_max_body_size(uint64_t bytes)
+ {
+ max_body_size_ = bytes;
+ }
+
inline void process_url()
{
handler_->handle_url();
}
- inline void process_header()
+ inline int process_header()
{
- handler_->handle_header();
+ return handler_->handle_header();
}
inline void process_message()
@@ -190,6 +206,8 @@ namespace crow
private:
int header_building_state = 0;
bool message_complete = false;
+ uint64_t body_bytes_{0};
+ uint64_t max_body_size_{UINT64_MAX};
std::string header_field;
std::string header_value;
diff --git a/include/crow/routing.h b/include/crow/routing.h
index 42ac0a78f9..5734e6b1ad 100644
--- a/include/crow/routing.h
+++ b/include/crow/routing.h
@@ -160,6 +160,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
std::string rule_;
std::string name_;
bool added_{false};
+ std::optional max_body_size_override_;
std::unique_ptr rule_to_upgrade_;
@@ -618,6 +619,13 @@ namespace crow // NOTE: Already documented in "crow/app.h"
static_cast(this)->mw_indices_.template push();
return static_cast(*this);
}
+
+ /// Override the app-wide request body size limit for this route.
+ self_t& max_body_size(uint64_t bytes)
+ {
+ static_cast(this)->max_body_size_override_ = bytes;
+ return static_cast(*this);
+ }
};
/// A rule that can change its parameters during runtime.
@@ -1842,6 +1850,25 @@ namespace crow // NOTE: Already documented in "crow/app.h"
return blueprints_;
}
+ /// Effective request body limit for a `handle_initial()` result.
+ ///
+ /// 404, 405, slash-redirect, and unmatched OPTIONS use `app_default`.
+ /// A matched rule uses its override when set, otherwise `app_default`.
+ uint64_t effective_max_body_size(const routing_handle_result& found, uint64_t app_default) const
+ {
+ if (found.catch_all || found.rule_index <= RULE_SPECIAL_REDIRECT_SLASH)
+ return app_default;
+ if (found.method >= HTTPMethod::InternalMethodCount)
+ return app_default;
+ const auto& rules = per_methods_[static_cast(found.method)].rules;
+ if (found.rule_index >= rules.size())
+ return app_default;
+ const BaseRule* rule = rules[found.rule_index];
+ if (!rule || !rule->max_body_size_override_)
+ return app_default;
+ return *rule->max_body_size_override_;
+ }
+
std::function& exception_handler()
{
return exception_handler_;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 00db73a7be..6773e80e74 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -22,6 +22,7 @@ enable_testing()
# list the test sources
set(TEST_SRCS
unittest.cpp
+ max_body_size_tests.cpp
query_string_tests.cpp
unit_tests/test_http_response.cpp
unit_tests/test_json.cpp
diff --git a/tests/fuzz/http_fuzzer.cpp b/tests/fuzz/http_fuzzer.cpp
index c77956ca26..5dd653a667 100644
--- a/tests/fuzz/http_fuzzer.cpp
+++ b/tests/fuzz/http_fuzzer.cpp
@@ -4,8 +4,10 @@
struct DummyHandler {
void handle_url() {}
- void handle_header() {}
+ int handle_header() { return 0; }
void handle() {}
+ void reject_payload_too_large() {}
+ bool parser_should_abort() const { return false; }
size_t stream_threshold() { return 1024*1024; }
};
diff --git a/tests/max_body_size_tests.cpp b/tests/max_body_size_tests.cpp
new file mode 100644
index 0000000000..e936a718f6
--- /dev/null
+++ b/tests/max_body_size_tests.cpp
@@ -0,0 +1,680 @@
+#define CROW_ENABLE_DEBUG
+#define CROW_LOG_LEVEL 0
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "catch2/catch_all.hpp"
+#include "crow.h"
+#include "crow/middlewares/cors.h"
+
+using namespace crow;
+
+#ifdef CROW_USE_BOOST
+namespace asio = boost::asio;
+using asio_error_code = boost::system::error_code;
+#else
+using asio_error_code = asio::error_code;
+#endif
+
+#define LOCALHOST_ADDRESS "127.0.0.1"
+
+namespace
+{
+ // Finds the final (non-1xx) status line, skipping any interim 1xx lines
+ // (e.g. "100 Continue") that may precede it. Returns npos if the data
+ // seen so far doesn't yet contain one.
+ std::size_t find_final_status_line(const std::string& data)
+ {
+ auto search_from = std::size_t{0};
+ while (true)
+ {
+ const auto status_line = data.find("HTTP/1.1 ", search_from);
+ if (status_line == std::string::npos)
+ return std::string::npos;
+ const auto code = std::atoi(data.c_str() + status_line + 9);
+ if (code >= 100 && code < 200)
+ {
+ const auto header_end = data.find("\r\n\r\n", status_line);
+ if (header_end == std::string::npos)
+ return std::string::npos;
+ search_from = header_end + 4;
+ continue;
+ }
+ return status_line;
+ }
+ }
+
+ bool response_complete(const std::string& data)
+ {
+ const auto status_line = find_final_status_line(data);
+ if (status_line == std::string::npos)
+ return false;
+ const auto header_end = data.find("\r\n\r\n", status_line);
+ if (header_end == std::string::npos)
+ return false;
+ const auto length_pos = data.find("Content-Length:", status_line);
+ if (length_pos == std::string::npos || length_pos > header_end)
+ return true;
+ const auto length = static_cast(std::stoul(data.substr(length_pos + 15)));
+ return data.size() >= header_end + 4 + length;
+ }
+
+ class TestClient
+ {
+ public:
+ TestClient(uint16_t port):
+ socket_(io_context_)
+ {
+ socket_.connect(asio::ip::tcp::endpoint(asio::ip::make_address(LOCALHOST_ADDRESS), port));
+ }
+
+ void send(const std::string& data)
+ {
+ asio::write(socket_, asio::buffer(data));
+ }
+
+ std::string receive()
+ {
+ std::string response;
+ std::array buffer{};
+ const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
+ while (!response_complete(response))
+ {
+ REQUIRE(std::chrono::steady_clock::now() < deadline);
+ asio_error_code ec;
+ const auto n = socket_.read_some(asio::buffer(buffer), ec);
+ if (ec)
+ break;
+ response.append(buffer.data(), n);
+ }
+ return response;
+ }
+
+ private:
+ asio::io_context io_context_{};
+ asio::ip::tcp::socket socket_;
+ };
+
+ int status_of(const std::string& response)
+ {
+ const auto status_line = find_final_status_line(response);
+ if (status_line == std::string::npos)
+ return 0;
+ return std::atoi(response.c_str() + status_line + 9);
+ }
+} // namespace
+
+TEST_CASE("max_body_size advertised length", "[http][max_body_size]")
+{
+ std::atomic handler_ran{false};
+ SimpleApp app;
+ app.max_body_size(8);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([&handler_ran](const request& req) {
+ handler_ran = true;
+ return req.body;
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+ const auto port = app.port();
+
+ auto post = [&](const std::string& target, const std::string& payload,
+ const std::string& extra = {}) {
+ TestClient client(port);
+ client.send(
+ "POST " + target +
+ " HTTP/1.1\r\n"
+ "Host: localhost\r\n" +
+ extra +
+ "Content-Length: " + std::to_string(payload.size()) +
+ "\r\n"
+ "\r\n" +
+ payload);
+ return client.receive();
+ };
+
+ SECTION("body of exactly max is accepted")
+ {
+ const auto resp = post("/upload", "12345678");
+ CHECK(status_of(resp) == 200);
+ CHECK(resp.find("12345678") != std::string::npos);
+ CHECK(handler_ran);
+ }
+
+ SECTION("Content-Length above the cap is 413 at headers and does not run the handler")
+ {
+ handler_ran = false;
+ const auto resp = post("/upload", "123456789");
+ CHECK(status_of(resp) == 413);
+ CHECK(resp.find("Connection: close") != std::string::npos);
+ CHECK_FALSE(handler_ran);
+ }
+
+ SECTION("Expect 100-continue is not sent for an over-limit body")
+ {
+ handler_ran = false;
+ TestClient client(port);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Expect: 100-continue\r\n"
+ "Content-Length: 5000000\r\n"
+ "\r\n");
+ const auto resp = client.receive();
+ CHECK(resp.find("100 Continue") == std::string::npos);
+ CHECK(status_of(resp) == 413);
+ CHECK_FALSE(handler_ran);
+ }
+
+ SECTION("unmatched URL still applies the app limit")
+ {
+ handler_ran = false;
+ const auto resp = post("/missing", std::string(100, 'x'));
+ CHECK(status_of(resp) == 413);
+ CHECK_FALSE(handler_ran);
+ }
+
+ SECTION("wrong method still applies the app limit")
+ {
+ handler_ran = false;
+ TestClient client(port);
+ client.send(
+ "PUT /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 100\r\n"
+ "\r\n" +
+ std::string(100, 'x'));
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 413);
+ CHECK_FALSE(handler_ran);
+ }
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size slash redirect", "[http][max_body_size]")
+{
+ std::atomic handler_ran{false};
+ SimpleApp app;
+ app.max_body_size(8);
+
+ CROW_ROUTE(app, "/probe/")
+ .methods("POST"_method)([&handler_ran] {
+ handler_ran = true;
+ return "ok";
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+
+ TestClient client(app.port());
+ client.send(
+ "POST /probe HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 100\r\n"
+ "\r\n" +
+ std::string(100, 'x'));
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 413);
+ CHECK_FALSE(handler_ran);
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size chunked accumulate", "[http][max_body_size]")
+{
+ std::atomic handler_ran{false};
+ SimpleApp app;
+ app.max_body_size(8);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([&handler_ran](const request&) {
+ handler_ran = true;
+ return "ok";
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+
+ TestClient client(app.port());
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n"
+ "5\r\nhello\r\n"
+ "5\r\nworld\r\n"
+ "0\r\n"
+ "\r\n");
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 413);
+ CHECK_FALSE(handler_ran);
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size chunked accumulate under limit succeeds", "[http][max_body_size]")
+{
+ SimpleApp app;
+ app.max_body_size(10);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([](const request& req) {
+ return req.body;
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+
+ TestClient client(app.port());
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n"
+ "5\r\nhello\r\n"
+ "0\r\n"
+ "\r\n");
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 200);
+ CHECK(resp.find("hello") != std::string::npos);
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size per-route override", "[http][max_body_size]")
+{
+ SimpleApp app;
+ app.max_body_size(8);
+
+ CROW_ROUTE(app, "/small")
+ .methods("POST"_method)([](const request& req) {
+ return req.body;
+ });
+ CROW_ROUTE(app, "/large")
+ .methods("POST"_method)
+ .max_body_size(32)([](const request& req) {
+ return req.body;
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+ const auto port = app.port();
+
+ const std::string payload(16, 'A');
+ {
+ TestClient client(port);
+ client.send(
+ "POST /small HTTP/1.1\r\nHost: localhost\r\nContent-Length: 16\r\n\r\n" + payload);
+ CHECK(status_of(client.receive()) == 413);
+ }
+ {
+ TestClient client(port);
+ client.send(
+ "POST /large HTTP/1.1\r\nHost: localhost\r\nContent-Length: 16\r\n\r\n" + payload);
+ CHECK(status_of(client.receive()) == 200);
+ }
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size zero limit", "[http][max_body_size]")
+{
+ SimpleApp app;
+ app.max_body_size(0);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([](const request& req) {
+ return std::to_string(req.body.size());
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+ const auto port = app.port();
+
+ SECTION("empty body is accepted")
+ {
+ TestClient client(port);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n");
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 200);
+ CHECK(resp.find("0") != std::string::npos);
+ }
+
+ SECTION("any non-empty body is 413")
+ {
+ TestClient client(port);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 1\r\n"
+ "\r\n"
+ "x");
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 413);
+ }
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size default unlimited", "[http][max_body_size]")
+{
+ SimpleApp app;
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([](const request& req) {
+ return std::to_string(req.body.size());
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+
+ const std::string payload(64 * 1024, 'B');
+ TestClient client(app.port());
+ client.send(
+ "POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: " +
+ std::to_string(payload.size()) + "\r\n\r\n" + payload);
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 200);
+ CHECK(resp.find("65536") != std::string::npos);
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size 413 runs after-handlers", "[http][max_body_size]")
+{
+ App app;
+ app.max_body_size(8);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([] {
+ return "ok";
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+
+ TestClient client(app.port());
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Origin: https://example.test\r\n"
+ "Content-Length: 100\r\n"
+ "\r\n" +
+ std::string(100, 'x'));
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 413);
+ CHECK(resp.find("Access-Control-Allow-Origin: *") != std::string::npos);
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size unlimited does not allocate advertised Content-Length", "[http][max_body_size]")
+{
+ SimpleApp app;
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([](const request& req) {
+ return std::to_string(req.body.size());
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+ const auto port = app.port();
+
+ // Keep this socket open so handle_header actually sees the huge advertised
+ // length. Closing immediately can hide a reserve() that only runs after
+ // the server reads the headers.
+ TestClient attacker(port);
+ attacker.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 1125899906842624\r\n"
+ "\r\n");
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+
+ TestClient client(port);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 4\r\n"
+ "\r\n"
+ "ping");
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 200);
+ CHECK(resp.find("4") != std::string::npos);
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size 413 skips before-handlers", "[http][max_body_size]")
+{
+ struct ProbeMiddleware
+ {
+ std::atomic before{false};
+ std::atomic after{false};
+
+ struct context
+ {};
+
+ void before_handle(request& /*req*/, response& /*res*/, context& /*ctx*/)
+ {
+ before = true;
+ }
+
+ void after_handle(request& /*req*/, response& /*res*/, context& /*ctx*/)
+ {
+ after = true;
+ }
+ };
+
+ App app;
+ app.max_body_size(8);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([] {
+ return "ok";
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+
+ TestClient client(app.port());
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 100\r\n"
+ "\r\n" +
+ std::string(100, 'x'));
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 413);
+
+ auto& probe = app.get_middleware();
+ CHECK_FALSE(probe.before.load());
+ CHECK(probe.after.load());
+
+ probe.before = false;
+ probe.after = false;
+ TestClient ok_client(app.port());
+ ok_client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 4\r\n"
+ "\r\n"
+ "abcd");
+ CHECK(status_of(ok_client.receive()) == 200);
+ CHECK(probe.before.load());
+ CHECK(probe.after.load());
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size over-limit upload can still be written and the 413 read back", "[http][max_body_size]")
+{
+ // Regression test: a client that writes its whole over-limit body before
+ // reading must not see its write fail (e.g. with ECONNRESET). The server
+ // must linger and keep draining the socket instead of closing on unread
+ // bytes, which can RST a peer that is still mid-upload and lose the
+ // response it already sent. A small payload cannot reproduce this: it
+ // needs to be big enough that the blocking write below has to wait on
+ // the server to keep reading, rather than completing into socket buffers
+ // before the server has even reacted.
+ SimpleApp app;
+ app.max_body_size(1024);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([](const request& req) {
+ return req.body;
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+ const auto port = app.port();
+
+ const std::string payload(5'000'000, 'x');
+ TestClient client(port);
+ const std::string request =
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: " +
+ std::to_string(payload.size()) +
+ "\r\n"
+ "\r\n" +
+ payload;
+
+ REQUIRE_NOTHROW(client.send(request));
+ CHECK(status_of(client.receive()) == 413);
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size small over-limit response does not leak the connection", "[http][max_body_size]")
+{
+ // Regression test: a 413 for a body small enough to use the sync write
+ // path (not the streamed/large-body path) must still result in the
+ // connection being closed promptly once the client goes away, rather
+ // than being left open waiting for a read that will never complete.
+ SimpleApp app;
+ app.max_body_size(8);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)([](const request& req) {
+ return req.body;
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+ const auto port = app.port();
+
+ const int baseline = crow::connectionCount;
+ {
+ TestClient client(port);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "012345678901234567");
+ CHECK(status_of(client.receive()) == 413);
+ } // client socket closes here
+
+ const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
+ while (crow::connectionCount > baseline && std::chrono::steady_clock::now() < deadline)
+ {
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ }
+ CHECK(crow::connectionCount == baseline);
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size does not apply to an unmatched HEAD/OPTIONS request", "[http][max_body_size]")
+{
+ // Documented exemption: an unmatched HEAD or OPTIONS request is answered
+ // (404/204) before headers finish parsing and before the max_body_size
+ // check runs, so an over-limit advertised length must not turn into a 413.
+ SimpleApp app;
+ app.max_body_size(8);
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+ const auto port = app.port();
+
+ {
+ TestClient client(port);
+ client.send(
+ "HEAD /missing HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 100\r\n"
+ "\r\n");
+ CHECK(status_of(client.receive()) == 404);
+ }
+ {
+ TestClient client(port);
+ client.send(
+ "OPTIONS /missing HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 100\r\n"
+ "\r\n");
+ CHECK(status_of(client.receive()) == 404);
+ }
+
+ app.stop();
+}
+
+TEST_CASE("max_body_size applies to a matched-route HEAD/OPTIONS request", "[http][max_body_size]")
+{
+ // Unlike the unmatched case above, a HEAD/OPTIONS request that matches a
+ // route reaches the max_body_size check in handle_header(). A HEAD 413
+ // must still carry no body (skip_body set explicitly in the 413 branch,
+ // not left to accident), while OPTIONS gets a normal 413 body.
+ SimpleApp app;
+ app.max_body_size(8);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("GET"_method)([] {
+ return "ok";
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(0).run_async();
+ app.wait_for_server_start();
+ const auto port = app.port();
+
+ {
+ TestClient client(port);
+ client.send(
+ "HEAD /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 100\r\n"
+ "\r\n");
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 413);
+ CHECK(resp.find("Connection: close") != std::string::npos);
+ CHECK(resp.find("Content-Length: 0") != std::string::npos);
+ const auto header_end = resp.find("\r\n\r\n");
+ REQUIRE(header_end != std::string::npos);
+ CHECK(resp.size() == header_end + 4); // no body after the headers
+ }
+ {
+ TestClient client(port);
+ client.send(
+ "OPTIONS /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 100\r\n"
+ "\r\n");
+ const auto resp = client.receive();
+ CHECK(status_of(resp) == 413);
+ CHECK(resp.find("Connection: close") != std::string::npos);
+ }
+
+ app.stop();
+}