basecamp / basecamp/basecamp-sdk
SDKs disagree about which statuses honour Retry-After
- Dominant language
- Go
- Stars
- 49
- Forks
- 12
- Avg merge
- 20h 47m
- Merged PRs (30d)
- 89
Description
Split out of #564, which fixes SPEC §6's Retry-After **parsing** algorithm (Kotlin's missing
HTTP-date branch, and TypeScript's two re-implementations of the parser). Parsing is only half the
contract: SPEC §6 says how to turn a header value into seconds, and says nothing about **which
response statuses** a parsed value is honoured at. The six SDKs have quietly landed in three
different places, and #564 deliberately left that alone — converging it is a behaviour change across
five SDKs, not a parsing fix.
### Where each SDK stands today
| SDK | Honours `Retry-After` on | Evidence |
|---|---|---|
| Python | **any** status | `python/src/basecamp/errors.py:257` parses the header for every status; `errors.py:291` (`err.retry_after = err.retry_after or retry_after`) attaches it to *every* error; `python/src/basecamp/_http.py:374-378` (`_calculate_delay`) returns it in place of backoff whenever it is positive |
| Go | 429 **and 503** | `go/pkg/basecamp/http.go:202-204` — `if resp.StatusCode == 429 \|\| resp.StatusCode == 503`. The older typed-client path at `go/pkg/basecamp/client.go:846-848` is 429-only |
| Ruby | 429 only | `ruby/lib/basecamp/http.rb:625` parses it for every status, but `http.rb:639` passes it only into `RateLimitError`; `http.rb:701-704` (`calculate_delay`) can therefore only ever see a 429's value |
| Kotlin | 429 only (for the delay) | `kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/http/BasecampHttpClient.kt:204-208` — the parsed value is attached to the exception on any status, but `status == 429` gates whether it becomes the sleep |
| Swift | 429 only | `swift/Sources/Basecamp/HTTP/HTTPClient.swift:548-549` — `if statusCode == 429, let retryAfter = ...` |
| TypeScript | 429 only | `typescript/src/retry.ts:147-148` and `typescript/src/services/base.ts:290-291` — both read the header only when `response.status === 429` |
Note that 503 is in every operation's declared `retryOn` set, so this is not a theoretical
difference: a 503 carrying `Retry-After: 120` is obeyed by Go and Python and ignored by the other
four, which back off ~1s instead.
### What the RFCs say
RFC 9110 §10.2.3 defines `Retry-After` as a general response header field — it is not restricted to
a status set, and the section gives explicit semantics for two cases: a **503 (Service
Unavailable)**, where it indicates how long the service is expected to be unavailable, and any
**3xx (Redirection)**, where it is the minimum wait before issuing the redirected request. RFC 6585
§4, which defines **429**, says such a response "MAY include a Retry-After header indicating how long
to wait before making a new request". So 429 is a permitted use, not the canonical one; 503 is the
canonical one, and it is the status four of our six SDKs ignore the header at.
### What to decide
SPEC §6 should say which statuses honour a parsed `Retry-After`, and then the SDKs either converge on
it or the remainder is recorded as a deliberate divergence with the reason. Candidate positions:
1. **Any retryable status** (Python's position). Closest to RFC 9110; the parsed value simply
replaces the backoff term wherever a retry is already going to happen. Cheapest to state: the
status gate disappears entirely and the existing retry-eligibility gates (SPEC §7's three gates)
do all the work.
2. **429 + 503** (Go's position). Covers both canonical uses and nothing else. Requires a rule for
what happens when a future `retryOn` set grows.
3. **429 only** (the current majority). Defensible only as "we do not trust origin-side
`Retry-After` on 5xx"; that argument should be written down if it is the one we pick, because it
is the position furthest from the RFC.
Whichever is chosen, the fix wants a conformance case per honoured status, and #564's new
`Retry-After`-rejection cases in `conformance/tests/retry.json` are the template.
Contributor guide
Research direction
Start with SPEC §6 and the status-handling paths named in python/src/basecamp/errors.py, python/src/basecamp/_http.py, go/pkg/basecamp/http.go, ruby/lib/basecamp/http.rb, kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/http/BasecampHttpClient.kt, swift/Sources/Basecamp/HTTP/HTTPClient.swift, and the TypeScript retry files. Review conformance/tests/retry.json and decide which statuses the specification honours. Done means the policy is documented, conformance cases cover each honoured status, and SDK behavior is converged or its divergence is recorded.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, kotlin, python, ruby, swift, typescript
- Domain
- api, testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100