From fd8da4d8e4ec84e70a0d0cfc0b623cdd5d8a776c Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Mon, 9 Jun 2025 21:59:25 +0200 Subject: [PATCH] Feature/multipart headers (#2152) * Adds headers to multipart form data Adds a `headers` field to the `MultipartFormData` struct. Populates this field by parsing headers from the multipart form data. This allows access to specific headers associated with each form data part. * Adds multipart header access test Verifies the correct retrieval of headers from multipart form data file parts. Ensures that custom and content-related headers are accessible and parsed as expected. * Enables automatic test discovery with GoogleTest Uses `gtest_discover_tests` to automatically find and run tests, simplifying test maintenance and improving discoverability. * Removes explicit GoogleTest include * Refactors header parsing logic Improves header parsing by using a dedicated parsing function, resulting in cleaner and more robust code. This change also adds error handling during header parsing, returning an error and marking the request as invalid if parsing fails. * clang-format corrected * Renames variable for better readability. Renames the `customHeader` variable to `custom_header` for improved code readability and consistency. * typo --- httplib.h | 12 ++++++++++ test/test.cc | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/httplib.h b/httplib.h index 54b80d7..022b015 100644 --- a/httplib.h +++ b/httplib.h @@ -544,6 +544,7 @@ struct MultipartFormData { std::string content; std::string filename; std::string content_type; + Headers headers; }; using MultipartFormDataItems = std::vector; using MultipartFormDataMap = std::multimap; @@ -5045,6 +5046,16 @@ public: return false; } + // parse and emplace space trimmed headers into a map + if (!parse_header( + header.data(), header.data() + header.size(), + [&](const std::string &key, const std::string &val) { + file_.headers.emplace(key, val); + })) { + is_valid_ = false; + return false; + } + constexpr const char header_content_type[] = "Content-Type:"; if (start_with_case_ignore(header, header_content_type)) { @@ -5144,6 +5155,7 @@ private: file_.name.clear(); file_.filename.clear(); file_.content_type.clear(); + file_.headers.clear(); } bool start_with_case_ignore(const std::string &a, const char *b) const { diff --git a/test/test.cc b/test/test.cc index de3aae5..11f622a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -8010,6 +8010,68 @@ TEST(MultipartFormDataTest, ContentLength) { ASSERT_TRUE(send_request(1, req, &response)); ASSERT_EQ("200", response.substr(9, 3)); } + +TEST(MultipartFormDataTest, AccessPartHeaders) { + auto handled = false; + + Server svr; + svr.Post("/test", [&](const Request &req, Response &) { + ASSERT_EQ(2u, req.files.size()); + + auto it = req.files.begin(); + ASSERT_EQ("text1", it->second.name); + ASSERT_EQ("text1", it->second.content); + ASSERT_EQ(1, it->second.headers.count("Content-Length")); + auto content_length = it->second.headers.find("CONTENT-length"); + ASSERT_EQ("5", content_length->second); + ASSERT_EQ(3, it->second.headers.size()); + + ++it; + ASSERT_EQ("text2", it->second.name); + ASSERT_EQ("text2", it->second.content); + auto &headers = it->second.headers; + ASSERT_EQ(3, headers.size()); + auto custom_header = headers.find("x-whatever"); + ASSERT_TRUE(custom_header != headers.end()); + ASSERT_NE("customvalue", custom_header->second); + ASSERT_EQ("CustomValue", custom_header->second); + ASSERT_TRUE(headers.find("X-Test") == headers.end()); // text1 header + + handled = true; + }); + + thread t = thread([&] { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + ASSERT_TRUE(handled); + }); + + svr.wait_until_ready(); + + auto req = "POST /test HTTP/1.1\r\n" + "Content-Type: multipart/form-data;boundary=--------\r\n" + "Content-Length: 232\r\n" + "\r\n----------\r\n" + "Content-Disposition: form-data; name=\"text1\"\r\n" + "Content-Length: 5\r\n" + "X-Test: 1\r\n" + "\r\n" + "text1" + "\r\n----------\r\n" + "Content-Disposition: form-data; name=\"text2\"\r\n" + "Content-Type: text/plain\r\n" + "X-Whatever: CustomValue\r\n" + "\r\n" + "text2" + "\r\n------------\r\n" + "That should be disregarded. Not even read"; + + std::string response; + ASSERT_TRUE(send_request(1, req, &response)); + ASSERT_EQ("200", response.substr(9, 3)); +} #endif TEST(TaskQueueTest, IncreaseAtomicInteger) {