add support for requests with both MultipartFormDataItems and Content Providers (#1454)
* add support for requests with both MultipartFormDataItems and ContentProviders
* rework implementation
* use const auto & and fix offset calculation
* fix zero items
* snake case variables
* clang-format
* commonize get_multipart_content_provider, add Put() with MultipartFormDataProviderItems
* fix linker multiple definition error
* add test MultipartFormDataTest.DataProviderItems
diff --git a/httplib.h b/httplib.h
index 751aa92..1d0e628 100644
--- a/httplib.h
+++ b/httplib.h
@@ -369,6 +369,14 @@
using ContentProviderResourceReleaser = std::function<void(bool success)>;
+struct MultipartFormDataProvider {
+ std::string name;
+ ContentProviderWithoutLength provider;
+ std::string filename;
+ std::string content_type;
+};
+using MultipartFormDataProviderItems = std::vector<MultipartFormDataProvider>;
+
using ContentReceiverWithProgress =
std::function<bool(const char *data, size_t data_length, uint64_t offset,
uint64_t total_length)>;
@@ -934,6 +942,9 @@
const MultipartFormDataItems &items);
Result Post(const std::string &path, const Headers &headers,
const MultipartFormDataItems &items, const std::string &boundary);
+ Result Post(const std::string &path, const Headers &headers,
+ const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items);
Result Put(const std::string &path);
Result Put(const std::string &path, const char *body, size_t content_length,
@@ -963,6 +974,9 @@
const MultipartFormDataItems &items);
Result Put(const std::string &path, const Headers &headers,
const MultipartFormDataItems &items, const std::string &boundary);
+ Result Put(const std::string &path, const Headers &headers,
+ const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items);
Result Patch(const std::string &path);
Result Patch(const std::string &path, const char *body, size_t content_length,
@@ -1201,6 +1215,9 @@
ContentProvider content_provider,
ContentProviderWithoutLength content_provider_without_length,
const std::string &content_type);
+ ContentProviderWithoutLength get_multipart_content_provider(
+ const std::string &boundary, const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items);
std::string adjust_host_string(const std::string &host) const;
@@ -1296,6 +1313,10 @@
const MultipartFormDataItems &items);
Result Post(const std::string &path, const Headers &headers,
const MultipartFormDataItems &items, const std::string &boundary);
+ Result Post(const std::string &path, const Headers &headers,
+ const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items);
+
Result Put(const std::string &path);
Result Put(const std::string &path, const char *body, size_t content_length,
const std::string &content_type);
@@ -1324,6 +1345,10 @@
const MultipartFormDataItems &items);
Result Put(const std::string &path, const Headers &headers,
const MultipartFormDataItems &items, const std::string &boundary);
+ Result Put(const std::string &path, const Headers &headers,
+ const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items);
+
Result Patch(const std::string &path);
Result Patch(const std::string &path, const char *body, size_t content_length,
const std::string &content_type);
@@ -2854,8 +2879,7 @@
}
inline bool get_ip_and_port(const struct sockaddr_storage &addr,
- socklen_t addr_len, std::string &ip,
- int &port) {
+ socklen_t addr_len, std::string &ip, int &port) {
if (addr.ss_family == AF_INET) {
port = ntohs(reinterpret_cast<const struct sockaddr_in *>(&addr)->sin_port);
} else if (addr.ss_family == AF_INET6) {
@@ -4129,29 +4153,48 @@
return valid;
}
+template <typename T>
+inline std::string
+serialize_multipart_formdata_item_begin(const T &item,
+ const std::string &boundary) {
+ std::string body = "--" + boundary + "\r\n";
+ body += "Content-Disposition: form-data; name=\"" + item.name + "\"";
+ if (!item.filename.empty()) {
+ body += "; filename=\"" + item.filename + "\"";
+ }
+ body += "\r\n";
+ if (!item.content_type.empty()) {
+ body += "Content-Type: " + item.content_type + "\r\n";
+ }
+ body += "\r\n";
+
+ return body;
+}
+
+inline std::string serialize_multipart_formdata_item_end() { return "\r\n"; }
+
+inline std::string
+serialize_multipart_formdata_finish(const std::string &boundary) {
+ return "--" + boundary + "--\r\n";
+}
+
+inline std::string
+serialize_multipart_formdata_get_content_type(const std::string &boundary) {
+ return "multipart/form-data; boundary=" + boundary;
+}
+
inline std::string
serialize_multipart_formdata(const MultipartFormDataItems &items,
- const std::string &boundary,
- std::string &content_type) {
+ const std::string &boundary, bool finish = true) {
std::string body;
for (const auto &item : items) {
- body += "--" + boundary + "\r\n";
- body += "Content-Disposition: form-data; name=\"" + item.name + "\"";
- if (!item.filename.empty()) {
- body += "; filename=\"" + item.filename + "\"";
- }
- body += "\r\n";
- if (!item.content_type.empty()) {
- body += "Content-Type: " + item.content_type + "\r\n";
- }
- body += "\r\n";
- body += item.content + "\r\n";
+ body += serialize_multipart_formdata_item_begin(item, boundary);
+ body += item.content + serialize_multipart_formdata_item_end();
}
- body += "--" + boundary + "--\r\n";
+ if (finish) body += serialize_multipart_formdata_finish(boundary);
- content_type = "multipart/form-data; boundary=" + boundary;
return body;
}
@@ -4536,8 +4579,8 @@
*reinterpret_cast<struct sockaddr_storage *>(rp->ai_addr);
std::string ip;
int dummy = -1;
- if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage),
- ip, dummy)) {
+ if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip,
+ dummy)) {
addrs.push_back(ip);
}
}
@@ -6647,6 +6690,49 @@
return true;
}
+inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider(
+ const std::string &boundary, const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items) {
+ size_t cur_item = 0, cur_start = 0;
+ // cur_item and cur_start are copied to within the std::function and maintain
+ // state between successive calls
+ return [&, cur_item, cur_start](size_t offset,
+ DataSink &sink) mutable -> bool {
+ if (!offset && items.size()) {
+ sink.os << detail::serialize_multipart_formdata(items, boundary, false);
+ return true;
+ } else if (cur_item < provider_items.size()) {
+ if (!cur_start) {
+ const auto &begin = detail::serialize_multipart_formdata_item_begin(
+ provider_items[cur_item], boundary);
+ offset += begin.size();
+ cur_start = offset;
+ sink.os << begin;
+ }
+
+ DataSink cur_sink;
+ bool has_data = true;
+ cur_sink.write = sink.write;
+ cur_sink.done = [&]() { has_data = false; };
+ cur_sink.is_writable = sink.is_writable;
+
+ if (!provider_items[cur_item].provider(offset - cur_start, cur_sink))
+ return false;
+
+ if (!has_data) {
+ sink.os << detail::serialize_multipart_formdata_item_end();
+ cur_item++;
+ cur_start = 0;
+ }
+ return true;
+ } else {
+ sink.os << detail::serialize_multipart_formdata_finish(boundary);
+ sink.done();
+ return true;
+ }
+ };
+}
+
inline bool
ClientImpl::process_socket(const Socket &socket,
std::function<bool(Stream &strm)> callback) {
@@ -6869,9 +6955,10 @@
inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
const MultipartFormDataItems &items) {
- std::string content_type;
- const auto &body = detail::serialize_multipart_formdata(
- items, detail::make_multipart_data_boundary(), content_type);
+ const auto &boundary = detail::make_multipart_data_boundary();
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
return Post(path, headers, body, content_type.c_str());
}
@@ -6882,12 +6969,25 @@
return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};
}
- std::string content_type;
- const auto &body =
- detail::serialize_multipart_formdata(items, boundary, content_type);
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
return Post(path, headers, body, content_type.c_str());
}
+inline Result
+ClientImpl::Post(const std::string &path, const Headers &headers,
+ const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items) {
+ const auto &boundary = detail::make_multipart_data_boundary();
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ return send_with_content_provider(
+ "POST", path, headers, nullptr, 0, nullptr,
+ get_multipart_content_provider(boundary, items, provider_items),
+ content_type);
+}
+
inline Result ClientImpl::Put(const std::string &path) {
return Put(path, std::string(), std::string());
}
@@ -6964,9 +7064,10 @@
inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
const MultipartFormDataItems &items) {
- std::string content_type;
- const auto &body = detail::serialize_multipart_formdata(
- items, detail::make_multipart_data_boundary(), content_type);
+ const auto &boundary = detail::make_multipart_data_boundary();
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
return Put(path, headers, body, content_type);
}
@@ -6977,12 +7078,24 @@
return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};
}
- std::string content_type;
- const auto &body =
- detail::serialize_multipart_formdata(items, boundary, content_type);
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
return Put(path, headers, body, content_type);
}
+inline Result
+ClientImpl::Put(const std::string &path, const Headers &headers,
+ const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items) {
+ const auto &boundary = detail::make_multipart_data_boundary();
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ return send_with_content_provider(
+ "PUT", path, headers, nullptr, 0, nullptr,
+ get_multipart_content_provider(boundary, items, provider_items),
+ content_type);
+}
inline Result ClientImpl::Patch(const std::string &path) {
return Patch(path, std::string(), std::string());
}
@@ -7443,7 +7556,7 @@
}
inline void SSLSocketStream::get_local_ip_and_port(std::string &ip,
- int &port) const {
+ int &port) const {
detail::get_local_ip_and_port(sock_, ip, port);
}
@@ -8147,6 +8260,12 @@
const std::string &boundary) {
return cli_->Post(path, headers, items, boundary);
}
+inline Result
+Client::Post(const std::string &path, const Headers &headers,
+ const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items) {
+ return cli_->Post(path, headers, items, provider_items);
+}
inline Result Client::Put(const std::string &path) { return cli_->Put(path); }
inline Result Client::Put(const std::string &path, const char *body,
size_t content_length,
@@ -8210,6 +8329,12 @@
const std::string &boundary) {
return cli_->Put(path, headers, items, boundary);
}
+inline Result
+Client::Put(const std::string &path, const Headers &headers,
+ const MultipartFormDataItems &items,
+ const MultipartFormDataProviderItems &provider_items) {
+ return cli_->Put(path, headers, items, provider_items);
+}
inline Result Client::Patch(const std::string &path) {
return cli_->Patch(path);
}
diff --git a/test/test.cc b/test/test.cc
index 88937ce..e592ed1 100644
--- a/test/test.cc
+++ b/test/test.cc
@@ -5146,6 +5146,204 @@
t.join();
}
+TEST(MultipartFormDataTest, DataProviderItems) {
+
+ std::random_device seed_gen;
+ std::mt19937 random(seed_gen());
+
+ std::string rand1;
+ rand1.resize(1000);
+ std::generate(rand1.begin(), rand1.end(), [&]() { return random(); });
+
+ std::string rand2;
+ rand2.resize(3000);
+ std::generate(rand2.begin(), rand2.end(), [&]() { return random(); });
+
+ SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
+
+ svr.Post("/post-none", [&](const Request &req, Response & /*res*/,
+ const ContentReader &content_reader) {
+ ASSERT_FALSE(req.is_multipart_form_data());
+
+ std::string body;
+ content_reader([&](const char *data, size_t data_length) {
+ body.append(data, data_length);
+ return true;
+ });
+
+ EXPECT_EQ(body, "");
+ });
+
+ svr.Post("/post-items", [&](const Request &req, Response & /*res*/,
+ const ContentReader &content_reader) {
+ ASSERT_TRUE(req.is_multipart_form_data());
+ MultipartFormDataItems files;
+ content_reader(
+ [&](const MultipartFormData &file) {
+ files.push_back(file);
+ return true;
+ },
+ [&](const char *data, size_t data_length) {
+ files.back().content.append(data, data_length);
+ return true;
+ });
+
+ ASSERT_TRUE(files.size() == 2);
+
+ EXPECT_EQ(std::string(files[0].name), "name1");
+ EXPECT_EQ(files[0].content, "Testing123");
+ EXPECT_EQ(files[0].filename, "filename1");
+ EXPECT_EQ(files[0].content_type, "application/octet-stream");
+
+ EXPECT_EQ(files[1].name, "name2");
+ EXPECT_EQ(files[1].content, "Testing456");
+ EXPECT_EQ(files[1].filename, "");
+ EXPECT_EQ(files[1].content_type, "");
+ });
+
+ svr.Post("/post-providers", [&](const Request &req, Response & /*res*/,
+ const ContentReader &content_reader) {
+ ASSERT_TRUE(req.is_multipart_form_data());
+ MultipartFormDataItems files;
+ content_reader(
+ [&](const MultipartFormData &file) {
+ files.push_back(file);
+ return true;
+ },
+ [&](const char *data, size_t data_length) {
+ files.back().content.append(data, data_length);
+ return true;
+ });
+
+ ASSERT_TRUE(files.size() == 2);
+
+ EXPECT_EQ(files[0].name, "name3");
+ EXPECT_EQ(files[0].content, rand1);
+ EXPECT_EQ(files[0].filename, "filename3");
+ EXPECT_EQ(files[0].content_type, "");
+
+ EXPECT_EQ(files[1].name, "name4");
+ EXPECT_EQ(files[1].content, rand2);
+ EXPECT_EQ(files[1].filename, "filename4");
+ EXPECT_EQ(files[1].content_type, "");
+ });
+
+ svr.Post("/post-both", [&](const Request &req, Response & /*res*/,
+ const ContentReader &content_reader) {
+ ASSERT_TRUE(req.is_multipart_form_data());
+ MultipartFormDataItems files;
+ content_reader(
+ [&](const MultipartFormData &file) {
+ files.push_back(file);
+ return true;
+ },
+ [&](const char *data, size_t data_length) {
+ files.back().content.append(data, data_length);
+ return true;
+ });
+
+ ASSERT_TRUE(files.size() == 4);
+
+ EXPECT_EQ(std::string(files[0].name), "name1");
+ EXPECT_EQ(files[0].content, "Testing123");
+ EXPECT_EQ(files[0].filename, "filename1");
+ EXPECT_EQ(files[0].content_type, "application/octet-stream");
+
+ EXPECT_EQ(files[1].name, "name2");
+ EXPECT_EQ(files[1].content, "Testing456");
+ EXPECT_EQ(files[1].filename, "");
+ EXPECT_EQ(files[1].content_type, "");
+
+ EXPECT_EQ(files[2].name, "name3");
+ EXPECT_EQ(files[2].content, rand1);
+ EXPECT_EQ(files[2].filename, "filename3");
+ EXPECT_EQ(files[2].content_type, "");
+
+ EXPECT_EQ(files[3].name, "name4");
+ EXPECT_EQ(files[3].content, rand2);
+ EXPECT_EQ(files[3].filename, "filename4");
+ EXPECT_EQ(files[3].content_type, "");
+ });
+
+ auto t = std::thread([&]() { svr.listen("localhost", 8080); });
+ while (!svr.is_running()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+
+ {
+ Client cli("https://localhost:8080");
+ cli.enable_server_certificate_verification(false);
+
+ MultipartFormDataItems items{
+ {"name1", "Testing123", "filename1", "application/octet-stream"},
+ {"name2", "Testing456", "", ""}, // not a file
+ };
+
+ {
+ auto res = cli.Post("/post-none", {}, {}, {});
+ ASSERT_TRUE(res);
+ ASSERT_EQ(200, res->status);
+ }
+
+ MultipartFormDataProviderItems providers;
+
+ {
+ auto res =
+ cli.Post("/post-items", {}, items, providers); // empty providers
+ ASSERT_TRUE(res);
+ ASSERT_EQ(200, res->status);
+ }
+
+ providers.push_back({"name3",
+ [&](size_t offset, httplib::DataSink &sink) -> bool {
+ // test the offset is given correctly at each step
+ if (!offset)
+ sink.os.write(rand1.data(), 30);
+ else if (offset == 30)
+ sink.os.write(rand1.data() + 30, 300);
+ else if (offset == 330)
+ sink.os.write(rand1.data() + 330, 670);
+ else if (offset == rand1.size())
+ sink.done();
+ return true;
+ },
+ "filename3",
+ {}});
+
+ providers.push_back({"name4",
+ [&](size_t offset, httplib::DataSink &sink) -> bool {
+ // test the offset is given correctly at each step
+ if (!offset)
+ sink.os.write(rand2.data(), 2000);
+ else if (offset == 2000)
+ sink.os.write(rand2.data() + 2000, 1);
+ else if (offset == 2001)
+ sink.os.write(rand2.data() + 2001, 999);
+ else if (offset == rand2.size())
+ sink.done();
+ return true;
+ },
+ "filename4",
+ {}});
+
+ {
+ auto res = cli.Post("/post-providers", {}, {}, providers);
+ ASSERT_TRUE(res);
+ ASSERT_EQ(200, res->status);
+ }
+
+ {
+ auto res = cli.Post("/post-both", {}, items, providers);
+ ASSERT_TRUE(res);
+ ASSERT_EQ(200, res->status);
+ }
+ }
+
+ svr.stop();
+ t.join();
+}
+
TEST(MultipartFormDataTest, WithPreamble) {
Server svr;
svr.Post("/post", [&](const Request & /*req*/, Response &res) {