From b2fe75d8eac6109dcf98d6ecb98df43606b973fd Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 20:28:05 +0700 Subject: [PATCH 1/2] docs: define v1 error model --- docs/api/error-model.md | 200 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 docs/api/error-model.md diff --git a/docs/api/error-model.md b/docs/api/error-model.md new file mode 100644 index 0000000..17a8c24 --- /dev/null +++ b/docs/api/error-model.md @@ -0,0 +1,200 @@ +# Error Model + +## Status + +- Project: `cpp_request` +- Target release: MVP v1.0 +- Status: Proposed v1 contract +- Language baseline: C++17 + +## Goals + +The v1 error model is designed to be: + +1. **Structured** — callers branch on stable library-owned codes, not diagnostic strings. +2. **Non-exception-based** — expected URL, DNS, socket, timeout, and protocol failures are returned through `Result`. +3. **Portable** — Winsock and POSIX errors are normalized before reaching public API consumers. +4. **Lightweight** — error objects do not require heap allocation for normal reporting. +5. **Diagnostic-friendly** — native OS error values may be retained as secondary information. + +## Public Error Shape + +The public contract should conceptually expose: + +```cpp +enum class ErrorCode { + InvalidUrl, + UnsupportedScheme, + InvalidPort, + + ResolveFailed, + SocketCreateFailed, + ConnectFailed, + ConnectTimeout, + WriteFailed, + WriteTimeout, + ReadFailed, + ReadTimeout, + ConnectionClosed, + + MalformedResponse, + InvalidStatusLine, + InvalidHeader, + InvalidContentLength, + InvalidChunkSize, + InvalidChunkFraming, + ConflictingMessageFraming, + UnexpectedEof, + + RedirectLimitExceeded, + MissingRedirectLocation, + UnsupportedRedirectScheme, + + Unknown +}; + +struct Error { + ErrorCode code; + int native_code; +}; +``` + +The exact physical representation may change during implementation, but the semantic contract above is frozen for v1 unless implementation proves a concrete correctness issue. + +## `native_code` + +`native_code` is optional diagnostic metadata encoded as an integer-compatible value. + +Rules: + +- `0` means no native diagnostic is attached. +- POSIX errors may preserve `errno`. +- Windows errors may preserve the result of `WSAGetLastError()` or an equivalent native code. +- callers must not use `native_code` as the primary portable error contract. +- library behavior must branch on `ErrorCode`, not platform-native numbers. + +## Error Categories + +### URL / input errors + +| Code | Meaning | +| --- | --- | +| `InvalidUrl` | URL syntax cannot be accepted by the v1 parser. | +| `UnsupportedScheme` | URL uses a scheme unsupported by v1, including `https`. | +| `InvalidPort` | Explicit port is syntactically invalid or outside the accepted range. | + +### Resolution / socket / transport errors + +| Code | Meaning | +| --- | --- | +| `ResolveFailed` | Hostname or service resolution failed. | +| `SocketCreateFailed` | A native socket could not be created. | +| `ConnectFailed` | TCP connection establishment failed for reasons other than timeout. | +| `ConnectTimeout` | Connection establishment exceeded the configured connect timeout. | +| `WriteFailed` | Request bytes could not be sent completely for reasons other than timeout. | +| `WriteTimeout` | Request transmission exceeded the configured write timeout. | +| `ReadFailed` | Response bytes could not be read for reasons other than timeout or clean peer closure. | +| `ReadTimeout` | Waiting for response bytes exceeded the configured read timeout. | +| `ConnectionClosed` | Peer closure is observed where the operation requires an active connection. | + +### HTTP protocol errors + +| Code | Meaning | +| --- | --- | +| `MalformedResponse` | Response violates HTTP syntax but does not fit a more specific code. | +| `InvalidStatusLine` | HTTP status line is malformed or unsupported. | +| `InvalidHeader` | Header field syntax is invalid for accepted v1 parsing rules. | +| `InvalidContentLength` | `Content-Length` is malformed, invalid, or otherwise unusable. | +| `InvalidChunkSize` | Chunk-size line is invalid. | +| `InvalidChunkFraming` | Chunk delimiters, CRLF, or terminal framing are invalid. | +| `ConflictingMessageFraming` | Response framing metadata is contradictory or unsafe to interpret silently. | +| `UnexpectedEof` | Connection ended before protocol-defined response completion. | + +### Redirect errors + +| Code | Meaning | +| --- | --- | +| `RedirectLimitExceeded` | Configured redirect count was exceeded. | +| `MissingRedirectLocation` | A followed redirect requires `Location`, but no usable target exists. | +| `UnsupportedRedirectScheme` | Redirect target requires an unsupported scheme/transport. | + +### Fallback + +`Unknown` is reserved for failures that cannot yet be classified safely. New implementation paths should prefer a specific stable code whenever practical. + +## HTTP Status Codes Are Not Library Errors + +An HTTP response such as `404`, `500`, or `503` is still a successfully received HTTP response. + +```mermaid +flowchart LR + A[Request] --> B{Transport + HTTP parse succeeded?} + B -- No --> C[Result contains Error] + B -- Yes --> D[Result contains Response] + D --> E[Response.status_code may be 2xx, 4xx, 5xx, etc.] +``` + +The library must not convert HTTP status codes into `ErrorCode` values automatically. + +This separation allows callers to distinguish: + +- failure to communicate or parse the response, versus +- a valid HTTP response whose application-level status indicates failure. + +## Error Propagation Across Layers + +```mermaid +flowchart TD + OS[Winsock / POSIX failure] --> P[Platform layer captures native code] + P --> T[Transport classifies portable failure] + T --> H[HTTP layer may add protocol-specific classification] + H --> R[Result exposes cpp_request::Error] +``` + +Rules: + +- platform-specific numeric values never replace `ErrorCode`. +- lower-layer failures should not be reclassified unless the upper layer has additional semantic information. +- errors must preserve the most specific meaningful portable classification available. + +## Timeout Classification + +Timeouts remain distinct by operation: + +- `ConnectTimeout` +- `ReadTimeout` +- `WriteTimeout` + +They must not collapse into one generic timeout code in v1 because callers may need different recovery/logging behavior for each stage. + +## EOF Semantics + +EOF handling depends on HTTP framing context. + +Examples: + +- EOF while reading a `Content-Length` body before all declared bytes arrive → `UnexpectedEof`. +- EOF before a complete chunked message terminator → `UnexpectedEof` or a more specific chunk framing error. +- EOF after a valid close-delimited response body → normal message completion, not an error. +- EOF when a reusable connection is expected but no request is currently active may simply invalidate reuse state internally. + +## Diagnostic Message Function + +The library may expose a lightweight function such as: + +```cpp +std::string_view error_message(ErrorCode code) noexcept; +``` + +Requirements: + +- returns static/non-owning text, +- performs no heap allocation, +- messages are for diagnostics only, +- callers must not parse message text to determine behavior. + +## Exception Policy + +Expected runtime failures represented by this document must not require exceptions. + +The v1 contract does not require a global `noexcept` guarantee for every public function, because standard-library allocation may still fail. However, network/protocol error reporting itself must use `Result` rather than throwing library-specific exceptions. From 33cb2a84cae4166dbe0809714dc6b4af8a334450 Mon Sep 17 00:00:00 2001 From: Rafi Indra Pramudhito Zuhayr Date: Mon, 14 Sep 2026 20:28:33 +0700 Subject: [PATCH 2/2] docs: define v1 Result contract --- docs/api/result.md | 207 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 docs/api/result.md diff --git a/docs/api/result.md b/docs/api/result.md new file mode 100644 index 0000000..bda3e58 --- /dev/null +++ b/docs/api/result.md @@ -0,0 +1,207 @@ +# `Result` Contract + +## Status + +- Project: `cpp_request` +- Target release: MVP v1.0 +- Status: Proposed v1 contract +- Language baseline: C++17 + +## Purpose + +`Result` is the standard return type for fallible public operations in `cpp_request`. + +It represents exactly one of two states: + +- success containing `T`, or +- failure containing `Error`. + +```mermaid +stateDiagram-v2 + [*] --> Success + [*] --> Failure + Success --> [*] + Failure --> [*] +``` + +A `Result` must never represent both states simultaneously and must not require exception handling for expected network or protocol failures. + +## Conceptual Public Interface + +The public contract should support semantics equivalent to: + +```cpp +template +class [[nodiscard]] Result { +public: + bool has_value() const noexcept; + explicit operator bool() const noexcept; + + T& value() &; + const T& value() const &; + T&& value() &&; + + Error& error() &; + const Error& error() const &; +}; +``` + +Construction/factory details may differ in implementation, but the success/failure semantics are frozen. + +## `[[nodiscard]]` + +`Result` should be declared `[[nodiscard]]` so silently ignoring a potentially failed network operation produces a compiler diagnostic where supported. + +Example: + +```cpp +cpp_request::Client client; +client.get("http://example.com"); // should warn when result is discarded +``` + +## State Inspection + +Two equivalent checks are permitted: + +```cpp +if (result.has_value()) { + // success +} + +if (result) { + // success +} +``` + +`operator bool()` must be `explicit` to avoid unintended arithmetic or implicit conversions. + +## Value Access + +`value()` is valid only when `has_value() == true`. + +The v1 contract intentionally does not require `value()` to throw when called in the failure state. + +Calling the wrong-state accessor is a programmer error and has a documented precondition. + +This avoids imposing an exception-based access model on a library whose normal error handling is explicitly non-exception-based. + +Recommended usage: + +```cpp +auto result = client.get("http://example.com"); +if (!result) { + const auto& error = result.error(); + // handle error + return; +} + +const auto& response = result.value(); +``` + +## Error Access + +`error()` is valid only when `has_value() == false`. + +Like `value()`, calling it in the opposite state violates the accessor precondition. + +## Ownership + +`Result` owns whichever active state it contains. + +Therefore: + +- successful `Result` owns its `Response`, +- failed `Result` owns its `Error`, +- moving a result transfers its active state according to `T` / `Error` move semantics, +- copying is available only when the contained type permits it. + +## Storage Representation + +The exact physical storage is intentionally not part of the public API contract. + +Acceptable implementation approaches may include: + +- `std::variant`, +- manually managed discriminated storage, +- another zero-extra-allocation representation. + +The implementation must be benchmarked/inspected before choosing a more complex custom representation solely for performance. + +C++17 makes `std::variant` a valid baseline candidate and avoids unnecessary custom lifetime machinery during initial development. + +## Allocation Policy + +`Result` itself must not require a separate heap allocation solely to store its success/error discriminator. + +Any allocation performed by `T` remains a property of `T`, not of the result abstraction. + +## `Result` + +Operations that can fail but do not naturally return a value may use a `Result` specialization or an equivalent project-owned success type. + +Conceptually: + +```cpp +Result operation(); +``` + +The specialization must preserve the same state-inspection and `error()` semantics. + +Exact implementation is deferred until a concrete internal/public operation needs it. + +## No `ErrorCode::None` Requirement + +Success is represented by the `Result` state itself, not by an `ErrorCode::None` sentinel. + +This keeps the model explicit: + +```mermaid +flowchart LR + R[Result] -->|success| V[T] + R -->|failure| E[Error] +``` + +There is no valid state where a failure contains a "no error" code. + +## Error Propagation + +Internal functions should propagate failures without converting them to text and reparsing them later. + +Example conceptual flow: + +```mermaid +sequenceDiagram + participant C as Client + participant H as HTTP + participant T as Transport + participant P as Platform + + C->>H: execute request + H->>T: write/read bytes + T->>P: socket operation + P-->>T: native failure + T-->>H: Result / Error + H-->>C: propagate portable Error + C-->>C: return Result +``` + +## Exception Boundary + +`Result` is responsible for expected operation failures, not catastrophic runtime conditions such as allocation failure. + +The contract therefore distinguishes: + +- expected network/protocol failures → `Result` failure, +- programmer contract violations → accessor precondition violation, +- unrelated standard-library/system failures → not redefined by this result model. + +## Non-Goals + +For v1, `Result` does not need to provide a large functional-combinator API such as: + +- `and_then` +- `transform` +- `or_else` +- monadic pipelines + +These may be added later if real usage demonstrates value. The initial API should stay small and focused.