mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-28 12:40:54 +00:00
Add public MultipartFormDataWriter for multipart body serialization
Building a multipart/form-data body outside Client::Post/Put/Patch (e.g. to feed a custom content provider or compose with other body sources) previously required calling several detail:: functions directly, which is not a stable interface. Introduce MultipartFormDataWriter, a small public class that owns the boundary and delegates to the existing detail:: serializers: whole-body serialization with known content length, and per-part framing (item_begin/item_end/finish) for streaming. Also expose is_valid_multipart_boundary() so callers using an explicit boundary can validate it without exceptions.
This commit is contained in:
1 parent
8149bb38fc
commit
9e8b960e44
2 files changed
+157
No files matched your search
@@ -1680,6 +1680,34 @@ make_multipart_content_provider(const UploadFormDataItems &items,
|
|||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
|
bool is_valid_multipart_boundary(const std::string &boundary);
|
||||||
|
|
||||||
|
// Serializer for multipart/form-data request bodies. The boundary is owned
|
||||||
|
// by the writer so that per-part framing and the final terminator always
|
||||||
|
// agree. Field names and filenames are escaped following the WHATWG HTML
|
||||||
|
// standard ('"' -> %22, CR -> %0D, LF -> %0A).
|
||||||
|
class MultipartFormDataWriter {
|
||||||
|
public:
|
||||||
|
MultipartFormDataWriter();
|
||||||
|
// precondition: is_valid_multipart_boundary(boundary)
|
||||||
|
explicit MultipartFormDataWriter(std::string boundary);
|
||||||
|
|
||||||
|
const std::string &boundary() const;
|
||||||
|
std::string content_type() const;
|
||||||
|
|
||||||
|
// In-memory items -> whole body (known length)
|
||||||
|
std::string serialize(const UploadFormDataItems &items) const;
|
||||||
|
size_t content_length(const UploadFormDataItems &items) const;
|
||||||
|
|
||||||
|
// Per-part framing for streaming via a content provider
|
||||||
|
std::string item_begin(const UploadFormData &item) const;
|
||||||
|
static std::string item_end();
|
||||||
|
std::string finish() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string boundary_;
|
||||||
|
};
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
public:
|
public:
|
||||||
using Handler = std::function<void(const Request &, Response &)>;
|
using Handler = std::function<void(const Request &, Response &)>;
|
||||||
@@ -10002,6 +10030,48 @@ inline size_t MultipartFormData::get_file_count(const std::string &key) const {
|
|||||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
return static_cast<size_t>(std::distance(r.first, r.second));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multipart FormData writer implementation
|
||||||
|
inline bool is_valid_multipart_boundary(const std::string &boundary) {
|
||||||
|
return detail::is_multipart_boundary_chars_valid(boundary);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline MultipartFormDataWriter::MultipartFormDataWriter()
|
||||||
|
: boundary_(detail::make_multipart_data_boundary()) {}
|
||||||
|
|
||||||
|
inline MultipartFormDataWriter::MultipartFormDataWriter(std::string boundary)
|
||||||
|
: boundary_(std::move(boundary)) {}
|
||||||
|
|
||||||
|
inline const std::string &MultipartFormDataWriter::boundary() const {
|
||||||
|
return boundary_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string MultipartFormDataWriter::content_type() const {
|
||||||
|
return detail::serialize_multipart_formdata_get_content_type(boundary_);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string
|
||||||
|
MultipartFormDataWriter::serialize(const UploadFormDataItems &items) const {
|
||||||
|
return detail::serialize_multipart_formdata(items, boundary_);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline size_t MultipartFormDataWriter::content_length(
|
||||||
|
const UploadFormDataItems &items) const {
|
||||||
|
return detail::get_multipart_content_length(items, boundary_);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string
|
||||||
|
MultipartFormDataWriter::item_begin(const UploadFormData &item) const {
|
||||||
|
return detail::serialize_multipart_formdata_item_begin(item, boundary_);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string MultipartFormDataWriter::item_end() {
|
||||||
|
return detail::serialize_multipart_formdata_item_end();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string MultipartFormDataWriter::finish() const {
|
||||||
|
return detail::serialize_multipart_formdata_finish(boundary_);
|
||||||
|
}
|
||||||
|
|
||||||
// Response implementation
|
// Response implementation
|
||||||
inline size_t Response::get_header_value_u64(const std::string &key, size_t def,
|
inline size_t Response::get_header_value_u64(const std::string &key, size_t def,
|
||||||
size_t id) const {
|
size_t id) const {
|
||||||
|
|||||||
@@ -6591,6 +6591,93 @@ TEST(MultipartFormDataTest, FieldEscaping) {
|
|||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(MultipartFormDataTest, PublicWriterAPI) {
|
||||||
|
const UploadFormDataItems items = {
|
||||||
|
{"name1", "Content 1", "", ""},
|
||||||
|
{"name2", "Content 2", "file2.txt", "text/plain"},
|
||||||
|
};
|
||||||
|
|
||||||
|
Server svr;
|
||||||
|
|
||||||
|
svr.Post("/post", [&](const Request &req, Response & /*res*/,
|
||||||
|
const ContentReader &content_reader) {
|
||||||
|
ASSERT_TRUE(req.is_multipart_form_data());
|
||||||
|
std::vector<FormData> received;
|
||||||
|
content_reader(
|
||||||
|
[&](const FormData &file) {
|
||||||
|
received.push_back(file);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[&](const char *data, size_t data_length) {
|
||||||
|
received.back().content.append(data, data_length);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ASSERT_EQ(2U, received.size());
|
||||||
|
EXPECT_EQ("name1", received[0].name);
|
||||||
|
EXPECT_EQ("Content 1", received[0].content);
|
||||||
|
EXPECT_EQ("name2", received[1].name);
|
||||||
|
EXPECT_EQ("Content 2", received[1].content);
|
||||||
|
EXPECT_EQ("file2.txt", received[1].filename);
|
||||||
|
EXPECT_EQ("text/plain", received[1].content_type);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port("localhost");
|
||||||
|
auto t = std::thread([&]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
Client cli("localhost", port);
|
||||||
|
|
||||||
|
// Whole-body serialization with a generated boundary
|
||||||
|
{
|
||||||
|
MultipartFormDataWriter writer;
|
||||||
|
EXPECT_TRUE(is_valid_multipart_boundary(writer.boundary()));
|
||||||
|
EXPECT_EQ("multipart/form-data; boundary=" + writer.boundary(),
|
||||||
|
writer.content_type());
|
||||||
|
|
||||||
|
auto body = writer.serialize(items);
|
||||||
|
EXPECT_EQ(body.size(), writer.content_length(items));
|
||||||
|
|
||||||
|
auto res = cli.Post("/post", body, writer.content_type());
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-part framing with a custom boundary via a content provider
|
||||||
|
{
|
||||||
|
EXPECT_FALSE(is_valid_multipart_boundary("bad boundary"));
|
||||||
|
ASSERT_TRUE(is_valid_multipart_boundary("custom-boundary_123"));
|
||||||
|
|
||||||
|
MultipartFormDataWriter writer("custom-boundary_123");
|
||||||
|
EXPECT_EQ("custom-boundary_123", writer.boundary());
|
||||||
|
|
||||||
|
std::string body;
|
||||||
|
for (const auto &item : items) {
|
||||||
|
body += writer.item_begin(item);
|
||||||
|
body += item.content;
|
||||||
|
body += MultipartFormDataWriter::item_end();
|
||||||
|
}
|
||||||
|
body += writer.finish();
|
||||||
|
EXPECT_EQ(body.size(), writer.content_length(items));
|
||||||
|
|
||||||
|
auto res = cli.Post(
|
||||||
|
"/post", body.size(),
|
||||||
|
[&](size_t offset, size_t length, DataSink &sink) {
|
||||||
|
sink.write(body.data() + offset, length);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
writer.content_type());
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ServerTest, PostContentReceiverGzip) {
|
TEST_F(ServerTest, PostContentReceiverGzip) {
|
||||||
cli_.set_compress(true);
|
cli_.set_compress(true);
|
||||||
auto res = cli_.Post("/content_receiver", "content", "text/plain");
|
auto res = cli_.Post("/content_receiver", "content", "text/plain");
|
||||||
|
|||||||
Reference in new issue
Block a user