From 14897364da214680796603c75d7ad1b8d5303423 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Tue, 15 Sep 2026 09:30:40 +0700 Subject: [PATCH 1/2] docs: add v0.9 user guide and examples --- README.md | 191 +++++++++++++++++++++++++++ docs/getting-started.md | 229 +++++++++++++++++++++++++++++++++ examples/CMakeLists.txt | 3 + examples/configured_client.cpp | 44 +++++++ examples/get.cpp | 23 ++++ examples/post.cpp | 35 +++++ 6 files changed, 525 insertions(+) create mode 100644 README.md create mode 100644 docs/getting-started.md create mode 100644 examples/configured_client.cpp create mode 100644 examples/get.cpp create mode 100644 examples/post.cpp diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8d6dff --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +# cpp_request + +`cpp_request` is a lightweight synchronous HTTP/1.1 client for C++17 built directly on native OS sockets. + +The v1 scope is intentionally small: plaintext HTTP over TCP, predictable synchronous execution, structured errors, and zero third-party runtime dependencies. + +> HTTPS/TLS is not supported in v1. + +## Highlights + +- C++17 +- HTTP/1.1 over plaintext TCP +- Windows, Linux, and macOS +- native Winsock2 / POSIX sockets +- GET, HEAD, POST, PUT, PATCH, DELETE +- custom headers and query parameters +- IPv4 and IPv6 +- configurable connect/read/write timeouts +- bounded automatic redirects +- HTTP/1.1 keep-alive connection reuse +- `Content-Length`, close-delimited, and chunked response bodies +- configurable in-memory response limits +- structured `Result` / `Error` model +- installable CMake package: `cpp_request::cpp_request` +- zero third-party runtime dependencies + +## Quick start + +```cpp +#include +#include + +#include + +int main() { + auto result = cpp_request::get("http://127.0.0.1:8080/"); + if (!result) { + std::cerr << cpp_request::error_message(result.error().code) << '\n'; + return 1; + } + + const auto& response = result.value(); + std::cout << response.status_code() << '\n'; + std::cout << response.body() << '\n'; +} +``` + +The one-shot helpers create a temporary `Client`. For repeated requests to the same origin, keep a `Client` alive so eligible HTTP/1.1 connections can be reused. + +## Build + +Requirements: + +- CMake 3.21+ +- a C++17 compiler + +```sh +cmake -S . -B build -DCPP_REQUEST_BUILD_TESTS=OFF +cmake --build build --config Release +``` + +To build the examples too: + +```sh +cmake -S . -B build -DCPP_REQUEST_BUILD_EXAMPLES=ON +cmake --build build --config Release +``` + +## Install and consume with CMake + +Install into a prefix: + +```sh +cmake -S . -B build \ + -DCPP_REQUEST_BUILD_TESTS=OFF \ + -DCPP_REQUEST_BUILD_EXAMPLES=OFF +cmake --build build --config Release +cmake --install build --config Release --prefix /path/to/prefix +``` + +Consumer project: + +```cmake +find_package(cpp_request CONFIG REQUIRED) + +target_link_libraries(my_app PRIVATE cpp_request::cpp_request) +``` + +Point CMake at a non-system prefix with `CMAKE_PREFIX_PATH` when necessary. + +## Custom request + +```cpp +#include +#include + +#include + +cpp_request::Client client; + +std::string body = R"({"name":"example"})"; +cpp_request::Request request{ + cpp_request::Method::Post, + "http://127.0.0.1:8080/items"}; + +request.headers().set("Content-Type", "application/json"); +request.headers().set("Accept", "application/json"); +request.add_query_param("source", "cpp request"); +request.set_body(body); + +auto result = client.request(request); +``` + +`Request` borrows its URL and body through `std::string_view`; the referenced storage must remain alive until `Client::request()` returns. Headers and appended query parameters own their storage. + +## Client configuration + +```cpp +#include +#include + +#include + +using namespace std::chrono_literals; + +cpp_request::Client client; +client.set_connect_timeout(2s); +client.set_read_timeout(5s); +client.set_write_timeout(5s); +client.set_follow_redirects(true); +client.set_max_redirects(5); + +cpp_request::ResponseLimits limits; +limits.max_head_bytes = 32 * 1024; +limits.max_body_bytes = 8 * 1024 * 1024; +client.set_response_limits(limits); +``` + +## Thread safety + +A `Client` owns mutable connection-reuse state and is **not guaranteed thread-safe**. Do not concurrently call one `Client` from multiple threads without external synchronization. Separate `Client` instances may be used independently. + +## v1 scope + +Supported in v1: + +- synchronous HTTP/1.1 +- in-memory request and response bodies +- one reusable same-origin connection per `Client` +- finite redirect following +- explicit resource limits + +Intentionally outside v1: + +- HTTPS / TLS +- asynchronous APIs and coroutines +- HTTP/2 and HTTP/3 +- WebSocket +- proxy support +- cookie jar +- multipart builder +- gzip/brotli decompression +- request/response streaming +- generalized connection pool +- automatic retries +- response cache + +## Documentation + +- [Getting started](docs/getting-started.md) +- [Public API](docs/api/public-api.md) +- [Error model](docs/api/error-model.md) +- [Result model](docs/api/result.md) +- [Lifetime rules](docs/api/lifetime.md) +- [Response limits](docs/api/response-limits.md) +- [Roadmap](docs/roadmap.md) +- [CMake structure](cmake/README.md) + +## Development + +Tests are enabled by default when `cpp_request` is the top-level project. Benchmarks are opt-in: + +```sh +cmake -S . -B build-bench \ + -DBUILD_TESTING=OFF \ + -DCPP_REQUEST_BUILD_BENCHMARKS=ON \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build-bench --config Release --target cpp_request_benchmark_smoke +``` + +See `cmake/README.md` for the complete build-option list. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..887459e --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,229 @@ +# Getting started + +This guide covers the public v0.9 API that is intended to become the v1 baseline. + +## Requirements + +- C++17 +- CMake 3.21+ when building from source +- Windows, Linux, or macOS + +`cpp_request` has no third-party runtime dependency. GoogleTest and Google Benchmark are development-only dependencies used by the project test and benchmark builds. + +## Protocol scope + +`cpp_request` v1 supports only `http://` URLs over plaintext TCP. + +`https://` is rejected as an unsupported scheme before a TLS connection is attempted. TLS/HTTPS is intentionally post-v1 work. + +## One-shot requests + +For a single request, use the free helpers: + +```cpp +#include +#include + +#include + +int main() { + auto result = cpp_request::get("http://127.0.0.1:8080/"); + if (!result) { + const auto error = result.error(); + std::cerr << cpp_request::error_message(error.code); + if (error.native_code != 0) { + std::cerr << " (native=" << error.native_code << ')'; + } + std::cerr << '\n'; + return 1; + } + + const auto& response = result.value(); + std::cout << "status: " << response.status_code() << '\n'; + std::cout << response.body() << '\n'; +} +``` + +Available one-shot helpers are `get`, `head`, `post`, `put`, `patch`, and `del`. + +## Reusable Client + +Use a persistent `Client` for repeated requests. When HTTP framing and peer behavior allow reuse, sequential requests to the same effective host and port can use the retained HTTP/1.1 connection. + +```cpp +cpp_request::Client client; + +auto first = client.get("http://127.0.0.1:8080/one"); +auto second = client.get("http://127.0.0.1:8080/two"); +``` + +There is no hidden retry of a request after a stale keep-alive failure. A failed stale connection is discarded; the caller decides whether to issue another request. + +## POST body + +The convenience body helpers accept a borrowed `std::string_view`: + +```cpp +std::string body = R"({"name":"alpha"})"; +auto result = client.post("http://127.0.0.1:8080/items", body); +``` + +For custom headers, construct `Request` explicitly. + +## Custom headers and query parameters + +```cpp +#include +#include + +#include + +std::string body = R"({"name":"alpha"})"; + +cpp_request::Request request{ + cpp_request::Method::Post, + "http://127.0.0.1:8080/items?existing=1"}; + +request.headers().set("Content-Type", "application/json"); +request.headers().add("X-Trace", "first"); +request.headers().add("X-Trace", "second"); +request.add_query_param("search", "low level C++"); +request.add_query_param("tag", "network/http"); +request.set_body(body); + +auto result = client.request(request); +``` + +Header names are looked up case-insensitively. Ordered duplicate fields are preserved. `Headers::get()` returns the first matching field. + +Appended query parameters preserve insertion order and are percent-encoded by the request serializer. An existing raw query in the URL is preserved before appended parameters. + +## Borrowed lifetime rules + +`Request` intentionally uses non-owning views for its URL and body. This avoids an unconditional payload copy. + +The URL and body storage must remain valid until `Client::request()` returns: + +```cpp +std::string url = "http://127.0.0.1:8080/items"; +std::string body = "payload"; + +cpp_request::Request request{cpp_request::Method::Post, url}; +request.set_body(body); + +auto result = client.request(request); // url/body still alive here +``` + +Headers and appended query parameters own their strings. + +A successful `Response` owns its reason phrase, headers, and body. Views returned by `Response` stay valid while that `Response` object and its corresponding owned storage remain alive and unchanged. + +## Timeouts + +Connect, read, and write timeouts are configured separately: + +```cpp +using namespace std::chrono_literals; + +cpp_request::Client client; +client.set_connect_timeout(2s); +client.set_write_timeout(5s); +client.set_read_timeout(5s); +``` + +The connect timeout is the connection-attempt budget, the write timeout bounds request transmission, and the read timeout applies while waiting for response progress. + +## Redirects + +Redirect following is enabled by default with a maximum of 10 redirects. + +```cpp +client.set_follow_redirects(true); +client.set_max_redirects(5); +``` + +Supported redirect status codes are 301, 302, 303, 307, and 308. Redirects to unsupported schemes such as HTTPS fail explicitly instead of silently changing transports. + +## Response limits + +Responses are memory-resident in v1, so finite limits are part of the public client configuration: + +```cpp +cpp_request::ResponseLimits limits; +limits.max_head_bytes = 64 * 1024; +limits.max_body_bytes = 16 * 1024 * 1024; +limits.max_chunk_line_bytes = 8 * 1024; +limits.max_trailer_bytes = 64 * 1024; +client.set_response_limits(limits); +``` + +The defaults are 64 KiB response head, 64 MiB decoded body, 8 KiB chunk-size line, and 64 KiB aggregate trailers. Zero is a real zero-byte limit, not an unlimited sentinel. + +A limit violation returns `ErrorCode::ResponseLimitExceeded`. + +## Error handling + +Expected URL, transport, timeout, HTTP framing, redirect, and response-limit failures are returned as `Result`. + +```cpp +auto result = client.get("http://127.0.0.1:8080/"); +if (!result) { + const cpp_request::Error& error = result.error(); + std::cerr << cpp_request::error_message(error.code) << '\n'; + return; +} + +const cpp_request::Response& response = result.value(); +``` + +`Result::value()` and `Result::error()` have state preconditions. Check the result first; accessing the wrong state is programmer misuse. + +## Thread safety + +`Client` is move-only and owns mutable reusable-connection state. One `Client` is not guaranteed safe for concurrent requests from multiple threads. + +Use one `Client` per independent execution context, or provide external synchronization around a shared instance. + +## Install-tree consumption + +Build and install: + +```sh +cmake -S . -B build \ + -DCPP_REQUEST_BUILD_TESTS=OFF \ + -DCPP_REQUEST_BUILD_EXAMPLES=OFF +cmake --build build --config Release +cmake --install build --config Release --prefix /path/to/prefix +``` + +Consume: + +```cmake +cmake_minimum_required(VERSION 3.21) +project(example LANGUAGES CXX) + +find_package(cpp_request CONFIG REQUIRED) + +add_executable(example main.cpp) +target_link_libraries(example PRIVATE cpp_request::cpp_request) +target_compile_features(example PRIVATE cxx_std_17) +``` + +If the install prefix is not in a default search location, configure the consumer with: + +```sh +cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/prefix +``` + +## Examples + +The repository examples target a local HTTP server at `127.0.0.1:8080` and do not depend on public internet services. + +Build them with: + +```sh +cmake -S . -B build -DCPP_REQUEST_BUILD_EXAMPLES=ON +cmake --build build --config Release +``` + +Examples are demonstrations, not network-dependent automated tests. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e69de29..4a6fe2e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -0,0 +1,3 @@ +cpp_request_add_example(cpp_request_example_get get.cpp) +cpp_request_add_example(cpp_request_example_post post.cpp) +cpp_request_add_example(cpp_request_example_configured_client configured_client.cpp) diff --git a/examples/configured_client.cpp b/examples/configured_client.cpp new file mode 100644 index 0000000..12323f7 --- /dev/null +++ b/examples/configured_client.cpp @@ -0,0 +1,44 @@ +#include +#include +#include + +#include +#include + +int main() { + using namespace std::chrono_literals; + + cpp_request::Client client; + client.set_connect_timeout(2s); + client.set_write_timeout(5s); + client.set_read_timeout(5s); + client.set_follow_redirects(true); + client.set_max_redirects(5); + + cpp_request::ResponseLimits limits; + limits.max_head_bytes = 32 * 1024; + limits.max_body_bytes = 8 * 1024 * 1024; + limits.max_chunk_line_bytes = 8 * 1024; + limits.max_trailer_bytes = 32 * 1024; + client.set_response_limits(limits); + + const char* urls[] = { + "http://127.0.0.1:8080/one", + "http://127.0.0.1:8080/two", + }; + + for (const char* url : urls) { + auto result = client.get(url); + if (!result) { + std::cerr << "request failed: " + << cpp_request::error_message(result.error().code) + << '\n'; + return 1; + } + + std::cout << result.value().status_code() << ' ' + << result.value().body() << '\n'; + } + + return 0; +} diff --git a/examples/get.cpp b/examples/get.cpp new file mode 100644 index 0000000..fb8d700 --- /dev/null +++ b/examples/get.cpp @@ -0,0 +1,23 @@ +#include +#include + +#include + +int main() { + auto result = cpp_request::get("http://127.0.0.1:8080/"); + if (!result) { + const auto error = result.error(); + std::cerr << "request failed: " + << cpp_request::error_message(error.code); + if (error.native_code != 0) { + std::cerr << " (native=" << error.native_code << ')'; + } + std::cerr << '\n'; + return 1; + } + + const auto& response = result.value(); + std::cout << "HTTP status: " << response.status_code() << '\n'; + std::cout << response.body() << '\n'; + return 0; +} diff --git a/examples/post.cpp b/examples/post.cpp new file mode 100644 index 0000000..72dc3ec --- /dev/null +++ b/examples/post.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +#include +#include + +int main() { + cpp_request::Client client; + + std::string body = R"({"name":"cpp_request","kind":"example"})"; + cpp_request::Request request{ + cpp_request::Method::Post, + "http://127.0.0.1:8080/items?existing=1"}; + + request.headers().set("Content-Type", "application/json"); + request.headers().set("Accept", "application/json"); + request.headers().add("X-Example", "cpp_request"); + request.add_query_param("search", "low level C++"); + request.add_query_param("tag", "network/http"); + request.set_body(body); + + auto result = client.request(request); + if (!result) { + std::cerr << "request failed: " + << cpp_request::error_message(result.error().code) + << '\n'; + return 1; + } + + const auto& response = result.value(); + std::cout << "HTTP status: " << response.status_code() << '\n'; + std::cout << response.body() << '\n'; + return 0; +} From 1f30adcbbd2692e2140cacb7ff1852dcda721751 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Tue, 15 Sep 2026 09:32:48 +0700 Subject: [PATCH 2/2] docs: synchronize roadmap through v0.9 --- docs/roadmap.md | 504 +++++++++++++++++++++--------------------------- 1 file changed, 221 insertions(+), 283 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 53211ad..e0ecf82 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,25 +4,23 @@ - Project: `cpp_request` - Target release: MVP v1.0 -- Roadmap status: Active implementation baseline +- Current development version: v0.9.0 - Language baseline: C++17 - Protocol scope: synchronous HTTP/1.1 over plaintext TCP - Last roadmap review: 2026-09-15 -This document is the implementation-order source of truth for the v1 release. +This document is the implementation-order source of truth for the v1 release. The frozen functional and non-functional requirements remain authoritative for **what** v1 must provide; this roadmap records milestone completion and the remaining release gate. -The frozen functional and non-functional requirements remain authoritative for **what** v1 must provide. This roadmap defines **when** those requirements are implemented and which milestone must be complete before v1.0 can ship. - -Version labels below are development milestones. A milestone does not require a public release tag unless the project explicitly decides to publish one. +Version labels are development milestones and do not require a public release tag unless explicitly decided. --- ## Status Legend -- โœ… **Complete** โ€” merged into `main` and covered by the expected tests. -- ๐Ÿšง **In progress** โ€” implementation exists in an open PR or active branch. -- โณ **Planned** โ€” required for v1 but implementation has not started. -- ๐Ÿงช **Release gate** โ€” verification/hardening required before v1.0. +- โœ… **Complete** โ€” merged implementation exists and the milestone exit criteria are satisfied. +- ๐Ÿšง **In progress** โ€” active implementation or finalization work exists. +- โณ **Planned** โ€” required work has not started. +- ๐Ÿงช **Release gate** โ€” integrated verification required before v1.0. - โžก๏ธ **Post-v1** โ€” intentionally outside the frozen v1 scope. --- @@ -38,9 +36,9 @@ Version labels below are development milestones. A milestone does not require a | v0.5 | Public `Client` end-to-end execution path | โœ… Complete | | v0.6 | HTTP/1.1 connection reuse / keep-alive | โœ… Complete | | v0.7 | Redirect handling | โœ… Complete | -| v0.8 | Protocol/API correctness hardening | ๐Ÿšง In progress | -| v0.9 | Packaging, benchmarks, examples, documentation | โณ Planned | -| v1.0 | Release hardening and acceptance gate | ๐Ÿงช Planned | +| v0.8 | Protocol/API correctness hardening | โœ… Complete | +| v0.9 | Packaging, benchmarks, examples, documentation | โœ… Complete on merge of PR #29 | +| v1.0 | Release hardening and acceptance gate | ๐Ÿงช Next | --- @@ -48,19 +46,15 @@ Version labels below are development milestones. A milestone does not require a Goal: freeze the v1 product boundary before implementation expands. -Completed work: +Completed: -- v1 functional requirements frozen. -- v1 non-functional requirements frozen. -- architecture layers defined: Public API, HTTP, Transport, Platform, OS. -- public API baseline defined. -- lifetime and ownership contracts defined. -- structured `Error` taxonomy defined. -- project-owned `Result` contract defined. -- C++17 minimum frozen. -- no PImpl for v1 frozen. -- HTTPS/TLS explicitly excluded from v1. -- synchronous-only v1 execution model frozen. +- frozen functional and non-functional requirements, +- layered Public API / HTTP / Transport / Platform architecture, +- public API, lifetime, error, and `Result` contracts, +- C++17 minimum, +- synchronous-only v1, +- no PImpl requirement, +- HTTPS/TLS explicitly outside v1. Historical references: @@ -75,22 +69,20 @@ Exit criteria: **complete**. # v0.2 โ€” Native Transport Foundation โœ… -Goal: establish a portable, bounded, RAII-owned TCP byte stream without exposing platform sockets publicly. +Goal: provide a portable bounded TCP byte stream without leaking native sockets into the public API. -Completed work: +Completed: -- URL parsing and HTTP-only scheme validation. -- IPv4 and IPv6 endpoint representation. -- Winsock/POSIX native socket RAII wrapper. -- network runtime initialization. -- OS-backed DNS resolution. -- timed TCP connection establishment. -- endpoint fallback. -- timed `write_all()` and `read_some()`. -- partial I/O handling. -- connection-close/error normalization. -- SIGPIPE-safe POSIX write behavior. -- Windows/Linux/macOS CI coverage. +- URL parsing and HTTP-only scheme validation, +- IPv4 and IPv6 endpoints, +- Winsock/POSIX native socket RAII, +- network runtime initialization, +- OS DNS resolution, +- timed connect/read/write, +- endpoint fallback, +- partial-I/O handling, +- SIGPIPE-safe POSIX behavior, +- normalized transport errors. Historical references: @@ -106,22 +98,19 @@ Exit criteria: **complete**. # v0.3 โ€” HTTP Request Model and Serialization โœ… -Goal: represent and serialize all v1 request methods without unnecessary payload copies. +Goal: represent and serialize all v1 request methods while avoiding unnecessary payload copies. -Completed work: +Completed: -- owning ordered `Headers` storage. -- case-insensitive header lookup. -- duplicate request-header support. -- `Request` with borrowed URL/body views. -- GET, HEAD, POST, PUT, PATCH, DELETE method model. -- HTTP/1.1 request-line serialization. -- automatic `Host` generation. -- IPv6 host formatting. -- automatic and caller-validated `Content-Length`. -- outgoing header syntax validation. -- CR/LF request-injection rejection. -- request-body zero-copy serialization boundary. +- ordered owning `Headers`, +- case-insensitive lookup and duplicate fields, +- borrowed request URL/body, +- GET / HEAD / POST / PUT / PATCH / DELETE, +- request-line and header serialization, +- automatic `Host`, +- IPv6 Host formatting, +- Content-Length generation/validation, +- request-header syntax and injection validation. Historical references: @@ -134,23 +123,21 @@ Exit criteria: **complete**. # v0.4 โ€” HTTP Response Parser and Framing โœ… -Goal: incrementally parse HTTP/1.1 responses independently of TCP packet boundaries. +Goal: parse HTTP/1.1 incrementally and independently of TCP packet boundaries. -Completed work: +Completed: -- public owning `Response`. -- incremental status-line/header parsing. -- interim `1xx` handling. -- HEAD/no-body semantics. -- `Content-Length` body framing. -- close-delimited body framing. -- chunked transfer decoding. -- chunk extensions and trailer consumption/validation. -- premature EOF detection. -- conflicting framing rejection. -- `Connection: close` detection. -- protocol-upgrade non-reuse semantics. -- preservation of bytes belonging to a following response. +- owning `Response`, +- status-line and header parsing, +- interim 1xx handling, +- HEAD/no-body semantics, +- Content-Length framing, +- close-delimited framing, +- chunked transfer decoding, +- chunk extension/trailer validation, +- EOF and framing-conflict detection, +- connection-reuse eligibility signals, +- preservation of pending bytes belonging to the following response. Historical references: @@ -163,30 +150,20 @@ Exit criteria: **complete**. # v0.5 โ€” Public Client Core โœ… -Goal: connect all merged layers into one usable synchronous HTTP request path. - -Completed work: - -1. serialize `Request`, -2. resolve host, -3. connect TCP, -4. write request head and borrowed body, -5. read response bytes, -6. feed incremental parser, -7. return owning `Response`. +Goal: connect serialization, DNS, TCP, parser, and public result handling into one synchronous request path. -Public API completed: +Completed: -- `Client::request(const Request&)` -- `Client::get()` / `head()` / `post()` / `put()` / `patch()` / `del()` -- equivalent one-shot free helpers -- connect/read/write timeout setters - -Verification includes loopback GET, POST body transmission, custom headers, Content-Length, chunked, close-delimited, malformed response propagation, and HTTPS rejection before networking. +- `Client::request(const Request&)`, +- member GET / HEAD / POST / PUT / PATCH / DELETE helpers, +- equivalent one-shot free helpers, +- connect/read/write timeout configuration, +- loopback integration coverage, +- unsupported HTTPS rejection before networking. Historical reference: -- PR #15 โ€” public `Client` core execution path +- PR #15 โ€” public `Client` execution path Exit criteria: **complete**. @@ -194,22 +171,17 @@ Exit criteria: **complete**. # v0.6 โ€” Connection Reuse / Keep-Alive โœ… -Goal: make `Client` genuinely stateful and satisfy HTTP/1.1 connection-reuse requirements. - -Completed work: +Goal: make `Client` genuinely stateful for sequential same-origin HTTP/1.1 traffic. -- retain one eligible TCP connection inside `Client`. -- same-origin identity by effective host + port. -- reuse only after a fully consumed reusable response. -- close retained connection on `Connection: close`. -- never reuse close-delimited or protocol-upgraded connections. -- discard retained connection after transport/protocol failure. -- reconnect when the request origin changes. -- stale peer-closed keep-alive state is discarded without hidden automatic retry. -- preserve parser pending-byte invariant. -- move-only `Client` resource ownership. +Completed: -Verification includes one-socket sequential requests, forced reconnect scenarios, origin changes, and stale keep-alive behavior. +- one retained eligible connection, +- effective host+port origin identity, +- reuse only after complete reusable responses, +- forced close for `Connection: close`, close-delimited, upgrade, and failed states, +- origin-change reconnect, +- no hidden automatic retry after stale keep-alive failure, +- move-only client ownership. Historical reference: @@ -221,24 +193,18 @@ Exit criteria: **complete**. # v0.7 โ€” Redirect Handling โœ… -Goal: implement bounded automatic redirects without expanding beyond plaintext HTTP. - -Completed work: +Goal: implement bounded redirects without expanding beyond plaintext HTTP. -- configurable redirect following. -- finite redirect limit. -- 301 / 302 / 303 / 307 / 308 handling. -- POST-to-GET policy for 301/302. -- non-HEAD-to-GET policy for 303. -- method/body preservation for 307/308. -- absolute, scheme-relative, absolute-path, relative-path, query-only, and fragment-aware `Location` resolution. -- explicit HTTPS/unsupported-scheme rejection. -- same-origin redirect reuse. -- cross-origin connection replacement. -- cross-origin stripping of `Host`, `Authorization`, `Proxy-Authorization`, and `Cookie`. -- `MissingRedirectLocation`, `RedirectLimitExceeded`, and `UnsupportedRedirectScheme` propagation. +Completed: -Verification includes loopback redirect chains, disabled following, finite limits, method/body policy, credential stripping, and cross-origin hops. +- configurable finite redirect following, +- 301 / 302 / 303 / 307 / 308 handling, +- method/body rewrite policy, +- absolute and relative Location resolution, +- same-origin reuse and cross-origin reconnect, +- sensitive header stripping across origins, +- unsupported redirect-scheme rejection, +- redirect-specific structured errors. Historical reference: @@ -248,221 +214,195 @@ Exit criteria: **complete**. --- -# v0.8 โ€” Protocol and API Correctness Hardening ๐Ÿšง - -Goal: close known correctness gaps before treating the API as release-candidate quality. - -## 1. `Result` hardening โ€” โœ… complete - -Completed work: - -- replaced type-based state lookup with explicit variant-index/discriminant access. -- removed hidden wrong-state `std::bad_variant_access` paths from `noexcept` accessors. -- retained accessor misuse as a documented programmer precondition violation. -- added explicit success/failure factories. -- made `Result` capable of representing success and failure unambiguously. -- preserved move-only payload support. -- retained `std::variant` as the baseline representation until measurement justifies custom storage. -- kept `Result` deferred until a concrete operation requires it. - -Historical reference: +# v0.8 โ€” Protocol and API Correctness Hardening โœ… -- PR #22 โ€” `Result` state hardening +Goal: close correctness and bounded-resource gaps before presenting the API as release-candidate quality. -Exit criteria: **complete**. +## Result hardening -## 2. URL and query correctness โ€” โœ… complete +Completed: -Completed work: +- explicit variant-index state access, +- no hidden `bad_variant_access` from noexcept accessors, +- explicit success/failure factories, +- unambiguous `Result`, +- move-only payload support. -- added owned `Request::add_query_param(name, value)` parameters. -- percent-encoded appended query parameters. -- preserved repeated query keys and insertion order. -- preserved an existing raw URL query before appended parameters. -- validated raw `%HH` escapes. -- replaced locale-sensitive scheme handling with ASCII-only normalization. -- strengthened bracketed IPv6 literals using numeric IPv6 validation. -- explicitly rejected unsupported v1 authority encodings / zone identifiers. -- kept redirect base URL resolution consistent with the effective serialized request URL. +Reference: PR #22. -Historical reference: +## URL and query correctness -- PR #23 โ€” URL parsing and query parameter hardening +Completed: -Exit criteria: **complete**. +- owned appended query parameters, +- insertion-order/repeated-key preservation, +- percent encoding, +- raw-query preservation, +- percent-escape validation, +- ASCII-only scheme handling, +- stronger IPv6 literal validation. -## 3. HTTP correctness review โ€” โœ… complete +Reference: PR #23. -Completed work: +## HTTP framing correctness -- enforced safe response framing precedence. -- rejected `Transfer-Encoding` + `Content-Length` ambiguity before connection reuse. -- rejected framing fields where `1xx` / `204` semantics forbid them. -- kept `HEAD` / `304` header-terminated while preserving allowed representation metadata. -- corrected `205 Reset Content` framing so unframed responses are close-delimited rather than incorrectly reusable. -- rejected actual content in a 205 response. -- validated chunk extension token / quoted-string grammar instead of accepting arbitrary printable bytes. -- retained trailer validation and rejected framing-critical trailer fields. -- emitted `Content-Length: 0` for empty POST/PUT/PATCH requests while leaving empty GET/HEAD/DELETE unchanged. -- documented timeout boundaries: connect budget across endpoint attempts, one write budget across request transmission, read timeout per wait for response progress. -- preserved context-sensitive EOF semantics: incomplete explicit framing is `UnexpectedEof`; valid close-delimited EOF completes normally; timeout never masquerades as EOF. -- fixed a dangling `std::string_view` in Transfer-Encoding analysis exposed by MSVC Debug CI. +Completed: -Historical reference: +- safe framing precedence, +- Transfer-Encoding + Content-Length conflict rejection, +- 1xx/204/205/304/HEAD framing review, +- strict chunk-extension and trailer validation, +- correct empty-body request Content-Length behavior, +- explicit timeout/EOF semantics, +- CI-exposed lifetime bug fixes. -- PR #24 โ€” HTTP framing correctness hardening +Reference: PR #24. -Exit criteria: **complete**. +## Response resource bounds -## 4. Resource-bound review โ€” ๐Ÿšง current work +Completed: -Because response bodies are memory-resident in v1, the parser now receives explicit finite response limits from `Client`. +- public `ResponseLimits`, +- default 64 KiB head limit, +- default 64 MiB decoded body limit, +- default 8 KiB chunk-line limit, +- default 64 KiB trailer limit, +- early Content-Length rejection, +- bounded close-delimited and chunked accumulation, +- `ResponseLimitExceeded`, +- zero treated as a real limit, +- deterministic boundary tests, +- failed limited responses never leave the connection reusable. -Current hardening scope: +Reference: PR #25. -- public `ResponseLimits` value type stored by-value in `Client`. -- default response-head limit: 64 KiB. -- default decoded-body limit: 64 MiB. -- default chunk-size-line limit: 8 KiB. -- default trailer-section limit: 64 KiB. -- reject oversized `Content-Length` before reserving body capacity. -- bound close-delimited accumulation before append. -- bound chunked decoded body before chunk payload append. -- bound unterminated/pathological chunk-size lines. -- bound aggregate chunked trailer bytes. -- classify limit failures as `ResponseLimitExceeded` rather than malformed HTTP. -- make zero a real limit rather than an implicit unlimited sentinel. -- document the resource-limit contract without adding response streaming. +v0.8 exit criteria: **complete**. -Exit for this substep: - -- head, body, chunk-line, and trailer limits have deterministic tests. -- a body exactly at the configured limit remains valid. -- `Client` preserves configured limits across move operations. -- limit failures cannot leave the active connection eligible for reuse. +--- -Once this substep merges, v0.8 is complete and the next milestone is v0.9 packaging/benchmarks/examples/documentation. +# v0.9 โ€” Packaging, Benchmarks, Examples, and Documentation โœ… -v0.8 exit criteria: +Goal: make the library consumable, measurable, and understandable before the release gate. -- no known correctness blocker remains for the frozen v1 scope. -- accepted hardening decisions are reflected in docs and tests. +## Packaging โœ… ---- +Completed: -# v0.9 โ€” Packaging, Benchmarks, Examples, and Documentation โณ +- public header installation, +- library installation/export, +- `cpp_requestConfig.cmake` and version config, +- stable installed target `cpp_request::cpp_request`, +- install-tree consumer test, +- no GTest/Google Benchmark leakage into consumer package metadata. -Goal: make the library consumable and make performance claims measurable. +Reference: PR #26. -## Packaging +## Benchmarks โœ… -Planned work: +Executable benchmarks now cover: -- install public headers. -- install/export the core library target. -- provide `cpp_requestConfig.cmake` / version config as appropriate. -- expose a stable consumer target such as `cpp_request::cpp_request`. -- add an install-tree consumer test. -- ensure test/benchmark dependencies do not leak to consumers. +- request serialization, +- response/header parsing, +- chunked decoding, +- end-to-end local loopback request overhead, +- connection reuse vs reconnect. -## Benchmarks +The benchmark suite is opt-in and has a dedicated Release smoke matrix across Windows, Linux, and macOS. -Required benchmark targets: +Reference: PR #27. -- request serialization. -- response/header parsing. -- chunked decoding. -- end-to-end loopback request overhead. -- connection reuse vs reconnect comparison. +## Build-system integration โœ… -Performance work should follow measurement; do not add complexity solely on intuition. +Completed: -## Examples and user documentation +- modular target-scoped CMake configuration, +- project version aligned to v0.9.0, +- namespaced build helpers, +- centralized test/benchmark/example/install modules, +- clean install/export ownership, +- source-tree-safe configure behavior, +- development tooling cleanup. -Planned work: +Reference: PR #28. -- minimal GET example. -- POST body example. -- custom header example. -- timeout configuration example. -- redirect configuration example. -- reusable `Client` example. -- error-handling example. -- explicit thread-safety statement. -- explicit HTTPS/TLS exclusion. -- public API reference synchronized with implementation. +## Examples and user documentation โœ… -## Project presentation +Completed by PR #29: -Before v1.0, add a root README containing at minimum: +- root README, +- minimal GET example, +- POST/custom-header/query example, +- configured reusable Client example, +- timeout configuration, +- redirect configuration, +- response-limit configuration, +- structured error handling, +- explicit thread-safety statement, +- explicit HTTP-only / HTTPS-TLS exclusion, +- install/consumer instructions, +- getting-started guide aligned with the implemented public API. -- project purpose. -- supported platforms. -- C++17 requirement. -- build/install instructions. -- quick-start example. -- supported v1 features. -- explicit non-goals. -- link to this roadmap. +Existing API-specific documentation remains authoritative for detailed contracts: -Exit criteria: +- `docs/api/public-api.md`, +- `docs/api/error-model.md`, +- `docs/api/result.md`, +- `docs/api/lifetime.md`, +- `docs/api/response-limits.md`. -- a clean consumer can install and use the library. -- critical components have executable benchmarks. -- user-facing documentation matches the actual API. +v0.9 exit criteria: **complete when PR #29 merges**. --- # v1.0 โ€” Release Hardening and Acceptance Gate ๐Ÿงช -Goal: verify the frozen requirements as one coherent release. +Goal: verify the frozen requirements as one coherent release candidate. Required verification: -- Windows CI green. -- Linux CI green. -- macOS compatibility green where maintained. -- C++17 build validated. -- required unit and loopback integration tests green. -- no required test depends on the public internet. -- sanitizer configuration reviewed and exercised where supported. -- deterministic socket cleanup verified on error paths. -- connect/read/write timeout tests present. -- connection reuse and redirect tests present. -- install-tree consumer test present. -- benchmark targets build and run. -- public headers reviewed for accidental internal/platform leakage. -- documentation reviewed against frozen v1 scope. +- Windows Debug/Release CI green, +- Linux Debug/Release CI green, +- macOS Debug/Release CI green, +- C++17 build validated, +- all unit and loopback integration tests green, +- no required test depends on public internet access, +- sanitizer configuration reviewed and exercised where supported, +- deterministic socket cleanup verified on failure paths, +- connect/read/write timeout coverage present, +- connection reuse and redirect coverage present, +- install-tree consumer test green, +- benchmark targets build and run, +- examples build, +- public headers reviewed for internal/platform leakage, +- documentation checked against the frozen v1 scope, +- release versioning/tagging decision finalized. ## v1.0 Definition of Done -v1.0 is ready only when all of the following are true: +v1.0 is ready only when: -1. every **MUST** functional requirement is implemented or an explicit requirements change is accepted first, +1. every **MUST** functional requirement is implemented or explicitly revised first, 2. every **MUST** non-functional requirement has a concrete verification path, 3. no known correctness bug can corrupt an HTTP message boundary or reuse an invalid connection, -4. expected network/protocol failures remain representable through structured errors, +4. expected URL/network/protocol/resource failures remain representable through structured errors, 5. the installed library has zero third-party runtime dependency, -6. public API/lifetime contracts match implementation, -7. benchmarks exist for the performance claims the project intends to make. +6. public API and lifetime contracts match implementation, +7. benchmarks exist for any performance claims the project intends to make, +8. all release-gate CI and documentation checks pass. --- -# Dependency Order From Current State +# Next Step ```text -Protocol + API correctness hardening +v0.9 examples + docs merge โ†“ -Packaging + benchmarks + examples + docs +v1.0 integrated release hardening โ†“ -Release acceptance / v1.0 +v1.0 release candidate ``` -Why this order: - -- correctness decisions must stabilize before packaging and public documentation are finalized, -- benchmarks are more meaningful after behavior is stable, -- release hardening should verify the final integrated system rather than intermediate layers. +No new feature should enter the v1 release path unless a frozen requirement is explicitly changed. --- @@ -492,30 +432,28 @@ These features must not delay v1.0 unless the frozen requirements are explicitly # Post-v1 Direction โžก๏ธ -Potential future work, not ordered or committed yet: +Potential future work includes: -- TLS/HTTPS transport abstraction. -- asynchronous/coroutine execution API. -- streaming request/response bodies. -- generalized multi-origin connection pool. -- proxy support. -- cookie management. -- compression/decompression. -- retry policies. +- TLS/HTTPS transport abstraction, +- asynchronous/coroutine execution, +- streaming request/response bodies, +- generalized multi-origin connection pooling, +- proxy support, +- cookie management, +- compression/decompression, +- retry policies, - HTTP/2 and later protocol exploration. -A post-v1 item should receive its own requirements/design work before implementation begins. +Every post-v1 item should receive its own requirements/design work before implementation. --- # Roadmap Maintenance Rule -This roadmap must be updated when any of the following occurs: +Update this roadmap whenever: - a v1 MUST requirement changes, -- a milestone is completed, -- implementation order changes materially, +- a milestone completes, +- implementation order materially changes, - a newly discovered correctness blocker becomes release-critical, -- a feature is moved into or out of v1 scope. - -Normal implementation-detail changes do not require a roadmap edit. +- a feature moves into or out of v1 scope.