From 28f4e2ff01dd1b156764e64c5acfb991feace7b1 Mon Sep 17 00:00:00 2001 From: Reddimus Date: Sun, 7 Jun 2026 00:52:14 -0700 Subject: [PATCH 1/3] docs(readme): bump FetchContent GIT_TAG to latest release v0.3.1 The README's FetchContent example pinned a superseded tag; consumers copy-pasting it pulled an older release than the current one. Point it at the latest tag (matches project VERSION in CMakeLists). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81fd75e..38b05ce 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ make coverage # Code coverage report (requires lcov) include(FetchContent) FetchContent_Declare(ncei-cpp GIT_REPOSITORY https://github.com/Reddimus/ncei-cpp.git - GIT_TAG v0.3.0 + GIT_TAG v0.3.1 ) FetchContent_MakeAvailable(ncei-cpp) target_link_libraries(myapp PRIVATE ncei) From e8d5a4fb9371802165301c200e8ddba354ddc020 Mon Sep 17 00:00:00 2001 From: Reddimus Date: Sun, 7 Jun 2026 05:00:43 -0700 Subject: [PATCH 2/3] fix(rate-limit): a disabled daily limit must report unlimited, not 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit daily_requests_remaining() returned 0 when daily_limit<=0 ('no cap'), but CDOClient::do_get() rejects with quota_exceeded when remaining<=0 — so disabling the daily cap blocked EVERY request, contradicting the documented '0 = no limit' semantics. Return INT32_MAX when disabled. Regression test in NoDailyLimitWhenZero. --- src/core/rate_limit.cpp | 8 +++++++- tests/test_rate_limit.cpp | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/core/rate_limit.cpp b/src/core/rate_limit.cpp index db0c6cf..48d132a 100644 --- a/src/core/rate_limit.cpp +++ b/src/core/rate_limit.cpp @@ -1,5 +1,7 @@ #include "ncei/rate_limit.hpp" +#include + namespace ncei { RateLimiter::RateLimiter(Config config) @@ -81,7 +83,11 @@ std::uint16_t RateLimiter::available_tokens() const noexcept { std::int32_t RateLimiter::daily_requests_remaining() const noexcept { std::lock_guard lock(mutex_); if (config_.daily_limit <= 0) { - return 0; + // daily_limit <= 0 means "no daily cap" (see rate_limit.hpp). Signal + // unlimited headroom — NOT 0, which callers (CDOClient::do_get) treat + // as quota-exhausted and would reject every request when the cap is + // intentionally disabled. + return std::numeric_limits::max(); } return config_.daily_limit - daily_requests_used_; } diff --git a/tests/test_rate_limit.cpp b/tests/test_rate_limit.cpp index d48d534..450faba 100644 --- a/tests/test_rate_limit.cpp +++ b/tests/test_rate_limit.cpp @@ -1,6 +1,8 @@ #include "ncei/rate_limit.hpp" +#include #include +#include namespace ncei { namespace { @@ -127,6 +129,12 @@ TEST(RateLimiterTest, NoDailyLimitWhenZero) { EXPECT_TRUE(limiter.try_acquire()); EXPECT_TRUE(limiter.try_acquire()); EXPECT_TRUE(limiter.try_acquire()); + + // Regression: a disabled daily limit must report "unlimited" headroom, NOT + // 0. CDOClient::do_get() rejects with quota_exceeded when remaining <= 0, so + // returning 0 here would block every request whenever the cap is disabled. + EXPECT_GT(limiter.daily_requests_remaining(), 0); + EXPECT_EQ(limiter.daily_requests_remaining(), std::numeric_limits::max()); } TEST(RateLimiterTest, DailyCounterResetsOnReset) { From 2bdf99569657d24efeab16d2605298503e5edb3e Mon Sep 17 00:00:00 2001 From: Reddimus Date: Sun, 7 Jun 2026 06:43:30 -0700 Subject: [PATCH 3/3] fix(http): check curl_slist_append return (silent header loss + leak on OOM) curl_slist_append returns nullptr on allocation failure WITHOUT freeing the existing list; assigning the result straight back to `headers` leaked the prior nodes and silently dropped headers. Capture, check, free + return a network error on failure. --- src/http/client.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/http/client.cpp b/src/http/client.cpp index 5ecf595..db913ee 100644 --- a/src/http/client.cpp +++ b/src/http/client.cpp @@ -89,13 +89,27 @@ Result HttpClient::get(std::string_view path) const { } // Set Accept header and extra headers + // curl_slist_append returns nullptr on allocation failure WITHOUT freeing + // the existing list — assigning its result straight back to `headers` would + // then leak the prior nodes and silently drop headers. Capture, check, free + // + error on failure. struct curl_slist* headers = nullptr; std::string accept_header = "Accept: " + impl_->config.accept; - headers = curl_slist_append(headers, accept_header.c_str()); + struct curl_slist* appended = curl_slist_append(headers, accept_header.c_str()); + if (appended == nullptr) { + curl_slist_free_all(headers); + return std::unexpected(Error::network("failed to build request headers")); + } + headers = appended; for (const std::pair& hdr : impl_->config.extra_headers) { std::string header_line = hdr.first + ": " + hdr.second; - headers = curl_slist_append(headers, header_line.c_str()); + appended = curl_slist_append(headers, header_line.c_str()); + if (appended == nullptr) { + curl_slist_free_all(headers); + return std::unexpected(Error::network("failed to build request headers")); + } + headers = appended; } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);