Skip to content

feat: add send_with_response method for atomic (response, decoded) pair#33

Merged
lesnik512 merged 10 commits into
mainfrom
feat/send-with-response
Jun 8, 2026
Merged

feat: add send_with_response method for atomic (response, decoded) pair#33
lesnik512 merged 10 commits into
mainfrom
feat/send-with-response

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

  • Adds send_with_response(request, *, response_model) -> tuple[httpx2.Response, T] to both AsyncClient and Client. Returns the raw response and a decoded typed body atomically — for callers who need response metadata (headers, status, request URL) alongside a typed body. Canonical use case: RFC 5988 Link header pagination.
  • Decoder failures wrap as httpware.DecodeError, identical to the existing send(..., response_model=) contract. except httpware.ClientError catches every failure mode.
  • Purely additive: no API surface changes, no deprecations. Spec at planning/specs/2026-06-08-send-with-response-design.md; implementation plan at planning/plans/2026-06-08-send-with-response-plan.md.

Test plan

  • Async tests in tests/test_client_send_with_response.py — 8 cases (success, headers preserved, request URL preserved, decode failure, malformed JSON, ClientError catches, StatusError on 4xx, middleware chain runs). All passing.
  • Sync tests in tests/test_client_send_with_response_sync.py — same 8 cases on Client. All passing.
  • Full test suite (397 tests, 100% coverage) green.
  • Lint clean (ruff format, ruff check, ty check).
  • mkdocs --strict builds clean.
  • Public API surface unchanged (tests/test_public_api.py).
  • Optional-extras isolation tests still pass.

Target release: 0.8.2 (patch — additive).

🤖 Generated with Claude Code

lesnik512 and others added 10 commits June 8, 2026 08:28
Tests cover: returns (response, decoded); preserves response headers;
preserves response.request.url; decode failure raises DecodeError with
the right original; malformed JSON raises DecodeError; DecodeError is
caught by ClientError; 4xx raises StatusError (not DecodeError); user
middleware mutation is observable on the wire and on response.request.

All tests currently fail with AttributeError — implementation lands next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rtions

Code quality review caught:
- `_client_with_payload`'s `record` keyword-only param had no caller
  (the middleware test builds its own inline handler) — dropped.
- `test_send_with_response_malformed_json_raises_decode_error` now
  asserts the full DecodeError shape (status_code/model/original),
  matching test_client_response_model.py's analog.

Tests still all fail with AttributeError — red phase preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns (response, decoded) atomically — routes the decode through the
configured ResponseDecoder so decoder failures surface as DecodeError,
identical to send(request, response_model=...). Use case: callers who
need response headers (Link, X-Total-Count, ...) alongside a typed
body, most commonly Link-header pagination.

Spec: planning/specs/2026-06-08-send-with-response-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sync siblings of the async test file: same eight cases, using
Client / httpx2.Client / before_request. All currently fail with
AttributeError; implementation lands next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sync sibling of AsyncClient.send_with_response. Same shape: returns
(response, decoded) atomically and routes the decode through the
configured ResponseDecoder. Identical docstring; sync dispatch path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs/index.md: new "Response metadata + typed body" subsection with a
Link-header pagination example; points body-only callers back at
client.get(..., response_model=).

planning/engineering.md: Seam B contract now names send_with_response
alongside send as the two call sites that wrap decoder exceptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-quality review on the Response metadata + typed body subsection
flagged that the example used undefined names without acknowledgment
and the closing "when to use what" list skipped the build_request
+ body-only case. This fixes both:

- One-line note above the example: process/next_link are caller-defined
- Adds a sentence pointing readers at client.send(request, response_model=)
  as the body-only alternative when they need a custom Request
- "Link-header" -> "Link header" to match RFC 5988 terminology

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 0.8.2 spec deliberately scoped send_with_response to a single
method; the per-verb sibling form (get_with_response etc.) was
considered and deferred. Recording here so it isn't forgotten if a
concrete consumer demand surfaces later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The double-check-with-lock pattern in AsyncBulkhead._check_loop has an
inner `elif self._loop is not current` arm that fires only when two
threads simultaneously pass the cached-loop check and queue on the
lock. It's a real race in concurrent use but extremely hard to trigger
deterministically in a test. Mark it `# pragma: no cover` with a short
justification rather than carry a contrived two-thread test.

Carry-along on PR #33 to unblock CI; bulkhead.py was modified by
df729fa on main and the coverage gap propagated to every PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 merged commit f884a26 into main Jun 8, 2026
5 checks passed
@lesnik512 lesnik512 deleted the feat/send-with-response branch June 8, 2026 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant