From 320c64f3d6cc0181958237059c58205ad4158e40 Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Fri, 6 Jun 2025 13:44:54 +0200 Subject: [PATCH 1/8] 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. --- httplib.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/httplib.h b/httplib.h index 54b80d702b..bac37927d8 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,14 @@ class MultipartFormDataParser { return false; } + // split header string by ':' and emplace space trimmed into headers map + auto colon_pos = header.find(':'); + if (colon_pos != std::string::npos) { + auto key = trim_copy(header.substr(0, colon_pos)); + auto val = trim_copy(header.substr(colon_pos + 1)); + file_.headers.emplace(key, val); + } + constexpr const char header_content_type[] = "Content-Type:"; if (start_with_case_ignore(header, header_content_type)) { @@ -5144,6 +5153,7 @@ class MultipartFormDataParser { 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 { From 4ef3708b7bf6eb04bf4421441dcad5f6e8b901e1 Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Fri, 6 Jun 2025 14:51:01 +0200 Subject: [PATCH 2/8] 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. --- test/test.cc | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/test.cc b/test/test.cc index de3aae5ecd..3d400fc701 100644 --- a/test/test.cc +++ b/test/test.cc @@ -8010,6 +8010,69 @@ 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_lenght = it->second.headers.find("CONTENT-length"); + ASSERT_EQ("5", content_lenght->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 customHeader = headers.find("x-whatever"); + ASSERT_TRUE(customHeader != headers.end()); + ASSERT_NE("customvalue", customHeader->second); + ASSERT_EQ("CustomValue", customHeader->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) { From 715d2cec5569b807744dfed82be34ab281375895 Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Fri, 6 Jun 2025 14:51:30 +0200 Subject: [PATCH 3/8] Enables automatic test discovery with GoogleTest Uses `gtest_discover_tests` to automatically find and run tests, simplifying test maintenance and improving discoverability. --- test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d4e684c9b2..33a26010e5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,7 @@ find_package(CURL REQUIRED) add_executable(httplib-test test.cc include_httplib.cc $<$:include_windows_h.cc>) target_compile_options(httplib-test PRIVATE "$<$:/utf-8;/bigobj>") target_link_libraries(httplib-test PRIVATE httplib GTest::gtest_main CURL::libcurl) +include(GoogleTest) gtest_discover_tests(httplib-test) file( From 3a492c18be46c01cfa29ef7eec7e8375c99a6837 Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Sun, 8 Jun 2025 13:57:20 +0200 Subject: [PATCH 4/8] Removes explicit GoogleTest include --- test/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 33a26010e5..d4e684c9b2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,7 +29,6 @@ find_package(CURL REQUIRED) add_executable(httplib-test test.cc include_httplib.cc $<$:include_windows_h.cc>) target_compile_options(httplib-test PRIVATE "$<$:/utf-8;/bigobj>") target_link_libraries(httplib-test PRIVATE httplib GTest::gtest_main CURL::libcurl) -include(GoogleTest) gtest_discover_tests(httplib-test) file( From 0bc40d677751455cff6f381374442c837f34ca02 Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Sun, 8 Jun 2025 14:04:41 +0200 Subject: [PATCH 5/8] 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. --- httplib.h | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/httplib.h b/httplib.h index bac37927d8..8ed3e630fd 100644 --- a/httplib.h +++ b/httplib.h @@ -5046,12 +5046,14 @@ class MultipartFormDataParser { return false; } - // split header string by ':' and emplace space trimmed into headers map - auto colon_pos = header.find(':'); - if (colon_pos != std::string::npos) { - auto key = trim_copy(header.substr(0, colon_pos)); - auto val = trim_copy(header.substr(colon_pos + 1)); - file_.headers.emplace(key, val); + // 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:"; From 8b70d519104cb49ea148898c8ef5d619000b95e2 Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Sun, 8 Jun 2025 16:00:19 +0200 Subject: [PATCH 6/8] clang-format corrected --- httplib.h | 8 ++++---- test/test.cc | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/httplib.h b/httplib.h index 8ed3e630fd..022b015054 100644 --- a/httplib.h +++ b/httplib.h @@ -5048,10 +5048,10 @@ class MultipartFormDataParser { // 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); - })) { + header.data(), header.data() + header.size(), + [&](const std::string &key, const std::string &val) { + file_.headers.emplace(key, val); + })) { is_valid_ = false; return false; } diff --git a/test/test.cc b/test/test.cc index 3d400fc701..45e8eb2f92 100644 --- a/test/test.cc +++ b/test/test.cc @@ -8029,14 +8029,13 @@ TEST(MultipartFormDataTest, AccessPartHeaders) { ++it; ASSERT_EQ("text2", it->second.name); ASSERT_EQ("text2", it->second.content); - auto& headers = it->second.headers; + auto &headers = it->second.headers; ASSERT_EQ(3, headers.size()); auto customHeader = headers.find("x-whatever"); ASSERT_TRUE(customHeader != headers.end()); ASSERT_NE("customvalue", customHeader->second); ASSERT_EQ("CustomValue", customHeader->second); - ASSERT_TRUE(headers.find("X-Test") == headers.end()); //text1 header - + ASSERT_TRUE(headers.find("X-Test") == headers.end()); // text1 header handled = true; }); From 3bc6e739ffdd9e5c4d23c97a09548f49aff3501d Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Mon, 9 Jun 2025 15:42:59 +0200 Subject: [PATCH 7/8] Renames variable for better readability. Renames the `customHeader` variable to `custom_header` for improved code readability and consistency. --- test/test.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test.cc b/test/test.cc index 45e8eb2f92..bfb58aae05 100644 --- a/test/test.cc +++ b/test/test.cc @@ -8031,10 +8031,10 @@ TEST(MultipartFormDataTest, AccessPartHeaders) { ASSERT_EQ("text2", it->second.content); auto &headers = it->second.headers; ASSERT_EQ(3, headers.size()); - auto customHeader = headers.find("x-whatever"); - ASSERT_TRUE(customHeader != headers.end()); - ASSERT_NE("customvalue", customHeader->second); - ASSERT_EQ("CustomValue", customHeader->second); + 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; From f4f2992911fa3be7f0d5a01f7360ac7069b2405e Mon Sep 17 00:00:00 2001 From: Marek Kwasecki Date: Mon, 9 Jun 2025 15:44:01 +0200 Subject: [PATCH 8/8] typo --- test/test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test.cc b/test/test.cc index bfb58aae05..11f622a1f5 100644 --- a/test/test.cc +++ b/test/test.cc @@ -8022,8 +8022,8 @@ TEST(MultipartFormDataTest, AccessPartHeaders) { ASSERT_EQ("text1", it->second.name); ASSERT_EQ("text1", it->second.content); ASSERT_EQ(1, it->second.headers.count("Content-Length")); - auto content_lenght = it->second.headers.find("CONTENT-length"); - ASSERT_EQ("5", content_lenght->second); + auto content_length = it->second.headers.find("CONTENT-length"); + ASSERT_EQ("5", content_length->second); ASSERT_EQ(3, it->second.headers.size()); ++it;