Skip to content

feat(cpp): gaia::HttpClient — a general HTTP client abstraction - #2809

Open
kovtcharov wants to merge 1 commit into
mainfrom
cpp/http-client
Open

feat(cpp): gaia::HttpClient — a general HTTP client abstraction#2809
kovtcharov wants to merge 1 commit into
mainfrom
cpp/http-client

Conversation

@kovtcharov

Copy link
Copy Markdown
Contributor

Before: a C++ tool author who needed to call any HTTP service had to vendor their own client — the framework's only client was LemonadeClient's private, Lemonade-shaped httpGet/httpPost/httpPostStreaming, and cpp-httplib is a PRIVATE CMake dependency consumers cannot see. After: gaia::HttpClient gives them get/post/postStreaming with header maps, per-request timeouts and TLS, and every failure raises HttpError naming the URL and the failure mode (with status()/body() for programmatic handling) instead of a silent empty response. The transport stays private — including gaia/http_client.h pulls in no 10k-line header, enforced by a compile-time #error in the test file. LemonadeClient now sits on top of it, with its timeouts, URL normalization and SSE [DONE] handling preserved.

Two user-visible improvements fall out of the refactor: a non-2xx streaming body is no longer fed to SseParser (it is reported through HttpError instead), and an https:// URL on an HTTP-only build now says how to fix it rather than "SSL not supported."

Closes #2790.

Test plan

  • cmake -S cpp -B cpp/build -DGAIA_BUILD_TESTS=ON && cmake --build cpp/build && ctest --test-dir cpp/build --output-on-failure491/491 pass (463 before + 28 new)
  • All 28 existing test_lemonade_client cases pass unmodified
  • New cpp/tests/test_http_client.cpp: GET/POST/streaming, header passthrough and case-insensitive merge, response headers, read timeout, connection failure, non-2xx status, malformed port, CRLF injection, path joining, moved-from client
  • New LemonadeOverHttpClientTest cases drive the refactored transport end-to-end against the mock server — including a real SSE stream, which pins the [DONE] → normal-completion path that the offline unit tests never covered
  • Header isolation: cpp/tests/test_http_client.cpp #errors if gaia/http_client.h leaks httplib; separately verified a consumer TU compiles with no httplib include path at all
  • HTTP-only build (-DCMAKE_DISABLE_FIND_PACKAGE_OpenSSL=ON) compiles the no-TLS branches, warning-clean under -Wall -Wextra -Wpedantic

C++ tool authors had no way to call an HTTP service: the only client in the
framework was LemonadeClient's private httpGet/httpPost/httpPostStreaming,
and cpp-httplib is a PRIVATE CMake dependency consumers cannot see.

gaia::HttpClient is a pimpl over cpp-httplib, so the transport stays private —
including gaia/http_client.h pulls in no 10k-line header. It exposes
get/post/postStreaming with header maps, per-request timeouts and TLS
(reusing the existing OpenSSL auto-detection). Every failure raises HttpError
naming the URL and the failure mode, carrying status() and body(); there is no
empty-response fallback.

LemonadeClient's private HTTP methods are now thin forwards. Timeouts,
URL normalization (/v1 and /api/v1 preservation) and the SSE [DONE] →
normal-completion path are preserved; the streaming path no longer feeds a
non-2xx error body to SseParser, and an https:// URL on an HTTP-only build
now raises an actionable error instead of "SSL not supported."

Header names merge case-insensitively, CRLF in header fields or request
paths is rejected, ports are validated, and moved-from clients stay usable.

Unblocks the embeddings API (#2791) and MCP HTTP transport (#2802).
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Verdict: Approve

This PR extracts a general-purpose gaia::HttpClient C++ class out of LemonadeClient and rewires the Lemonade transport to delegate to it, with the httplib dependency kept fully private behind a pimpl. The refactor preserves the existing Lemonade behavior (timeouts, streaming [DONE] handling, error surfacing) and adds a broad new test suite. It reads as clean, correct, and unusually well-documented — nothing blocking.

The one thing worth knowing: this is a pure transport refactor, so the risk is behavioral drift in the Lemonade path. The tests cover that well — a dedicated LemonadeOverHttpClient group checks health/models/chat/streaming parity plus the [DONE]-sentinel completion path — so I'm comfortable it holds.

Note: the review environment blocked shell access, so I could not pull the PR description; this verdict rests on static review of the diff and the included tests.

Real-world evidence

N/A — C++ library change under cpp/, exercised by gtest unit tests. It touches no runnable GAIA surface that this lane covers (no Agent UI, gaia CLI, REST API, or MCP), and no evidence-bundle.md was produced. The added test_http_client.cpp (GET/POST/streaming, headers, timeouts, injection, moves) and the Lemonade parity tests are the appropriate proof for this surface.

🔍 Technical details

Strengths

  • Private-dependency invariant is enforced, not just documented. test_http_client.cpp:1262 does #ifdef CPPHTTPLIB_HTTPLIB_H #error, so the header leaking httplib into a consumer TU becomes a compile failure — the pimpl promise is actually testable.
  • Fail-loudly done right. Every failure mode (connect refused, timeout, TLS unavailable, non-2xx) throws HttpError naming the method + full URL + failure mode, with status()/body() for programmatic handling and no silent empty-response fallback (http_client.cpp:600, :836). Matches CLAUDE.md's no-silent-fallbacks rule.
  • Security-positive input handling. validateHeaderField (http_client.cpp:349) rejects CR/LF/NUL/colon/whitespace in header names and CR/LF/NUL in values, and resolve() rejects spaces/control chars in the request path (:542) — both close request-smuggling / header-injection vectors, and both are tested (test_http_client.cpp:1556). parsePort rejects 8080abc/0/99999 where the old std::stoi silently accepted 8080abc.
  • Behavior parity preserved. The Lemonade connect-timeout semantics carry over exactly — GET tracks read timeout on connect (lemonade_client.cpp:976), POST/streaming keep the 30s connect default via the config — and the streamDone out-param is cleanly absorbed into HttpClient's internal canceled-by-callback detection.
  • UTF-8-safe error truncation (utf8SafeCut, :337) avoids handing invalid UTF-8 to nlohmann::json::dump() downstream — a real, non-obvious failure mode, correctly guarded and commented.

🟢 Minor (optional, non-blocking)

  • Move-ctor comment references a move that doesn't exist (http_client.cpp:659-660): "LemonadeClient's defaulted move would otherwise leave a live null." Because LemonadeClient user-declares (deletes) its copy constructor, no move constructor is implicitly declared for it — LemonadeClient is non-movable. The re-arming of other.impl_ is still correct and genuinely needed for direct HttpClient moves (exercised by MovedFromClientStaysUsable, test_http_client.cpp:1591); only the rationale naming LemonadeClient is slightly off. Consider retargeting the comment at the direct-move contract to avoid misleading a future reader.

  • noexcept move allocates (http_client.cpp:661, :666): the re-arm does new Impl(...) inside noexcept move ctor/assignment, so an allocation failure would std::terminate rather than propagate. Acceptable and a common pattern, but if you'd rather not couple move to the allocator, a null-tolerant impl_ with a lazy re-init (or leaving moved-from as a documented null) sidesteps it. Purely defensive; fine to leave as-is.

@kovtcharov

Copy link
Copy Markdown
Contributor Author

The failing C++ Integration Tests (STX) check is not this PR

Diagnosed during the milestone #63 sweep — all three open Wave 1 PRs (#2807, #2809, #2816) fail this same check, and none for a reason related to their diffs. The build never reaches compilation:

CMake Error: Could not find CMAKE_ROOT !!!
Modules directory not found in
C:/Windows/Temp/cmake/cmake-3.31.4-windows-x86_64/share/cmake-3.31

.github/workflows/build_cpp.yml caches CMake under $env:TEMP on the self-hosted runner and gates re-download on Test-Path "$cmakeCached\cmake.exe" — it validates that bin/cmake.exe exists but never that share/cmake-3.31/Modules/ does, which is what CMAKE_ROOT resolves to. Temp cleanup removed share/ and left bin/, so the guard sees a healthy cache, skips the re-download, and prepends a broken CMake to PATH.

Tracked as #2817, fix in flight. Nothing to do on this PR for it.

kovtcharov-amd pushed a commit to Jonesxq/gaia that referenced this pull request Aug 6, 2026
…xe (amd#2818)

Every `cpp/**` PR has been failing the `C++ Integration Tests (STX)`
check before it compiles anything — amd#2807, amd#2809 and amd#2816 are all red
for a reason unrelated to their diffs. The self-hosted runner cached
CMake under `$env:TEMP` and re-downloaded it only when `bin\cmake.exe`
was missing; Windows Temp cleanup deleted `share\cmake-3.31\Modules` and
left `bin\`, so the job kept trusting a CMake that cannot resolve
`CMAKE_ROOT` and every run died the same way until someone cleared Temp
by hand. Now each candidate toolchain is probed for the thing the build
actually depends on, a failed probe falls through to a clean
re-download, and tools live in the runner tool cache instead of a
directory the OS sweeps.

One measurement drove the design and is worth flagging for review:
**exit codes cannot detect this failure.** A CMake missing its Modules
tree prints `Could not find CMAKE_ROOT` to stderr and still exits 0 —
for `--version` and for `--help-module-list` (measured on 4.4.2). So the
issue's suggested `cmake --version` exit-0 check would not have caught
it on its own; validity requires the Modules tree on disk *and* a probe
that does not report a broken root.

The stale `%TEMP%\cmake` tree on the runner is now inert — nothing reads
it — so no manual cleanup is needed to make this work; deleting it just
reclaims disk.

Closes amd#2817

## Test plan

- [ ] `pwsh -File .github/scripts/tests/CppBuildTools.Tests.ps1` passes
(21/21). It asserts the exact regression: `bin/cmake.exe` present +
`share/` absent reports **invalid**, both present reports **valid**, and
includes negative controls showing the old `Test-Path cmake.exe` check
and an exit-code-only check would both have accepted the broken install.
- [ ] New `C++ toolchain script tests` job is green (parse-checks every
`.github/scripts/*.ps1`, then runs the unit tests).
- [ ] `C++ Integration Tests (STX)` on this PR gets past `Ensure C++
build tools are available` and reaches compilation. The step log should
name which CMake it accepted and, if it rejected one, why.
- [ ] Reproduce the root cause on any machine: copy a `cmake` binary
alone into an empty directory and run `--version` — it prints the
`CMAKE_ROOT` error and exits 0.
- [ ] After merge, re-run CI on amd#2807, amd#2809 and amd#2816 with no changes
to their diffs and confirm the STX check goes green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cpp documentation Documentation changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cpp): gaia::HttpClient — a general HTTP client abstraction

1 participant