Skip to content

feat(net): HTTP contract — HttpError, HttpClient, ResponseBody + net-http-mock (Slice 0 PR 2)#60

Merged
NotAProfDev merged 13 commits into
mainfrom
feat/net-http-contract
Jul 1, 2026
Merged

feat(net): HTTP contract — HttpError, HttpClient, ResponseBody + net-http-mock (Slice 0 PR 2)#60
NotAProfDev merged 13 commits into
mainfrom
feat/net-http-contract

Conversation

@NotAProfDev

@NotAProfDev NotAProfDev commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

PR 2 of Slice 0 (the net-http foundation) — gives oath-adapter-net-http-api its HTTP data-plane contracts and ships the standalone oath-adapter-net-http-mock test harness. Builds on #58.

  • HttpError — one concrete transport/middleware error + HasErrorKind. Model A: HTTP 4xx/5xx statuses are not error-ified — they pass through as Ok(Response) with the body intact for the adapter to classify (ADR-0030 §5); the resilience layers will peek status()/Retry-After.
  • HttpClient — a blanket-impl'd Service sub-trait with send sugar; any backend Service of the right shape is HttpClient for free (RPITIT, no dyn).
  • ResponseBody<B> — a pin-project-lite buffer-xor-stream enum that forwards all three Body methods (poll_frame/is_end_stream/size_hint) to the active arm — a defaulted size_hint/is_end_stream would silently mis-size a caller's .collect() and defeat any max-size guard. BufferMode (Copy).
  • oath-adapter-net-http-mock — standalone harness: MockBody (frame-controllable body), MockTimer (virtual clock with a waker registry — advance drives now and wakes due sleepers; lock-drop-before-wake, no deadlock/lost-wake), MockClient (canned-response Service leaf that records requests). Consumed downstream via [dev-dependencies] only — no dev-dep cycle.

Out of scope (later slices/PRs)

AuthSource/Auth/Guarded (PR 3), RateKey/coverage (PR 4), the resilience layers + stack/build assembly + hyper leaf (later slices), the CircuitOpen HttpError variant (lands with the CircuitBreaker layer).

Verification

just ci green (fmt, fmt-toml, typos, lint, check, test, deny, doc, machete, gitleaks, actionlint, shellcheck), 31 tests. net-http-api stays runtime-free — no tokio/hyper/reqwest/serde in its production graph (tokio/http-body-util are dev-deps). No unsafe; no unwrap/expect in non-test code (Mutex poison recovered via PoisonError::into_inner).

Process

Subagent-driven: 4 tasks, each spec + code-quality reviewed (one Important test-gap on ResponseBody's is_end_stream forwarding was caught and fixed), plus a clean whole-branch review (one doc-list omission fixed).

Closes #59

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added HttpError for unified transport/middleware failures (with error-kind mapping).
    • Introduced HttpClient with send convenience, plus ResponseBody and BufferMode to support buffered vs streaming HTTP response bodies.
    • Added oath-adapter-net-http-mock test harness: MockClient, MockBody, and deterministic MockTimer.
  • Documentation
    • Updated CHANGELOG.md and added an implementation-plan document for the net-http HTTP contract work.

@NotAProfDev NotAProfDev added the enhancement New feature or request label Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1220d1ce-a1bd-467a-b335-0d0a49b33b16

📥 Commits

Reviewing files that changed from the base of the PR and between ae6b4d0 and 4f9bf93.

📒 Files selected for processing (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

This PR adds HTTP data-plane contracts to oath-adapter-net-http-api, introduces oath-adapter-net-http-mock with mock client/body/timer types, and updates workspace wiring, docs, and changelog entries.

Changes

net-http contract and mock harness

Layer / File(s) Summary
HttpError type and ErrorKind mapping
crates/adapter/net/http/api/src/error.rs
Defines BoxError and HttpError with constructors, HasErrorKind, and tests.
HttpClient trait and blanket Service impl
crates/adapter/net/http/api/src/client.rs
Adds HttpClient with send and a blanket Service implementation, with tests.
ResponseBody and BufferMode abstraction
crates/adapter/net/http/api/src/body.rs
Defines BufferMode and ResponseBody<B> with Body forwarding and tests.
MockBody and MockClient harness types
crates/adapter/net/http/mock/src/body.rs, crates/adapter/net/http/mock/src/client.rs
Implements queued-frame body and canned-response request-recording service mocks, with tests.
MockTimer virtual clock
crates/adapter/net/http/mock/src/timer.rs
Implements the virtual clock, Sleep future, Timer forwarding, and tests.
Crate exports, manifests, and workspace deps
crates/adapter/net/http/api/src/lib.rs, crates/adapter/net/http/api/Cargo.toml, crates/adapter/net/http/mock/Cargo.toml, Cargo.toml
Exposes new modules, adds crate manifests, and registers the mock crate and shared dependencies in the workspace.
Changelog and implementation plan docs
CHANGELOG.md, docs/superpowers/plans/2026-07-01-net-http-contract.md
Adds the changelog entry and the contract implementation plan.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • NotAProfDev/oath#56: Covers the same net-http contract surface described here, including HttpError, HttpClient, and ResponseBody/BufferMode.
  • NotAProfDev/oath#40: Related workspace topology and Cargo.toml structure used by the new mock crate registration.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and clearly summarizes the HTTP contract and mock harness changes.
Linked Issues check ✅ Passed The PR implements the linked HTTP contract scope: HttpError, HttpClient, ResponseBody, BufferMode, and the net-http mock harness.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes are evident; the docs, changelog, and dependency updates support the net-http contract work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/net-http-contract

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/adapter/net/http/mock/src/body.rs (1)

50-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: try_from/unwrap_or is defensive but effectively dead code.

usize never exceeds u64 on any Rust-supported target, so the unwrap_or(u64::MAX) fallback is unreachable. A plain f.len() as u64 would be simpler with identical behavior.

♻️ Optional simplification
-            .map(|f| u64::try_from(f.len()).unwrap_or(u64::MAX))
+            .map(|f| f.len() as u64)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/adapter/net/http/mock/src/body.rs` around lines 50 - 57, The
`size_hint` implementation in `Body` uses an unreachable
`try_from`/`unwrap_or(u64::MAX)` fallback when summing `frames`; simplify it by
using a direct `u64` conversion for each frame length and keep the existing
exact total behavior. Update the logic in `size_hint` only, and leave
`SizeHint::with_exact(total)` unchanged.
crates/adapter/net/http/mock/src/client.rs (2)

29-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: accept impl Into<Bytes> for ok.

Slightly nicer ergonomics for callers passing &'static [u8] or Vec<u8> literals without an explicit Bytes::from_static/Bytes::from.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/adapter/net/http/mock/src/client.rs` around lines 29 - 32, Update the
mock HTTP client helper `MockResponse::ok` to accept `impl Into<Bytes>` instead
of requiring a `Bytes` argument, so callers can pass `Vec<u8>` or `&'static
[u8]` directly. Keep the behavior the same by converting the input to `Bytes`
inside `ok` before calling `Self::new`, and leave the `new` constructor
unchanged.

38-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: dedupe the poison-recovery lock pattern.

Same .lock().unwrap_or_else(PoisonError::into_inner) sequence appears twice. Could extract a small private helper, but the duplication is trivial and low-risk here.

Also applies to: 56-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/adapter/net/http/mock/src/client.rs` around lines 38 - 40, The
poison-recovery lock sequence is duplicated in the mock HTTP client, so dedupe
the repeated .lock().unwrap_or_else(PoisonError::into_inner) pattern to keep
`Client` cleaner. Extract a small private helper in
`crates/adapter/net/http/mock/src/client.rs` and use it from the affected
methods around the shared lock access in the `Client` implementation, including
the other occurrence noted in the review.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-07-01-net-http-contract.md`:
- Line 174: The module-list doc entry for the new `error` module has a malformed
inline code span around `HttpError`, which markdownlint flags and will render
incorrectly. Update the `//! - [`error`] — ...` line in the `lib.rs` module-list
documentation so the `HttpError` name is wrapped with a valid inline code span
and the markdown syntax is balanced.

---

Nitpick comments:
In `@crates/adapter/net/http/mock/src/body.rs`:
- Around line 50-57: The `size_hint` implementation in `Body` uses an
unreachable `try_from`/`unwrap_or(u64::MAX)` fallback when summing `frames`;
simplify it by using a direct `u64` conversion for each frame length and keep
the existing exact total behavior. Update the logic in `size_hint` only, and
leave `SizeHint::with_exact(total)` unchanged.

In `@crates/adapter/net/http/mock/src/client.rs`:
- Around line 29-32: Update the mock HTTP client helper `MockResponse::ok` to
accept `impl Into<Bytes>` instead of requiring a `Bytes` argument, so callers
can pass `Vec<u8>` or `&'static [u8]` directly. Keep the behavior the same by
converting the input to `Bytes` inside `ok` before calling `Self::new`, and
leave the `new` constructor unchanged.
- Around line 38-40: The poison-recovery lock sequence is duplicated in the mock
HTTP client, so dedupe the repeated
.lock().unwrap_or_else(PoisonError::into_inner) pattern to keep `Client`
cleaner. Extract a small private helper in
`crates/adapter/net/http/mock/src/client.rs` and use it from the affected
methods around the shared lock access in the `Client` implementation, including
the other occurrence noted in the review.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 903c4af7-a58b-4127-842a-12c357a38fb7

📥 Commits

Reviewing files that changed from the base of the PR and between 19f7b56 and 0364b9b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (13)
  • CHANGELOG.md
  • Cargo.toml
  • crates/adapter/net/http/api/Cargo.toml
  • crates/adapter/net/http/api/src/body.rs
  • crates/adapter/net/http/api/src/client.rs
  • crates/adapter/net/http/api/src/error.rs
  • crates/adapter/net/http/api/src/lib.rs
  • crates/adapter/net/http/mock/Cargo.toml
  • crates/adapter/net/http/mock/src/body.rs
  • crates/adapter/net/http/mock/src/client.rs
  • crates/adapter/net/http/mock/src/lib.rs
  • crates/adapter/net/http/mock/src/timer.rs
  • docs/superpowers/plans/2026-07-01-net-http-contract.md

Comment thread docs/superpowers/plans/2026-07-01-net-http-contract.md Outdated
NotAProfDev and others added 5 commits July 1, 2026 18:26
- Fix malformed inline code span in the plan doc module-list note (MD038).
- Simplify MockBody::size_hint to a direct `as u64` sum (usize→u64 never
  truncates; the try_from/unwrap_or(u64::MAX) fallback was unreachable).
- Accept `impl Into<Bytes>` in MockClient::ok for nicer call-site ergonomics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`Sleep::poll` registered a fresh waker on every pending poll, so a `Sleep`
polled more than once before its deadline (e.g. inside a `select!` woken by a
sibling future — the pattern the ADR-0033 WS resilience layer will use) stacked
duplicate waiter entries and woke the task once per entry.

Guard the registration on `(deadline, will_wake)`: skip when this exact waker
is already queued for this deadline. Deadline alone is not identity (unrelated
futures can share one), so both must match — a re-poll of the same future is a
no-op while a distinct future still registers its own waker. Add a regression
test asserting one registration and exactly one wake.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Extract a private `lock()` helper in the mock crate for the
  `.lock().unwrap_or_else(PoisonError::into_inner)` poison-recovery pattern,
  replacing its six repetitions across MockClient and MockTimer.
- Collapse ResponseBody::poll_frame's Buffered arm from a four-branch `Poll`
  match to a single `Poll::map` that maps the `Infallible` body error away.

No behavior change; /simplify cleanup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies the CodeRabbit nitpick that was missed in 8eb0e4a: usize→u64 never
truncates, so the try_from/unwrap_or(u64::MAX) fallback was unreachable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(net): HTTP contract — HttpError, HttpClient, ResponseBody + net-http-mock (Slice 0 PR 2)

1 participant