basecamp / basecamp/basecamp-sdk

SDKs disagree about which statuses honour Retry-After

Open
#775 9 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.