Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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`; 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.

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] This framing claim does not match the parser. When a request has neither Content-Length nor Transfer-Encoding, s_headers_done assumes a zero-length body and invokes message_complete; it does not enter s_body_identity_eof. Please remove the read-until-close statement. Also avoid saying that no body bytes are read at all, since the socket read containing the headers can already contain body bytes; describe that over-limit advertised bodies are not parsed or stored instead.


```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
1 change: 1 addition & 0 deletions docs/guides/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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.

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

Expand Down
26 changes: 26 additions & 0 deletions include/crow/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion include/crow/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ namespace crow
size_t rule_index;
std::vector<size_t> blueprint_indices;
routing_params r_params;
HTTPMethod method;
HTTPMethod method{HTTPMethod::InternalMethodCount};

routing_handle_result() {}

Expand Down
98 changes: 94 additions & 4 deletions include/crow/http_connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -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")
{
Expand All @@ -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()
Expand All @@ -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"))
Expand Down Expand Up @@ -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<http_errno>(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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] linger_close() is bypassed when the 413 body takes the streamed response path. With app.stream_threshold(1), do_write_general() reaches its close_connection_ block and calls shutdown_readwrite() plus close() before control returns here. I reproduced this by sending only headers with Content-Length: 5000000, reading the 413, and then sending the body: asio::write fails with Broken pipe. Please apply the body-error guard already present in #1235 (close_connection_ && !body_error_status_) here as !payload_too_large_, including the static response path, and add the same regression test.

}
else
{
self->adaptor_.shutdown_read();
self->adaptor_.close();
}
}
else if (self->close_connection_)
{
Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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...>* middlewares_;
detail::context<Middlewares...> ctx_;
Expand Down
2 changes: 1 addition & 1 deletion include/crow/http_response.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 24 additions & 6 deletions include/crow/parser.h
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#pragma once

#include <algorithm>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <algorithm>

#include "crow/http_request.h"
#include "crow/http_parser_merged.h"
Expand Down Expand Up @@ -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<HTTPParser*>(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_)
Expand All @@ -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(),
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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;

Expand Down
27 changes: 27 additions & 0 deletions include/crow/routing.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
std::string rule_;
std::string name_;
bool added_{false};
std::optional<uint64_t> max_body_size_override_;

std::unique_ptr<BaseRule> rule_to_upgrade_;

Expand Down Expand Up @@ -618,6 +619,13 @@ namespace crow // NOTE: Already documented in "crow/app.h"
static_cast<self_t*>(this)->mw_indices_.template push<App, Middlewares...>();
return static_cast<self_t&>(*this);
}

/// Override the app-wide request body size limit for this route.
self_t& max_body_size(uint64_t bytes)
{
static_cast<self_t*>(this)->max_body_size_override_ = bytes;
return static_cast<self_t&>(*this);
}
};

/// A rule that can change its parameters during runtime.
Expand Down Expand Up @@ -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<int>(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<void(crow::response&)>& exception_handler()
{
return exception_handler_;
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion tests/fuzz/http_fuzzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
};

Expand Down
Loading
Loading