Skip to content
104 changes: 75 additions & 29 deletions docs/architecture/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,17 @@ The serializer is responsible for producing valid HTTP/1.1 wire format, includin
- terminating CRLF sequence
- request body bytes when present

Automatically generated headers such as `Host` and `Content-Length` may be added when required and not supplied explicitly, subject to the later public API contract.
Automatically generated headers such as `Host` and `Content-Length` are added when required and not supplied explicitly.

For v1 request framing:

- caller-supplied `Transfer-Encoding` is rejected,
- duplicate `Host` is rejected,
- duplicate `Content-Length` is rejected,
- caller-supplied `Content-Length` must exactly match the in-memory body size,
- non-empty bodies receive an automatic `Content-Length` when absent,
- empty `POST`, `PUT`, and `PATCH` requests receive `Content-Length: 0`,
- empty `GET`, `HEAD`, and `DELETE` requests do not receive an automatic zero length.

## Response Processing

Expand All @@ -48,66 +58,102 @@ Malformed protocol data transitions to an error result instead of continuing wit

## Body Framing

The parser must support v1 framing rules for:
The parser supports v1 framing rules for:

1. responses with no message body by HTTP semantics, including `HEAD`,
1. responses terminated by the header section because of request/status semantics,
2. `Transfer-Encoding: chunked`,
3. `Content-Length`,
4. connection-close-delimited body where valid/required by HTTP/1.1 behavior.
4. connection-close-delimited bodies where required by HTTP/1.1 framing.

Framing precedence follows HTTP/1.1 semantics and unsafe ambiguity is rejected rather than guessed.

The v1 hardening policy is:

Framing precedence must follow HTTP semantics; conflicting or invalid framing must not be guessed silently.
- responses to `HEAD` complete after the header section,
- `101`, `204`, and `304` are header-terminated responses,
- `1xx` responses other than `101` are interim and are consumed before the final response,
- framing fields on `1xx`/`204` responses are rejected,
- simultaneous `Transfer-Encoding` and `Content-Length` is rejected as conflicting framing,
- duplicate `Content-Length` fields are rejected rather than normalized,
- `304` and `HEAD` may carry representation-length/framing metadata while still having no response body,
- `205 Reset Content` is content-forbidden but not blindly treated as header-terminated: `Content-Length: 0` and zero-content chunked framing can complete on a persistent connection, while an unframed 205 is close-delimited and cannot be reused,
- any actual decoded/close-delimited content on a 205 is a malformed response.

This conservative policy prioritizes unambiguous message boundaries and connection-reuse safety.

## Chunked Decoder

The chunked decoder owns protocol state for:

- reading hexadecimal chunk size
- consuming chunk extensions without exposing them as body content when present
- consuming each chunk payload
- validating CRLF framing
- recognizing terminal zero-size chunk
- consuming trailer section as required for correct message completion
- reading hexadecimal chunk size,
- parsing chunk extensions,
- consuming each chunk payload,
- validating CRLF framing,
- recognizing the terminal zero-size chunk,
- consuming and validating the trailer section,
- preserving bytes that belong to a following response.

Chunk extensions follow token / quoted-string syntax rather than accepting arbitrary printable bytes. Empty extension names, missing extension values, malformed quoted strings, invalid chunk sizes, and invalid data delimiters are rejected.

Trailer fields are syntax-validated and consumed but are not merged into the public response-header collection in v1. Framing-critical trailer fields such as `Content-Length` and `Transfer-Encoding` are rejected.

Decoded body bytes are accumulated into the v1 in-memory response body.

## Header Handling

Headers should be parsed without requiring per-character heap allocation.
Headers are parsed without requiring per-character heap allocation.

The architecture should permit:
The architecture permits:

- case-insensitive header-name lookup
- duplicate header fields where HTTP permits them
- preserving sufficient response data for user-facing access
- case-insensitive header-name lookup,
- duplicate header fields where HTTP permits them,
- preserving sufficient response data for user-facing access.

Exact container representation is deferred to the public API and performance design phase.
Obsolete folded header lines and invalid field-name/value bytes are rejected.

## Timeout and EOF Semantics

Timeouts remain stage-specific:

- connect timeout bounds connection establishment across candidate endpoints,
- write timeout bounds the complete logical request transmission; segmented head/body writes share one deadline,
- read timeout bounds each wait for additional response data, acting as an inactivity/read-progress timeout rather than one deadline for the entire response.

EOF is interpreted only with HTTP framing context:

- incomplete `Content-Length` or chunked response at EOF is `UnexpectedEof`,
- a valid close-delimited response completes at EOF,
- a read timeout is not treated as EOF and therefore does not complete a close-delimited message.

## Connection Semantics

After a response is complete, the HTTP layer reports whether protocol semantics permit the underlying connection to remain reusable.

Reasons to reject reuse include:

- `Connection: close`
- incomplete or malformed response framing
- transport termination before required body completion
- any parser state that leaves unread bytes ambiguous
- `Connection: close`,
- close-delimited framing,
- protocol upgrade,
- incomplete or malformed response framing,
- transport termination before required body completion,
- any parser state that leaves unread bytes ambiguous.

The `Client` additionally refuses reuse when parser pending bytes remain after a completed non-pipelined request.

## Redirect Semantics

Redirect handling is orchestrated above the low-level parser but uses parsed status and `Location` metadata from this layer.

The v1 redirect policy must:

- respect the configured enable/disable option
- enforce a finite redirect count
- resolve relative locations
- reject redirects requiring unsupported HTTPS transport
The v1 redirect policy:

Method rewriting/preservation rules for redirect status codes must be specified before implementation of redirect execution.
- respects the configured enable/disable option,
- enforces a finite redirect count,
- resolves relative locations,
- rejects redirects requiring unsupported HTTPS transport,
- applies the documented 301/302/303/307/308 method/body policy.

## Parser Design Principle

The parser should support incremental input from `read_some()` rather than assuming one receive call contains a complete status line, header block, chunk, or body.
The parser supports incremental input from `read_some()` rather than assuming one receive call contains a complete status line, header block, chunk, or body.

This is required for correctness over TCP and also enables focused parser benchmarks independent of live network latency.
81 changes: 50 additions & 31 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,49 +252,68 @@ Exit criteria: **complete**.

Goal: close known correctness gaps before treating the API as release-candidate quality.

## 1. `Result<T>` hardening — 🚧 current work
## 1. `Result<T>` hardening — ✅ complete

Required work:
Completed work:

- eliminate wrong-state type-based `std::get` behavior hidden behind `noexcept`.
- use explicit discriminant/index-based state access.
- keep accessor misuse as a documented programmer precondition violation rather than a network error.
- make `Result<Error>` capable of representing both success and failure unambiguously.
- preserve move-only payload support.
- keep `std::variant` as the baseline representation until measurement justifies custom storage.
- keep `Result<void>` deferred until a concrete operation requires it.
- 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<Error>` 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<void>` deferred until a concrete operation requires it.

Exit for this substep:
Historical reference:

- dedicated `Result<T>` unit coverage passes on all CI matrices.
- `Result<Error>` success and failure are distinguishable.
- normal existing implicit error propagation remains source-compatible.
- PR #22 — `Result<T>` state hardening

## 2. URL and query correctness — ⏳ next
Exit criteria: **complete**.

REQ-HTTP-004 requires query behavior beyond merely preserving an already encoded raw query.
## 2. URL and query correctness — ✅ complete

Planned work:
Completed work:

- 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.

- define the v1 query-parameter API or explicitly refine the requirement if raw URL input is the intended contract.
- percent-encode reserved characters where required.
- validate percent escapes.
- strengthen bracketed IPv6 literal validation.
- add query/encoding tests.
Historical reference:

- PR #23 — URL parsing and query parameter hardening

Exit criteria: **complete**.

## 3. HTTP correctness review — ⏳
## 3. HTTP correctness review — 🚧 current work

Planned review:
Current hardening scope:

- enforce safe response framing precedence.
- reject `Transfer-Encoding` + `Content-Length` ambiguity before connection reuse.
- reject framing fields where `1xx` / `204` semantics forbid them.
- keep `HEAD` / `304` header-terminated while preserving allowed representation metadata.
- correct `205 Reset Content` framing so unframed responses are close-delimited rather than incorrectly reusable.
- reject actual content in a 205 response.
- validate chunk extension token / quoted-string grammar instead of accepting arbitrary printable bytes.
- retain trailer validation and reject framing-critical trailer fields.
- emit `Content-Length: 0` for empty POST/PUT/PATCH requests while leaving empty GET/HEAD/DELETE unchanged.
- document timeout boundaries: connect budget across endpoint attempts, one write budget across request transmission, read timeout per wait for response progress.
- preserve context-sensitive EOF semantics: incomplete explicit framing is `UnexpectedEof`; valid close-delimited EOF completes normally; timeout never masquerades as EOF.

Exit for this substep:

- response framing precedence and duplicate framing fields.
- status/no-body edge cases.
- header syntax edge cases.
- chunk extension/trailer correctness.
- request serializer Host/Content-Length policy.
- timeout boundary semantics.
- EOF classification.
- framing/status/chunk edge cases have deterministic parser tests.
- serializer framing policy is covered by tests.
- no reviewed HTTP message-boundary ambiguity remains known.

## 4. Resource-bound review — ⏳
## 4. Resource-bound review — ⏳ next

Because response bodies are memory-resident in v1, document or introduce practical defensive limits where appropriate:

Expand Down
Loading
Loading