diff --git a/README.md b/README.md
index 8edcf2909f..1bdd0bbf96 100644
--- a/README.md
+++ b/README.md
@@ -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 files for large uploads (write to disk while receiving).
- Uses modern C++ (11/14)
### Still in development
diff --git a/docs/guides/body-file.md b/docs/guides/body-file.md
new file mode 100644
index 0000000000..3bfe059472
--- /dev/null
+++ b/docs/guides/body-file.md
@@ -0,0 +1,60 @@
+[:octicons-feed-tag-16: master](https://github.com/CrowCpp/Crow)
+
+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 ask Crow to write the body to a unique file **while the bytes
+arrive**. `req.body` stays empty; the handler uses `req.body_file_path`.
+
+This is the request-side counterpart of returning a file with
+`response.set_static_file_info()`: the payload is never held as one string.
+
+## Route option
+
+```cpp
+CROW_ROUTE(app, "/upload")
+ .methods(crow::HTTPMethod::Put, crow::HTTPMethod::Post)
+ .body_file()([](const crow::request& req) {
+ if (!req.has_body_file())
+ return crow::response(500);
+ // req.body is empty; the bytes are in req.body_file_path
+ std::ifstream in(req.body_file_path, std::ios::binary);
+ // ...
+ return crow::response(200);
+ });
+```
+
+Pass a directory to `.body_file()` to override the app default for
+that route. Crow always generates a unique file name, so concurrent requests
+do not share a path.
+
+## App options
+
+```cpp
+app.body_file_directory("uploads"); // default: system temp directory
+app.max_body_file_size(64ull * 1024 * 1024); // default: 0 = unlimited
+```
+
+When the written size would exceed `max_body_file_size()`, Crow discards the
+partial file, still consumes the rest of the request so keep-alive stays in
+sync, and responds `413 Payload Too Large` without running the handler.
+
+## Lifetime
+
+The file is deleted after the response is sent. Call `req.keep_body_file()`
+if the application will use the file after the handler returns (for example
+after renaming it into place). Copy `body_file_path` if another thread will
+open it.
+
+A client that disconnects before the body is complete never reaches the
+handler; the partial file is removed.
+
+## What this is not
+
+`.body_file()` stores the **raw** request body. `multipart/form-data` is still
+parsed from `req.body` by `crow::multipart::message`. Saving individual
+multipart parts to disk as they arrive is a separate feature.
+
+The handler still runs only after the full body has been received. The gain
+is bounded memory, not an incremental handler API.
diff --git a/docs/guides/routes.md b/docs/guides/routes.md
index 4552905fac..15f0bd9d31 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 write a large request body to a file while it is received (instead of filling `req.body`), use `#!cpp .body_file()` on the route. See [Request body files](body-file.md).
!!! note "Note [:octicons-feed-tag-16: master](https://github.com/CrowCpp/Crow)"
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index f564ae5323..0d38aca4ad 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -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)
diff --git a/examples/example_body_file.cpp b/examples/example_body_file.cpp
new file mode 100644
index 0000000000..969208c528
--- /dev/null
+++ b/examples/example_body_file.cpp
@@ -0,0 +1,29 @@
+#include "crow.h"
+
+#include
+#include
+#include
+
+int main()
+{
+ crow::SimpleApp app;
+ app.body_file_directory("uploads").max_body_file_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_file()([](const crow::request& req) {
+ if (!req.has_body_file())
+ return crow::response(500);
+
+ std::ifstream in(req.body_file_path, std::ios::binary);
+ const std::string contents((std::istreambuf_iterator(in)), std::istreambuf_iterator());
+
+ crow::json::wvalue reply;
+ reply["bytes"] = req.body_file_size;
+ reply["preview"] = contents.substr(0, 32);
+ return crow::response(reply);
+ });
+
+ app.port(18080).multithreaded().run();
+}
diff --git a/include/crow/app.h b/include/crow/app.h
index 7aa60522db..b4ba5f42f6 100644
--- a/include/crow/app.h
+++ b/include/crow/app.h
@@ -520,6 +520,51 @@ namespace crow
return res_stream_threshold_;
}
+ /// \brief Directory used for request body files created by routes that call `body_file()`.
+ ///
+ /// Empty (the default) uses the system temporary directory. Unique names are generated per request.
+ self_t& body_file_directory(std::string directory)
+ {
+ body_file_directory_ = std::move(directory);
+ return *this;
+ }
+
+ const std::string& body_file_directory() const
+ {
+ return body_file_directory_;
+ }
+
+ /// \brief Maximum number of bytes written to a request body file (0 = unlimited, the default).
+ ///
+ /// A body that exceeds the limit is discarded and the client receives HTTP 413.
+ /// Remaining body bytes are still consumed so a keep-alive connection stays in sync.
+ self_t& max_body_file_size(uint64_t bytes)
+ {
+ max_body_file_size_ = bytes;
+ return *this;
+ }
+
+ uint64_t max_body_file_size() const
+ {
+ return max_body_file_size_;
+ }
+
+ /// \brief Whether the matched route writes the request body to a file (used by the connection).
+ bool should_save_body_to_file(const routing_handle_result& found)
+ {
+ BaseRule* rule = router_.get_rule(found);
+ return rule && rule->stores_body_in_file();
+ }
+
+ /// \brief Create a unique path for a request body file (used by the connection).
+ std::string create_body_file_path(const routing_handle_result& found)
+ {
+ BaseRule* rule = router_.get_rule(found);
+ std::string directory = body_file_directory_;
+ if (rule && !rule->body_file_directory().empty())
+ directory = rule->body_file_directory();
+ return utility::create_temporary_file(directory);
+ }
self_t& register_blueprint(Blueprint& blueprint)
{
@@ -920,6 +965,8 @@ namespace crow
detail::socket::tcp_socket_options tcp_socket_options_{};
detail::socket::tcp_socket_options websocket_tcp_socket_options_{};
size_t res_stream_threshold_ = 1048576;
+ std::string body_file_directory_;
+ uint64_t max_body_file_size_{0};
Router router_;
bool static_routes_added_{false};
diff --git a/include/crow/http_connection.h b/include/crow/http_connection.h
index 2ebf72a956..5c4190f1c0 100644
--- a/include/crow/http_connection.h
+++ b/include/crow/http_connection.h
@@ -141,6 +141,17 @@ namespace crow
parser_.done();
need_to_call_after_handlers_ = true;
complete_request();
+ return;
+ }
+ if (handler_->should_save_body_to_file(*routing_handle_result_))
+ {
+ const std::string path = handler_->create_body_file_path(*routing_handle_result_);
+ if (!parser_.open_body_file(path, handler_->max_body_file_size()))
+ {
+ req_.body_error_code = status::INTERNAL_SERVER_ERROR;
+ parser_.discard_remaining_body();
+ CROW_LOG_ERROR << "Failed to open request body file";
+ }
}
}
@@ -160,7 +171,12 @@ namespace crow
add_keep_alive_ = req_.keep_alive;
close_connection_ = req_.close_connection;
- if (req_.check_version(1, 1)) // HTTP/1.1
+ if (req_.body_error_code)
+ {
+ is_invalid_request = true;
+ res = response(req_.body_error_code);
+ }
+ else if (req_.check_version(1, 1)) // HTTP/1.1
{
if (!req_.headers.count("host"))
{
diff --git a/include/crow/http_request.h b/include/crow/http_request.h
index ab526f3eef..fe5405366f 100644
--- a/include/crow/http_request.h
+++ b/include/crow/http_request.h
@@ -51,6 +51,9 @@ namespace crow // NOTE: Already documented in "crow/app.h"
query_string url_params; ///< The parameters associated with the request. (everything after the `?` in the URL)
ci_map headers;
std::string body;
+ std::string body_file_path; ///< Path of the file that received the body, if the route used `body_file()`. Empty when the body is in `body`.
+ uint64_t body_file_size{}; ///< Number of bytes written to `body_file_path` when the body was stored on disk.
+ int body_error_code{}; ///< Non-zero when the body could not be stored (`413` or `500`); the handler is not invoked.
std::string remote_ip_address; ///< The IP address from which the request was sent.
unsigned char http_ver_major, http_ver_minor;
bool keep_alive, ///< Whether or not the server should send a `connection: Keep-Alive` header to the client.
@@ -60,6 +63,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
void* middleware_context{};
void* middleware_container{};
asio::io_context* io_context{};
+ mutable bool persist_body_file{false}; ///< When true, a body file is not deleted after the request. Set via `keep_body_file()`.
/// Construct an empty request. (sets the method to `GET`)
request():
@@ -91,12 +95,29 @@ namespace crow // NOTE: Already documented in "crow/app.h"
/// Get the body as parameters in QS format.
///
- /// This is meant to be used with requests of type "application/x-www-form-urlencoded"
+ /// This is meant to be used with requests of type "application/x-www-form-urlencoded".
+ /// It reads `body`; it does not open `body_file_path`.
const query_string get_body_params() const
{
return query_string(body, false);
}
+ /// True when the request body was written to `body_file_path` instead of `body`.
+ bool has_body_file() const
+ {
+ return !body_file_path.empty();
+ }
+
+ /// Keep the body file after the response is sent.
+
+ ///
+ /// Without this call the file is deleted once the connection is done with the request.
+ /// The handler must copy `body_file_path` if it will use the file after returning.
+ void keep_body_file() const
+ {
+ persist_body_file = true;
+ }
+
/// Send data to whoever made this request with a completion handler and return immediately.
template
void post(CompletionHandler handler)
diff --git a/include/crow/parser.h b/include/crow/parser.h
index 1417d6cb60..b517ec29eb 100644
--- a/include/crow/parser.h
+++ b/include/crow/parser.h
@@ -1,8 +1,13 @@
#pragma once
+#include
+#include
+#include
+#include
+#include
#include
+#include
#include
-#include
#include "crow/http_request.h"
#include "crow/http_parser_merged.h"
@@ -88,6 +93,26 @@ namespace crow
static int on_body(http_parser* self_, const char* at, size_t length)
{
HTTPParser* self = static_cast(self_);
+ if (self->body_discard_)
+ return 0;
+ if (self->body_file_)
+ {
+ if (self->max_body_file_size_ != 0 &&
+ (length > self->max_body_file_size_ ||
+ self->body_bytes_ > self->max_body_file_size_ - length))
+ {
+ self->fail_body_file(status::PAYLOAD_TOO_LARGE);
+ return 0;
+ }
+ self->body_file_->write(at, static_cast(length));
+ if (!*self->body_file_)
+ {
+ self->fail_body_file(status::INTERNAL_SERVER_ERROR);
+ return 0;
+ }
+ self->body_bytes_ += length;
+ return 0;
+ }
self->req.body.insert(self->req.body.end(), at, at + length);
return 0;
}
@@ -95,6 +120,9 @@ namespace crow
{
HTTPParser* self = static_cast(self_);
+ self->close_body_file_stream();
+ if (!self->req.body_file_path.empty())
+ self->req.body_file_size = self->body_bytes_;
self->message_complete = true;
self->process_message();
return 0;
@@ -106,6 +134,40 @@ namespace crow
http_parser_init(this);
}
+ ~HTTPParser()
+ {
+ cleanup_body_file(!req.persist_body_file);
+ }
+
+ /// Open `path` and write subsequent body bytes there instead of `req.body`.
+ bool open_body_file(const std::string& path, uint64_t max_size)
+ {
+ if (path.empty())
+ return false;
+
+ cleanup_body_file(true);
+ body_file_ = std::make_unique(path, std::ios::binary | std::ios::out | std::ios::trunc);
+ if (!body_file_ || !body_file_->is_open())
+ {
+ body_file_.reset();
+ std::error_code ec;
+ std::filesystem::remove(path, ec);
+ return false;
+ }
+
+ req.body_file_path = path;
+ max_body_file_size_ = max_size;
+ body_bytes_ = 0;
+ body_discard_ = false;
+ return true;
+ }
+
+ /// Stop storing the remainder of the body (do not append it to `req.body`).
+ void discard_remaining_body()
+ {
+ body_discard_ = true;
+ }
+
// return false on error
/// Parse a buffer into the different sections of an HTTP request.
bool feed(const char* buffer, int length)
@@ -139,12 +201,16 @@ namespace crow
void clear()
{
+ cleanup_body_file(!req.persist_body_file);
req = crow::request();
header_field.clear();
header_value.clear();
header_building_state = 0;
qs_point = 0;
message_complete = false;
+ body_discard_ = false;
+ body_bytes_ = 0;
+ max_body_file_size_ = 0;
state = CROW_NEW_MESSAGE();
}
@@ -188,8 +254,40 @@ namespace crow
request req;
private:
+ void close_body_file_stream()
+ {
+ if (!body_file_)
+ return;
+ body_file_->flush();
+ body_file_->close();
+ body_file_.reset();
+ }
+
+ void cleanup_body_file(bool remove_file)
+ {
+ close_body_file_stream();
+ if (remove_file && !req.body_file_path.empty())
+ {
+ std::error_code ec;
+ std::filesystem::remove(req.body_file_path, ec);
+ req.body_file_path.clear();
+ req.body_file_size = 0;
+ }
+ }
+
+ void fail_body_file(int code)
+ {
+ body_discard_ = true;
+ cleanup_body_file(true);
+ req.body_error_code = code;
+ }
+
int header_building_state = 0;
bool message_complete = false;
+ bool body_discard_{false};
+ uint64_t body_bytes_{0};
+ uint64_t max_body_file_size_{0};
+ std::unique_ptr body_file_;
std::string header_field;
std::string header_value;
diff --git a/include/crow/routing.h b/include/crow/routing.h
index 42ac0a78f9..123704938e 100644
--- a/include/crow/routing.h
+++ b/include/crow/routing.h
@@ -154,12 +154,26 @@ namespace crow // NOTE: Already documented in "crow/app.h"
const std::string& rule() { return rule_; }
+ /// True when this route writes the request body to a file while it is received.
+ bool stores_body_in_file() const
+ {
+ return body_file_;
+ }
+
+ /// Directory for this route's body files; empty uses the app default or the system temp directory.
+ const std::string& body_file_directory() const
+ {
+ return body_file_directory_;
+ }
+
protected:
uint64_t methods_{1ULL << static_cast(HTTPMethod::Get)};
std::string rule_;
std::string name_;
bool added_{false};
+ bool body_file_{false};
+ std::string body_file_directory_;
std::unique_ptr rule_to_upgrade_;
@@ -618,6 +632,20 @@ namespace crow // NOTE: Already documented in "crow/app.h"
static_cast(this)->mw_indices_.template push();
return static_cast(*this);
}
+
+ /// Write the request body to a unique file while it is received.
+
+ ///
+ /// `req.body` stays empty. The handler reads `req.body_file_path` instead.
+ /// An empty `directory` uses `app.body_file_directory()`, or the system
+ /// temporary directory. The file is deleted after the response unless the
+ /// handler calls `req.keep_body_file()`.
+ self_t& body_file(std::string directory = {})
+ {
+ static_cast(this)->body_file_ = true;
+ static_cast(this)->body_file_directory_ = std::move(directory);
+ return static_cast(*this);
+ }
};
/// A rule that can change its parameters during runtime.
@@ -1842,6 +1870,19 @@ namespace crow // NOTE: Already documented in "crow/app.h"
return blueprints_;
}
+ /// Matched rule for a handle_initial() result, or nullptr when no application rule applied.
+ BaseRule* get_rule(const routing_handle_result& found)
+ {
+ if (found.catch_all || found.rule_index <= RULE_SPECIAL_REDIRECT_SLASH)
+ return nullptr;
+ if (found.method >= HTTPMethod::InternalMethodCount)
+ return nullptr;
+ auto& rules = per_methods_[static_cast(found.method)].rules;
+ if (found.rule_index >= rules.size())
+ return nullptr;
+ return rules[found.rule_index];
+ }
+
std::function& exception_handler()
{
return exception_handler_;
diff --git a/include/crow/utility.h b/include/crow/utility.h
index a6f550aae0..4f5659d9cf 100644
--- a/include/crow/utility.h
+++ b/include/crow/utility.h
@@ -13,11 +13,18 @@
#include
#include
#include
+#include
+#include
#include "crow/settings.h"
#include
+#ifndef _WIN32
+#include
+#include
+#endif
+
// TODO(EDev): Adding C++20's [[likely]] and [[unlikely]] attributes might be useful
#if defined(__GNUG__) || defined(__clang__)
#define CROW_LIKELY(X) __builtin_expect(!!(X), 1)
@@ -804,6 +811,45 @@ namespace crow
return (std::filesystem::path(path) / fname).string();
}
+ /// Create an empty file with a unique name in `directory`.
+
+ ///
+ /// An empty `directory` uses the system temporary directory. Returns an empty
+ /// string if the file cannot be created.
+ inline static std::string create_temporary_file(const std::string& directory, const std::string& prefix = "crow-body-")
+ {
+ std::error_code ec;
+ const std::filesystem::path dir = directory.empty() ? std::filesystem::temp_directory_path(ec) : std::filesystem::path(directory);
+ if (ec)
+ return {};
+ std::filesystem::create_directories(dir, ec);
+ if (ec)
+ return {};
+
+ for (int attempt = 0; attempt < 128; ++attempt)
+ {
+ const auto path = dir / (prefix + random_alphanum(16));
+#ifndef _WIN32
+ const int fd = ::open(path.string().c_str(), O_CREAT | O_EXCL | O_WRONLY, 0600);
+ if (fd != -1)
+ {
+ ::close(fd);
+ return path.string();
+ }
+#else
+ if (std::filesystem::exists(path, ec))
+ continue;
+ std::ofstream file(path, std::ios::binary | std::ios::out | std::ios::trunc);
+ if (file)
+ {
+ file.close();
+ return path.string();
+ }
+#endif
+ }
+ return {};
+ }
+
/**
* @brief Checks two string for equality.
* Always returns false if strings differ in size.
diff --git a/mkdocs.yml b/mkdocs.yml
index e5013dbb99..eae6f97c38 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -70,6 +70,7 @@ nav:
- SBOM Generation: guides/sbom.md
- SSL: guides/ssl.md
- Static Files: guides/static.md
+ - Request body files: guides/body-file.md
- Blueprints: guides/blueprints.md
- Compression: guides/compression.md
- Websockets: guides/websockets.md
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 00db73a7be..beb86bfd67 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -22,6 +22,7 @@ enable_testing()
# list the test sources
set(TEST_SRCS
unittest.cpp
+ body_file_tests.cpp
query_string_tests.cpp
unit_tests/test_http_response.cpp
unit_tests/test_json.cpp
diff --git a/tests/body_file_tests.cpp b/tests/body_file_tests.cpp
new file mode 100644
index 0000000000..c92422e0f4
--- /dev/null
+++ b/tests/body_file_tests.cpp
@@ -0,0 +1,391 @@
+#define CROW_ENABLE_DEBUG
+#define CROW_LOG_LEVEL 0
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "catch2/catch_all.hpp"
+#include "crow.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
+{
+ std::string read_all(const std::string& path)
+ {
+ std::ifstream in(path, std::ios::binary);
+ return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator());
+ }
+
+ bool wait_until_removed(const std::string& path, std::chrono::milliseconds timeout)
+ {
+ const auto deadline = std::chrono::steady_clock::now() + timeout;
+ while (std::filesystem::exists(path))
+ {
+ if (std::chrono::steady_clock::now() >= deadline)
+ return false;
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ }
+ return true;
+ }
+
+ bool directory_is_empty(const std::filesystem::path& path)
+ {
+ std::error_code ec;
+ return std::filesystem::directory_iterator(path, ec) == std::filesystem::directory_iterator();
+ }
+
+ bool response_complete(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 false;
+ const auto code = std::atoi(data.c_str() + status_line + 9);
+ const auto header_end = data.find("\r\n\r\n", status_line);
+ if (header_end == std::string::npos)
+ return false;
+ if (code >= 100 && code < 200)
+ {
+ search_from = header_end + 4;
+ continue;
+ }
+ 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;
+ }
+ }
+
+ std::string http_body(const std::string& response)
+ {
+ auto search_from = std::size_t{0};
+ while (true)
+ {
+ const auto status_line = response.find("HTTP/1.1 ", search_from);
+ if (status_line == std::string::npos)
+ return {};
+ const auto code = std::atoi(response.c_str() + status_line + 9);
+ const auto header_end = response.find("\r\n\r\n", status_line);
+ if (header_end == std::string::npos)
+ return {};
+ if (code >= 100 && code < 200)
+ {
+ search_from = header_end + 4;
+ continue;
+ }
+ return response.substr(header_end + 4);
+ }
+ }
+
+ 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));
+ }
+
+ void send(const char* data, std::size_t size)
+ {
+ asio::write(socket_, asio::buffer(data, size));
+ }
+
+ 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);
+ }
+ REQUIRE(response_complete(response));
+ return response;
+ }
+
+ asio::ip::tcp::socket& socket() { return socket_; }
+
+ private:
+ asio::io_context io_context_{};
+ asio::ip::tcp::socket socket_;
+ };
+
+ struct TempDir
+ {
+ std::filesystem::path path;
+
+ TempDir():
+ path(std::filesystem::temp_directory_path() / ("crow-body-file-tests-" + utility::random_alphanum(12)))
+ {
+ std::filesystem::create_directories(path);
+ }
+
+ ~TempDir()
+ {
+ std::error_code ec;
+ std::filesystem::remove_all(path, ec);
+ }
+ };
+} // namespace
+
+TEST_CASE("request_body_file", "[http][body_file]")
+{
+ TempDir dir;
+ SimpleApp app;
+ app.body_file_directory(dir.path.string()).max_body_file_size(1024 * 1024);
+
+ CROW_ROUTE(app, "/memory")
+ .methods("POST"_method)([](const request& req) {
+ return std::string(req.has_body_file() ? "file" : "memory") + ':' + req.body;
+ });
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)
+ .body_file()([](const request& req) {
+ return std::string(req.has_body_file() ? "file:" : "memory:") +
+ std::to_string(req.body.size()) + ':' +
+ std::to_string(req.body_file_size) + ':' +
+ req.body_file_path + ':' +
+ read_all(req.body_file_path);
+ });
+
+ CROW_ROUTE(app, "/keep")
+ .methods("POST"_method)
+ .body_file()([](const request& req) {
+ req.keep_body_file();
+ return req.body_file_path;
+ });
+
+ CROW_ROUTE(app, "/custom")
+ .methods("POST"_method)
+ .body_file((dir.path / "route-dir").string())([](const request& req) {
+ return req.body_file_path;
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(45580).run_async();
+ app.wait_for_server_start();
+
+ auto post = [](const std::string& target, const std::string& payload,
+ const std::string& extra_headers = {}) {
+ TestClient client(45580);
+ client.send(
+ "POST " + target +
+ " HTTP/1.1\r\n"
+ "Host: localhost\r\n" +
+ extra_headers +
+ "Content-Length: " + std::to_string(payload.size()) +
+ "\r\n"
+ "\r\n");
+ if (!payload.empty())
+ client.send(payload.data(), payload.size());
+ return client.receive();
+ };
+
+ const auto memory = post("/memory", "hello");
+ CHECK(memory.find("HTTP/1.1 200") != std::string::npos);
+ CHECK(http_body(memory) == "memory:hello");
+
+ const std::string payload(64 * 1024, 'A');
+ const auto uploaded = post("/upload", payload);
+ REQUIRE(uploaded.find("HTTP/1.1 200") != std::string::npos);
+ const auto uploaded_body = http_body(uploaded);
+ REQUIRE(uploaded_body.rfind("file:0:" + std::to_string(payload.size()) + ':', 0) == 0);
+ const auto path_start = uploaded_body.find(':', 7) + 1;
+ const auto path_end = uploaded_body.find(':', path_start);
+ REQUIRE(path_end != std::string::npos);
+ const auto path = uploaded_body.substr(path_start, path_end - path_start);
+ CHECK(path.find(dir.path.string()) != std::string::npos);
+ CHECK(uploaded_body.substr(path_end + 1) == payload);
+ CHECK(wait_until_removed(path, std::chrono::seconds(2)));
+
+ const std::string binary(std::string("a") + '\0' + "b" + '\xff' + "c");
+ const auto binary_response = post("/upload", binary);
+ REQUIRE(binary_response.find("HTTP/1.1 200") != std::string::npos);
+ const auto binary_body = http_body(binary_response);
+ CHECK(binary_body.substr(binary_body.rfind(':') + 1) == binary);
+
+ const auto empty = post("/upload", "");
+ REQUIRE(empty.find("HTTP/1.1 200") != std::string::npos);
+ CHECK(http_body(empty).rfind("file:0:0:", 0) == 0);
+
+ {
+ TestClient client(45580);
+ 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"
+ "6\r\n world\r\n"
+ "0\r\n\r\n");
+ const auto response = client.receive();
+ REQUIRE(response.find("HTTP/1.1 200") != std::string::npos);
+ CHECK(http_body(response).find("hello world") != std::string::npos);
+ }
+
+ const auto continued = post("/upload", "ping", "Expect: 100-continue\r\n");
+ REQUIRE(continued.find("100 Continue") != std::string::npos);
+ REQUIRE(continued.find("HTTP/1.1 200") != std::string::npos);
+ CHECK(http_body(continued).find("ping") != std::string::npos);
+
+ {
+ const auto response = post("/keep", "abc");
+ auto kept = http_body(response);
+ while (!kept.empty() && (kept.back() == '\n' || kept.back() == '\r'))
+ kept.pop_back();
+ REQUIRE(std::filesystem::exists(kept));
+ CHECK(read_all(kept) == "abc");
+ std::filesystem::remove(kept);
+ }
+
+ {
+ const auto response = post("/custom", "Z");
+ auto custom_path = http_body(response);
+ while (!custom_path.empty() && (custom_path.back() == '\n' || custom_path.back() == '\r'))
+ custom_path.pop_back();
+ CHECK(custom_path.find((dir.path / "route-dir").string()) != std::string::npos);
+ }
+
+ {
+ TestClient client(45580);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 4\r\n"
+ "\r\n"
+ "one!");
+ const auto first = client.receive();
+ CHECK(http_body(first).find("one!") != std::string::npos);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 5\r\n"
+ "\r\n"
+ "two!!");
+ const auto second = client.receive();
+ CHECK(http_body(second).find("two!!") != std::string::npos);
+ }
+
+ app.stop();
+ server.wait();
+}
+
+TEST_CASE("request_body_file_too_large", "[http][body_file]")
+{
+ TempDir dir;
+ std::atomic handler_ran{false};
+ SimpleApp app;
+ app.body_file_directory(dir.path.string()).max_body_file_size(8);
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)
+ .body_file()([&handler_ran](const request&) {
+ handler_ran = true;
+ return "ran";
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(45581).run_async();
+ app.wait_for_server_start();
+
+ TestClient client(45581);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 9\r\n"
+ "\r\n"
+ "123456789");
+ const auto response = client.receive();
+ CHECK(response.find("HTTP/1.1 413") != std::string::npos);
+ CHECK_FALSE(handler_ran.load());
+ CHECK(directory_is_empty(dir.path));
+
+ app.stop();
+ server.wait();
+}
+
+TEST_CASE("request_body_file_disconnect_cleans_up", "[http][body_file]")
+{
+ TempDir dir;
+ std::atomic handler_ran{false};
+ SimpleApp app;
+ app.body_file_directory(dir.path.string());
+
+ CROW_ROUTE(app, "/upload")
+ .methods("POST"_method)
+ .body_file()([&handler_ran](const request&) {
+ handler_ran = true;
+ return "ran";
+ });
+
+ auto server = app.bindaddr(LOCALHOST_ADDRESS).port(45582).run_async();
+ app.wait_for_server_start();
+
+ {
+ TestClient client(45582);
+ client.send(
+ "POST /upload HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Content-Length: 64\r\n"
+ "\r\n"
+ "partial");
+ }
+
+ const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
+ bool empty = false;
+ while (std::chrono::steady_clock::now() < deadline)
+ {
+ empty = directory_is_empty(dir.path);
+ if (empty)
+ break;
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ }
+ CHECK(empty);
+ CHECK_FALSE(handler_ran.load());
+
+ app.stop();
+ server.wait();
+}
+
+TEST_CASE("create_temporary_file", "[utility][body_file]")
+{
+ TempDir dir;
+ const auto first = utility::create_temporary_file(dir.path.string());
+ const auto second = utility::create_temporary_file(dir.path.string());
+ REQUIRE_FALSE(first.empty());
+ REQUIRE_FALSE(second.empty());
+ CHECK(first != second);
+ CHECK(std::filesystem::exists(first));
+ CHECK(std::filesystem::file_size(first) == 0);
+}