Skip to content

Phase 1 of the API redesign: make the safe spelling the only one - #56

Merged
kelsayed17 merged 10 commits into
mainfrom
claude/api-redesign-phase-1-02bc16
Nov 6, 2026
Merged

kelsayed17 merged 10 commits into
mainfrom
claude/api-redesign-phase-1-02bc16

Conversation

@kelsayed17

@kelsayed17 kelsayed17 commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

What changes

Phase 1 of the public API redesign: safety by construction. The spec is specs/2026-11-05-public-api-redesign-design.md (section 1). The plan is specs/2026-11-05-phase-1-safety-by-construction.md. Both are committed here under specs/, outside the published docs. This is a clean break, with no deprecation aliases.

Breaking changes

  1. A call's URL is typed LiteralString | Template | httpx2.URL.
    • Applies to every verb, stream, paginate and PageRequest.
    • A computed str such as f"items/{uid}" no longer type-checks. It silently skipped path escaping: with uid="../../admin" it reached /admin.
    • Write a t-string for a path built from values, or httpx2.URL(...) for a URL taken from a response.
    • Pagers forward next-page URLs as httpx2.URL, so they are sent verbatim with their escapes. Any str is still accepted at runtime.
  2. A middleware must declare credential_headers and credential_params.
    • Each is a frozenset[str], empty when the middleware stamps none.
    • HttpOptions refuses with a TypeError, naming the class or function, a middleware that leaves either out, misspells it (credential_header), or gives a set, list, str or non-str members.
    • A plain function is no longer a middleware.
    • Before, an undeclared or misspelled declaration sent the credential to every origin.
  3. SigningAuth signs every attempt, inside the retry layer.
    • The auth flow marks the request. MiddlewareTransport signs after the last middleware and before the credential guard, so a retry or hedge carries a fresh signature and a cross-origin redirect hop has the signature withheld.
    • The 401 re-send is kept, signed afresh.
    • Only the client's own SigningAuth signs. A SigningAuth wrapped in another httpx2.Auth, or given to a bare httpx2.AsyncClient, sends unsigned. This fails closed: without the check, a wrapped signer's signature reached another origin.
    • Middleware now sees the request before it is signed. A middleware that replaces the request must carry request.extensions over.
    • docs/configuration.md's HMAC example is now a SigningAuth subclass.

The ty dev dependency now has a lower bound (>=0.0.93), because a test runs ty to prove that an f-string URL is rejected.

Left as is, on purpose

  • Middleware inherits its two members from headgate._core.CredentialCarrier instead of redeclaring them. Users only declare attributes and never import _core. Making the seam public belongs to the later phase that publishes the extension types (spec goal 4). For the same reason, the API snapshot shows CallUrl and CredentialCarrier by name, not their contents. The type test and the middleware tests guard those.
  • sign() also runs on a hop the guard will strip. Nothing leaks: the guard removes the signature. Skipping the call would need a new trusts() seam on the guard.
  • Signing happens before the cache and the rate-limit wait, as the stack order requires. The docs say that a long rate-limit backlog can age a short-lived timestamp.
  • CallUrl is a plain alias, not a type statement. With a type statement, ty reports Expected CallUrl and does not name the three accepted types.
  • Two pragma: no cover lines in the tests predate this branch. One of them moved with the code it covers.

Each task was implemented and then reviewed before the next started, and a whole-branch review ran at the end. Its findings are fixed in f91008c.

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 (2257 tests). zensical build --strict and the benchmarks pass. The lowest-direct run passes, and so does an install without extras.
  • 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: only the url: annotations and the Middleware base list changed)

🤖 Generated with Claude Code

kelsayed17 and others added 10 commits November 5, 2026 17:38
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bound ty from below so the lowest-direct job runs the checker the URL type test asserts on, and say in the changelog and docs what a bare, wrapped or middleware-replaced SigningAuth request now does. Cover stream, paginate and PageRequest in the URL type probe, a Link header and an escaped path in the verbatim next-URL test, and a wrapped signer's 401.

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

codspeed Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 44 untouched benchmarks


Comparing claude/api-redesign-phase-1-02bc16 (f91008c) with main (014fe45)

Open in CodSpeed

@kelsayed17
kelsayed17 merged commit afaea04 into main Nov 6, 2026
27 checks passed
@kelsayed17
kelsayed17 deleted the claude/api-redesign-phase-1-02bc16 branch November 6, 2026 03:55
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