From 80797772f572d257a77ab31c52b1e7e190333eea Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:14:32 +0700 Subject: [PATCH 01/12] feat: add public Headers value type --- include/cpp_request/headers.hpp | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 include/cpp_request/headers.hpp diff --git a/include/cpp_request/headers.hpp b/include/cpp_request/headers.hpp new file mode 100644 index 0000000..79b26e8 --- /dev/null +++ b/include/cpp_request/headers.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +namespace cpp_request { + +class Headers { +public: + struct Field { + std::string name; + std::string value; + }; + + using const_iterator = std::vector::const_iterator; + + void add(std::string_view name, std::string_view value); + void set(std::string_view name, std::string_view value); + + [[nodiscard]] bool contains(std::string_view name) const noexcept; + [[nodiscard]] std::string_view get(std::string_view name) const noexcept; + + [[nodiscard]] std::size_t size() const noexcept { return fields_.size(); } + [[nodiscard]] bool empty() const noexcept { return fields_.empty(); } + + [[nodiscard]] const_iterator begin() const noexcept { return fields_.begin(); } + [[nodiscard]] const_iterator end() const noexcept { return fields_.end(); } + +private: + std::vector fields_; +}; + +} // namespace cpp_request From bee893e3c401b7cf0541447d991b279e7aed3e01 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:14:46 +0700 Subject: [PATCH 02/12] feat: implement owned HTTP headers --- src/headers.cpp | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/headers.cpp diff --git a/src/headers.cpp b/src/headers.cpp new file mode 100644 index 0000000..80491d8 --- /dev/null +++ b/src/headers.cpp @@ -0,0 +1,83 @@ +#include + +#include + +namespace cpp_request { +namespace { + +constexpr char ascii_lower(char ch) noexcept { + if (ch >= 'A' && ch <= 'Z') { + return static_cast(ch + ('a' - 'A')); + } + return ch; +} + +bool ascii_iequals(std::string_view lhs, std::string_view rhs) noexcept { + if (lhs.size() != rhs.size()) { + return false; + } + + for (std::size_t index = 0; index < lhs.size(); ++index) { + if (ascii_lower(lhs[index]) != ascii_lower(rhs[index])) { + return false; + } + } + return true; +} + +} // namespace + +void Headers::add(std::string_view name, std::string_view value) { + fields_.push_back(Field{std::string{name}, std::string{value}}); +} + +void Headers::set(std::string_view name, std::string_view value) { + const auto first = std::find_if( + fields_.begin(), + fields_.end(), + [name](const Field& field) { + return ascii_iequals(field.name, name); + }); + + if (first == fields_.end()) { + add(name, value); + return; + } + + first->name.assign(name.data(), name.size()); + first->value.assign(value.data(), value.size()); + + fields_.erase( + std::remove_if( + std::next(first), + fields_.end(), + [name](const Field& field) { + return ascii_iequals(field.name, name); + }), + fields_.end()); +} + +bool Headers::contains(std::string_view name) const noexcept { + return std::any_of( + fields_.begin(), + fields_.end(), + [name](const Field& field) { + return ascii_iequals(field.name, name); + }); +} + +std::string_view Headers::get(std::string_view name) const noexcept { + const auto found = std::find_if( + fields_.begin(), + fields_.end(), + [name](const Field& field) { + return ascii_iequals(field.name, name); + }); + + if (found == fields_.end()) { + return {}; + } + return found->value; +} + +} // namespace cpp_request From edf73a4b6dce2cef38a37b8289a2e61290bf2fc3 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:14:58 +0700 Subject: [PATCH 03/12] feat: add public Request model --- include/cpp_request/request.hpp | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 include/cpp_request/request.hpp diff --git a/include/cpp_request/request.hpp b/include/cpp_request/request.hpp new file mode 100644 index 0000000..f0c9841 --- /dev/null +++ b/include/cpp_request/request.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include + +namespace cpp_request { + +enum class Method { + Get, + Head, + Post, + Put, + Patch, + Delete +}; + +class Request { +public: + Request(Method method, std::string_view url) noexcept + : method_(method), url_(url) {} + + [[nodiscard]] Method method() const noexcept { return method_; } + [[nodiscard]] std::string_view url() const noexcept { return url_; } + + [[nodiscard]] Headers& headers() noexcept { return headers_; } + [[nodiscard]] const Headers& headers() const noexcept { return headers_; } + + void set_body(std::string_view body) noexcept { body_ = body; } + [[nodiscard]] std::string_view body() const noexcept { return body_; } + +private: + Method method_; + std::string_view url_; + Headers headers_; + std::string_view body_; +}; + +} // namespace cpp_request From 39421a11f823e39a3598aefc8a66a97d60ba8003 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:15:16 +0700 Subject: [PATCH 04/12] test: cover Headers ownership and lookup semantics --- tests/headers_test.cpp | 85 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/headers_test.cpp diff --git a/tests/headers_test.cpp b/tests/headers_test.cpp new file mode 100644 index 0000000..e87ef7d --- /dev/null +++ b/tests/headers_test.cpp @@ -0,0 +1,85 @@ +#include + +#include + +#include +#include +#include + +namespace { + +using cpp_request::Headers; + +TEST(HeadersTest, AddPreservesDuplicatesAndInsertionOrder) { + Headers headers; + headers.add("Set-Cookie", "a=1"); + headers.add("X-Trace", "first"); + headers.add("set-cookie", "b=2"); + + ASSERT_EQ(headers.size(), 3U); + + auto it = headers.begin(); + EXPECT_EQ(it->name, "Set-Cookie"); + EXPECT_EQ(it->value, "a=1"); + ++it; + EXPECT_EQ(it->name, "X-Trace"); + EXPECT_EQ(it->value, "first"); + ++it; + EXPECT_EQ(it->name, "set-cookie"); + EXPECT_EQ(it->value, "b=2"); +} + +TEST(HeadersTest, LookupIsAsciiCaseInsensitiveAndReturnsFirstValue) { + Headers headers; + headers.add("Content-Type", "text/plain"); + headers.add("content-type", "application/json"); + + EXPECT_TRUE(headers.contains("CONTENT-TYPE")); + EXPECT_EQ(headers.get("content-TYPE"), "text/plain"); + EXPECT_FALSE(headers.contains("Accept")); + EXPECT_TRUE(headers.get("Accept").empty()); +} + +TEST(HeadersTest, SetReplacesAllMatchingFields) { + Headers headers; + headers.add("X-Test", "one"); + headers.add("Other", "keep"); + headers.add("x-test", "two"); + + headers.set("X-TEST", "final"); + + EXPECT_EQ(headers.size(), 2U); + EXPECT_EQ(headers.get("x-test"), "final"); + + std::size_t matches = 0; + for (const auto& field : headers) { + if (field.name == "X-TEST") { + ++matches; + } + } + EXPECT_EQ(matches, 1U); +} + +TEST(HeadersTest, InsertedFieldsOwnTheirStorage) { + Headers headers; + std::string name = "X-Owned"; + std::string value = "original"; + + headers.add(name, value); + + name.assign("changed"); + value.assign("mutated"); + + EXPECT_TRUE(headers.contains("X-Owned")); + EXPECT_EQ(headers.get("X-Owned"), "original"); +} + +TEST(HeadersTest, SetAddsFieldWhenMissing) { + Headers headers; + headers.set("Accept", "application/json"); + + ASSERT_EQ(headers.size(), 1U); + EXPECT_EQ(headers.get("accept"), "application/json"); +} + +} // namespace From fc7c1b6523c72ac85955c56617836aeef05d075a Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:15:30 +0700 Subject: [PATCH 05/12] test: cover Request borrowed data and headers --- tests/request_test.cpp | 66 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/request_test.cpp diff --git a/tests/request_test.cpp b/tests/request_test.cpp new file mode 100644 index 0000000..a537e1c --- /dev/null +++ b/tests/request_test.cpp @@ -0,0 +1,66 @@ +#include + +#include + +#include + +namespace { + +using cpp_request::Method; +using cpp_request::Request; + +TEST(RequestTest, StoresMethodAndBorrowedUrl) { + std::string url = "http://example.com/items"; + Request request{Method::Get, url}; + + EXPECT_EQ(request.method(), Method::Get); + EXPECT_EQ(request.url(), url); + EXPECT_EQ(request.url().data(), url.data()); +} + +TEST(RequestTest, BodyIsBorrowedWithoutCopy) { + std::string body = R"({"name":"zuu"})"; + Request request{Method::Post, "http://example.com/items"}; + + request.set_body(body); + + EXPECT_EQ(request.body(), body); + EXPECT_EQ(request.body().data(), body.data()); +} + +TEST(RequestTest, OwnsHeaderStorageIndependentlyFromBorrowedRequestData) { + std::string header_name = "Content-Type"; + std::string header_value = "application/json"; + + Request request{Method::Post, "http://example.com/items"}; + request.headers().add(header_name, header_value); + + header_name.assign("changed"); + header_value.assign("changed"); + + EXPECT_EQ(request.headers().get("content-type"), "application/json"); +} + +TEST(RequestTest, ConstAccessorsExposeRequestState) { + Request mutable_request{Method::Patch, "http://example.com/item/1"}; + mutable_request.set_body("patch-body"); + mutable_request.headers().add("X-Test", "value"); + + const Request& request = mutable_request; + + EXPECT_EQ(request.method(), Method::Patch); + EXPECT_EQ(request.url(), "http://example.com/item/1"); + EXPECT_EQ(request.body(), "patch-body"); + EXPECT_EQ(request.headers().get("x-test"), "value"); +} + +TEST(RequestTest, SupportsAllV1Methods) { + EXPECT_EQ(Request{Method::Get, "http://example.com"}.method(), Method::Get); + EXPECT_EQ(Request{Method::Head, "http://example.com"}.method(), Method::Head); + EXPECT_EQ(Request{Method::Post, "http://example.com"}.method(), Method::Post); + EXPECT_EQ(Request{Method::Put, "http://example.com"}.method(), Method::Put); + EXPECT_EQ(Request{Method::Patch, "http://example.com"}.method(), Method::Patch); + EXPECT_EQ(Request{Method::Delete, "http://example.com"}.method(), Method::Delete); +} + +} // namespace From 7846046cf762c55d007ed21f32c9e0c53b4e3426 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:15:58 +0700 Subject: [PATCH 06/12] fix: include iterator utilities for Headers --- src/headers.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/headers.cpp b/src/headers.cpp index 80491d8..cd6a742 100644 --- a/src/headers.cpp +++ b/src/headers.cpp @@ -1,6 +1,7 @@ #include #include +#include namespace cpp_request { namespace { From ce330604feb3c2f7dedf2e21ec01523941cc1363 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:16:11 +0700 Subject: [PATCH 07/12] fix: parenthesize Request temporaries in tests --- tests/request_test.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/request_test.cpp b/tests/request_test.cpp index a537e1c..d3cd08f 100644 --- a/tests/request_test.cpp +++ b/tests/request_test.cpp @@ -55,12 +55,12 @@ TEST(RequestTest, ConstAccessorsExposeRequestState) { } TEST(RequestTest, SupportsAllV1Methods) { - EXPECT_EQ(Request{Method::Get, "http://example.com"}.method(), Method::Get); - EXPECT_EQ(Request{Method::Head, "http://example.com"}.method(), Method::Head); - EXPECT_EQ(Request{Method::Post, "http://example.com"}.method(), Method::Post); - EXPECT_EQ(Request{Method::Put, "http://example.com"}.method(), Method::Put); - EXPECT_EQ(Request{Method::Patch, "http://example.com"}.method(), Method::Patch); - EXPECT_EQ(Request{Method::Delete, "http://example.com"}.method(), Method::Delete); + EXPECT_EQ((Request{Method::Get, "http://example.com"}.method()), Method::Get); + EXPECT_EQ((Request{Method::Head, "http://example.com"}.method()), Method::Head); + EXPECT_EQ((Request{Method::Post, "http://example.com"}.method()), Method::Post); + EXPECT_EQ((Request{Method::Put, "http://example.com"}.method()), Method::Put); + EXPECT_EQ((Request{Method::Patch, "http://example.com"}.method()), Method::Patch); + EXPECT_EQ((Request{Method::Delete, "http://example.com"}.method()), Method::Delete); } } // namespace From 3c7d9b55b9a3624c18dfccd63e176865916f3052 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:16:31 +0700 Subject: [PATCH 08/12] build: compile Headers implementation --- src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a370cc9..8e864dc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(internal ${CMAKE_CURRENT_SOURCE_DIR}/dummy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/url.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/headers.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platform/native_socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platform/network_runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platform/socket_mode.cpp From a824b2ed3a06bd631835f30bd43561ea44ee0365 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:16:39 +0700 Subject: [PATCH 09/12] test: wire Headers and Request suites --- tests/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 35aa12f..e07818a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,6 +3,8 @@ find_package(Threads REQUIRED) add_executable(cpp_request_tests ${CMAKE_CURRENT_SOURCE_DIR}/url_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/headers_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/request_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/native_socket_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/resolver_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/tcp_connection_test.cpp From a2ccf98c697489ade24460778781a5ccf41594af Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:17:05 +0700 Subject: [PATCH 10/12] fix: make Headers set safe for self-referential views --- src/headers.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/headers.cpp b/src/headers.cpp index cd6a742..dbc9536 100644 --- a/src/headers.cpp +++ b/src/headers.cpp @@ -33,27 +33,30 @@ void Headers::add(std::string_view name, std::string_view value) { } void Headers::set(std::string_view name, std::string_view value) { + std::string owned_name{name}; + std::string owned_value{value}; + const auto first = std::find_if( fields_.begin(), fields_.end(), - [name](const Field& field) { - return ascii_iequals(field.name, name); + [&owned_name](const Field& field) { + return ascii_iequals(field.name, owned_name); }); if (first == fields_.end()) { - add(name, value); + fields_.push_back(Field{std::move(owned_name), std::move(owned_value)}); return; } - first->name.assign(name.data(), name.size()); - first->value.assign(value.data(), value.size()); + first->name = owned_name; + first->value = std::move(owned_value); fields_.erase( std::remove_if( std::next(first), fields_.end(), - [name](const Field& field) { - return ascii_iequals(field.name, name); + [&owned_name](const Field& field) { + return ascii_iequals(field.name, owned_name); }), fields_.end()); } From f0ee685611106ee7edb2d5626afc5472bfa257a6 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:17:31 +0700 Subject: [PATCH 11/12] fix: include move utility for Headers --- src/headers.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/headers.cpp b/src/headers.cpp index dbc9536..94eddaf 100644 --- a/src/headers.cpp +++ b/src/headers.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace cpp_request { namespace { From d4db87c35e6e680c560934c1d612375427795af1 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 22:24:16 +0700 Subject: [PATCH 12/12] ci: decouple test matrix from vcpkg bootstrap --- .github/workflows/ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a353ccf..9a0384b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,15 +21,11 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - name: Setup vcpkg - uses: lukka/run-vcpkg@v11 - - name: Configure CMake shell: bash run: | cmake -B build \ - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ - -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - name: Build Project shell: bash