Skip to content

Fix four public-API bugs: json+data, retry=True on a view, wrong-kind options, paginate on a subclass - #54

Merged
kelsayed17 merged 3 commits into
mainfrom
claude/public-api-elegance-eval-47a0c1
Nov 6, 2026
Merged

kelsayed17 merged 3 commits into
mainfrom
claude/public-api-elegance-eval-47a0c1

Conversation

@kelsayed17

Copy link
Copy Markdown
Owner

What changes

Four bugs that an adversarial review of the public API turned up, each reproduced by a test that failed before the fix.

  • post(json=..., data=...) raises ValueError. httpx2 sent the form alone and silently dropped the JSON; check_one_body promised to refuse exactly that and did not check this pair.
  • A call's retry=True undoes its view's retry=False. A verb's retry= defaulted to True, which doubled as "inherit", so a call could not override a view's retry=False, contrary to "a call's own options win over the view's". The default is now None (the view's); True skips the view, leaving the route's policy or the client's (a route's NO_RETRY still holds, and a test pins that).
  • A value of the wrong kind is refused where it is written, with a TypeError naming the option and what it takes: rate=10, TransportPolicy(retry=None) (points at NO_RETRY), TransportPolicy(breaker=3), and timeout=5.0 on HttpOptions, with_options or a call (refused before anything is sent). Each used to pass construction and fail at the first request with an AttributeError.
  • paginate fetches pages through AsyncBaseClient's own get/post, so an SDK subclass whose endpoint method is named get or post can still paginate; before, every walk raised a TypeError from the subclass's method.

Also: BudgetBacklogError's docstring no longer claims it is public (callers only ever see it as ClientTimeoutError).

For reviewers: the retry default change is visible in the public signatures (verbs, stream, paginate), so the API-surface snapshot changed; the diff is only retry: 'bool | RetryPolicy' = True → retry: 'bool | RetryPolicy | None' = None. It is listed under Changed in the changelog. Not changed on purpose: using a closed client still raises RuntimeError, which an existing test pins. Other late-failing wrong-kind values (e.g. paginate(limit=100)) are left for a follow-up.

Checklist

  • uv run ruff check, uv run ruff format --check and uv run ty check pass, with no suppression comments
  • uv run python -m pytest --cov passes at 100% line and branch coverage
  • Docs (README / docs/) and docstrings describe the new behaviour
  • CHANGELOG.md has an entry under "Unreleased"
  • A change to the public API is reflected in the API-surface snapshot (--snapshot-update, diff reviewed)

🤖 Generated with Claude Code

- Refuse json= with data=: httpx2 sent the form and dropped the JSON.
- Default a verb's retry= to None (the view's), so a call's retry=True
  can undo a view's retry=False, as "a call's own options win" says.
- Refuse a value of the wrong kind where it is written, with a TypeError
  naming the option: rate=10, TransportPolicy(retry=None) or breaker=3,
  and timeout=5.0 on HttpOptions, a view or a call. Each used to fail at
  the first request with an AttributeError.
- Page through AsyncBaseClient's own get/post, so a subclass whose
  endpoint method is named get or post can still paginate.

Also correct BudgetBacklogError's docstring, which called it public.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codspeed

codspeed Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 44 untouched benchmarks


Comparing claude/public-api-elegance-eval-47a0c1 (18d9576) with main (713b4e5)

Open in CodSpeed

hishel 2.0.0 dropped the filter classes the cache layer imports, so
`import headgate` failed wherever the newest hishel was resolved: the
core-only install job and the lowest-dependencies job (whose `uv run`
re-syncs at the highest resolution) both broke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kelsayed17 added a commit that referenced this pull request Nov 6, 2026
## What changes

hishel 2.0.0 removed `FilterPolicy`, `BaseFilter` and `EntryMeta` and
rewrote its storage interface, so `import headgate` fails wherever the
newest hishel is installed. This ports the cache layer to hishel 2 and
requires `hishel>=2.0.0`. (#54 caps hishel below 2 as a stopgap; this
replaces that cap.)

**Public API (breaking, pre-1.0, listed in the changelog):**
- Removed `HttpCache(policy=...)`, `HttpCache.filter_policy()`, and
`headgate.cache.MethodFilter`, `PathFragmentFilter`, `StatusCodeFilter`:
they were built on hishel's filters. A POST can no longer be cached
(`use_body_key` has no hishel 2 equivalent).
- `methods`/`paths`/`statuses` keep working, now applied by headgate: a
request they don't name goes straight past hishel, a status they don't
name is never written. New `HttpCache.caches_request(method, path)` and
`stores_status(status)` say what they decide.
- `statuses` may name only 2xx other than 206 and raises `ValueError`
otherwise, since hishel 2 stores nothing else (so a declared 404 answer
is no longer cached).

**Behavior kept, or fixed, against hishel 2:**
- **No credential reaches storage.** hishel 2 records the stored request
from the request object *after* the network send, by which point the
layer below had restored the real URL — a query-string credential was
being stored. The credential-free view is now put back as soon as the
send returns.
- **`Vary` is honored.** hishel 2 keys entries by URL alone and ignores
`Vary`; headgate matches the varied headers itself (as keyed digests). A
`Vary: *` response is no longer stored. Under `Partition.SHARED`, a
response varying on a header the cache never stores (`Authorization`, a
cookie, a declared credential) was served to every caller — this
predates the port; such a response is now never stored.
- Lifetimes and hits move to hishel 2's `caching` extension
(`stamp_ttl`, `served_from_cache`). The `stored` flag reads `False` on a
miss until the call accepts it. GETs asking for different `cache_ttl=`
no longer share one download.

**For reviewers:**
- hishel 2's own storages log and absorb their faults instead of
raising, so `cache_degradations()` now counts only faults a storage
raises; documented in the caching guide.
- hishel 2 cannot read a hishel 1 SQLite database (every lookup raises,
so every call degrades to the network). The changelog tells users to
point the storage at a new file. Its default file name is now
`hishel_cache_v2.db`.
- One test was removed: a cached 503 not charging the breaker, a state
hishel 2 can no longer produce. `tests/cache/test_filters.py` became
`test_rules.py`.
- Two mutation-testing rows in `docs/mutation-testing.md` that described
removed code were dropped; the surviving-mutant table has not been
re-run with mutmut.
- An independent review of the diff probed credential storage (query
params, userinfo, `X-Api-Key`, `Set-Cookie`, HEAD, retries, 30
concurrent mixed-credential GETs) and found no leaks; its findings are
all fixed here, each with a test that fails without the fix.

## Checklist

- [x] `uv run ruff check`, `uv run ruff format --check` and `uv run ty
check` pass, with no suppression comments
- [x] `uv run python -m pytest --cov` passes at 100% line and branch
coverage (2225 tests)
- [x] Docs (README / docs/) and docstrings describe the new behaviour
(`zensical build --strict` clean)
- [x] `CHANGELOG.md` has an entry under "Unreleased"
- [x] A change to the public API is reflected in the API-surface
snapshot (`--snapshot-update`, diff reviewed)

Also reproduced locally: the lowest-declared-dependencies job, the
install without extras, and the benchmarks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…gance-eval-47a0c1

# Conflicts:
#	CHANGELOG.md
#	pyproject.toml
#	uv.lock
@kelsayed17
kelsayed17 enabled auto-merge (squash) November 6, 2026 01:33
@kelsayed17
kelsayed17 merged commit 014fe45 into main Nov 6, 2026
16 checks passed
@kelsayed17
kelsayed17 deleted the claude/public-api-elegance-eval-47a0c1 branch November 6, 2026 01:36
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