Skip to content

feat(exceptions): expose response headers on RoeAPIException (1.0.801)#38

Merged
jadenfix merged 7 commits into
mainfrom
jaden/sdk-1.0.801-retry-after-headers
May 19, 2026
Merged

feat(exceptions): expose response headers on RoeAPIException (1.0.801)#38
jadenfix merged 7 commits into
mainfrom
jaden/sdk-1.0.801-retry-after-headers

Conversation

@jadenfix
Copy link
Copy Markdown
Member

@jadenfix jadenfix commented May 17, 2026

Summary

RoeAPIException now carries the response headers so callers can read Retry-After on 429s symmetrically with the raw-httpx path. Today the SDK discards headers entirely in translate_response, forcing downstream consumers (e.g. roe-mcp) to hardcode retry_after=1.0 because there's no machine-readable signal available — even though roe-main correctly emits the standard Retry-After header on its DRF-throttled responses.

Changes

  • src/roe/exceptions.py: RoeAPIException.__init__ gains a headers: Mapping[str, str] | None = None kwarg; stored as a lowercase-normalised dict so callers can always look up by exc.headers["retry-after"] (httpx Headers iterates lowercased; generated-client MutableMapping preserves original casing — the normalisation makes the contract uniform). translate_response() threads getattr(response, "headers", None) into the raised exception.
  • tests/unit/test_translate_response.py: 7 new tests — Retry-After (numeric + HTTP-date), missing-header, general header preservation on 404/500, stub-without-headers safety, and the uppercased-headers normalisation contract.
  • pyproject.toml: version 1.0.801.0.801. Heads up: this intentionally skips past the natural patch sequence to ship the headers fix immediately on PR merge (auto-tags v1.0.801 via tag-on-release-merge.yml, publish.yml → PyPI). The next roe-main weekly (release-1-0-81-*) will write 1.0.81 to pyproject.toml; that's a downgrade in semver ordering (1.0.801 > 1.0.81), so tag-on-release-merge.yml fires v1.0.81 → publishes 1.0.81 to PyPI. Consumers with loose >= pins resolve to the highest published version (1.0.801) and stay on this PR's code until weekly patch sequence catches up past 1.0.801; consumers with exact ==1.0.81 pins get the natural weekly bundle.

Parity

Companion PRs: roe-ai/roe-typescript#13 (also 1.0.801), roe-ai/roe-golang#14 (1.0.81 — Go takes a different path because its weekly bump path is one of the lower-touched ones; see that PR for rationale).

Test plan

  • uv run pytest tests/unit/test_translate_response.py -v → 23/23 pass (16 existing + 7 new)
  • uv run ruff check src/roe/exceptions.py tests/unit/test_translate_response.py clean
  • CI green

…y-After parity

RoeAPIException now carries response headers so callers can read
Retry-After on 429s symmetrically with the raw-httpx path. Today the
SDK discards headers entirely in translate_response, forcing downstream
consumers (e.g. roe-mcp) to hardcode retry_after=1.0 because there's
no machine-readable signal available — even though roe-main correctly
emits the Retry-After header on its DRF-throttled responses.

Changes:
- src/roe/exceptions.py: add headers: Mapping[str, str] | None = None
  to RoeAPIException.__init__; store as a plain dict copy.
- src/roe/exceptions.py: translate_response() pulls
  getattr(response, "headers", None) and threads it into the raised
  exception. Works for both httpx.Response (Headers) and
  roe._generated.types.Response (MutableMapping[str, str]).
- tests/unit/test_translate_response.py: 6 new tests pinning the
  contract — Retry-After (numeric + HTTP-date), missing header,
  general header preservation on 404/500, and stub-without-headers
  safety.
- pyproject.toml: version bumped 1.0.80 -> 1.0.801 (skip past the
  natural patch sequence so this doesn't collide with the next
  auto-bump from roe-main).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 17, 2026

Greptile Summary

This PR exposes response headers on RoeAPIException so callers can read Retry-After on 429 responses without falling back to the raw-httpx layer. Keys are normalised to lowercase at store time, giving a consistent lookup contract across both httpx.Headers and the generated-client's MutableMapping.

  • RoeAPIException.__init__ gains a headers: Mapping[str, str] | None kwarg; stored as a lowercase-keyed dict via {k.lower(): v for k, v in headers.items()}.
  • translate_response threads getattr(response, \"headers\", None) into the raised exception, with a zero-crash fallback for stubs that omit the attribute.
  • 7 new unit tests cover the numeric Retry-After, HTTP-date form, absent header, general header preservation on 404/500, generated-stub normalisation, and the no-headers stub path; the version is bumped to 1.0.801.

Confidence Score: 5/5

Safe to merge — the change is additive, purely extends the exception with an optional attribute, and is guarded by a no-crash getattr fallback for legacy stubs.

The headers are normalised at store time, the translate_response fallback to None covers any response shape that lacks a .headers attribute, and the 7 new tests exercise all meaningful paths including the uppercase-normalisation contract. No existing behaviour is altered; the new headers kwarg defaults to None so all current call sites remain valid.

No files require special attention.

Important Files Changed

Filename Overview
src/roe/exceptions.py Adds headers kwarg to RoeAPIException.__init__ with lowercase normalisation; translate_response picks up headers via getattr with a safe fallback. Previously-flagged casing issue is addressed.
tests/unit/test_translate_response.py 7 new tests fully cover the headers contract (httpx path, generated-client stub, absent header, uppercase-normalisation, no-headers stub). Test name test_429_no_retry_after_header_when_absent now has an updated docstring clarifying headers dict is not empty.
pyproject.toml Version bumped 1.0.801.0.801; intentional skip to publish headers fix ahead of the weekly semver sequence, as described in the PR.
uv.lock Lock file updated to reflect the new roe-ai version; no other dependency changes.

Reviews (2): Last reviewed commit: "Revert "chore(version): revert pyproject..." | Re-trigger Greptile

Comment thread src/roe/exceptions.py Outdated
Comment thread tests/unit/test_translate_response.py Outdated
Addresses Greptile P1 + P2 on #38:

* RoeAPIException.__init__ now normalises header keys to lowercase at
  store time so `.get("retry-after")` works on both the httpx path
  (which already lowercases on iteration) and the generated-client
  MutableMapping path (which preserves original "Retry-After" casing).
  Without this, callers using the generated client would silently miss
  Retry-After even though the server sent it.

* New test test_generated_response_with_uppercased_headers_normalised_to_lowercase
  pins the contract by passing a stub with original-cased headers and
  asserting lowercase lookup succeeds.

* Rename test_429_no_retry_after_header_yields_empty_map ->
  test_429_no_retry_after_header_when_absent. httpx always synthesises
  content-length etc., so the dict is not actually empty — only the
  absence of retry-after is meaningful.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jadenfix and others added 3 commits May 17, 2026 22:18
…ease)

The earlier 1.0.801 bump (chosen to "skip past the natural patch
sequence") had an unintended consequence: 1.0.801 > 1.0.81 in semver
ordering. Once published, pip/npm/go-proxy always resolve to the
highest semver, so future weekly releases (1.0.81, 1.0.82, ...) from
roe-main's release-1-0-X branches would publish successfully but be
permanently uninstallable — users would stay pinned to 1.0.801
forever even after subsequent releases.

1.0.81 is the natural next patch (current PyPI latest is 1.0.80) and
keeps the weekly release pipeline working unchanged. Verified 1.0.81
is unclaimed on PyPI, npm, and the Go proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks back the manual 1.0.801 -> 1.0.81 bump on this branch and
leaves the version at 1.0.80 (unchanged from main). Rationale:

- roe-python is in active use; cleanest release path is to let
  roe-main's next weekly release-1-0-81 branch fire prepare-target
  for this repo, which will write 1.0.81 to pyproject.toml as part
  of the regular codegen-refresh PR. Our headers feature ships
  bundled into that natural 1.0.81 release with zero coordination.
- Leaves tag-on-release-merge.yml a no-op on this PR's merge
  (pyproject.toml is unchanged from main, so no v* tag is cut,
  publish.yml doesn't fire). The exceptions.py + test changes sit
  on main until the weekly picks them up.
- Preserves parity with roe-typescript (also reverted to 1.0.80).
  roe-go takes a different path — see roe-ai/roe-golang#14 commit
  message for why.

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

Restoring the 1.0.801 bump. Earlier flip-flop was my mistake — user
intent all along is:

  Python + TS at 1.0.801 (manually published immediately on merge)
  Go at 1.0.81 (manually published immediately on merge)

Next roe-main weekly (release-1-0-81-*) will:
  - Python/TS: write 1.0.81 to version files (a downgrade from
    1.0.801). tag-on-release-merge fires v1.0.81 -> publishes 1.0.81
    on PyPI / npm. Consumers with loose `>=` pins keep resolving
    1.0.801 (PEP 440 / semver picks highest); consumers with exact
    `==1.0.81` pins get the natural weekly bundle.
  - Go: writes 1.0.81 (same value as already on main). No diff in
    VERSION, no tag, no new publish — a clean "dud" weekly for Go.

This restores parity at 1.0.801 across Python + TS now and accepts
the orphan-release behaviour for the patch range 1.0.81..1.0.800
(those weekly releases will publish but stay un-installed by loose
pins until weekly version sequence catches up past 1.0.801).

This reverts commit 1aa8965a92099ef9ec24f4af9f5d2a0eed2c0d9c.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jadenfix
Copy link
Copy Markdown
Member Author

@greptile please re-review — latest commit includes Greptile-flagged fix-ups (lowercase-key contract / checked type assertions) plus the version-strategy decision (Python+TS at 1.0.801, Go at 1.0.81; see PR description for rationale)

jadenfix and others added 2 commits May 17, 2026 22:41
Adds a brief snippet right after the typed-exception section showing
how to read Retry-After (and other response headers) off the
exception, paired with a status_code==429 gate since Python's
exception hierarchy doesn't have a dedicated RateLimitError class
(429 raises bare RoeAPIException).

Documents the lowercase-key contract callers need to know.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jadenfix jadenfix merged commit 8ed1964 into main May 19, 2026
1 check passed
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.

2 participants