basecamp / basecamp/basecamp-sdk
SPEC §6 Retry-After parsing: SDKs disagree on rounding and on over-range values
- Dominant language
- Go
- Stars
- 49
- Forks
- 12
- Avg merge
- 20h 47m
- Merged PRs (30d)
- 89
Description
SPEC §6's parsing algorithm left two details unstated, and the SDKs picked differently on both. #796 wrote the classification into SPEC and brought Go into line; this issue is the convergence, and it carries the tables because they are too varied for a sentence.
## 1. Over-range delta-seconds (step 1)
RFC 9110 spells `delay-seconds` as `1*DIGIT` with no upper bound, so a value too large for the SDK's integer type is over-range, not malformed. §7 note 4 permits either saturating at a host limit or refusing a value the parser's numeric type cannot hold — but the SDKs are not split two ways, they are split four, and two of them reach the boundary *after* parsing:
| SDK | above its parse limit | where |
|---|---|---|
| Go | saturates at 2147483647s (as of #796) | in the parser |
| Swift | **rejects** — `Int(value)` returns nil, falls through to backoff | `BasecampError.parseRetryAfter` |
| TypeScript | **rejects** above `Number.MAX_SAFE_INTEGER` | `errors.ts` |
| Kotlin | **rejects** above `Int.MAX_VALUE` — `toIntOrNull()` returns null | `Pagination.kt`. Note its HTTP-date branch *does* saturate at `Int.MAX_VALUE`, so Kotlin is internally split between its own two branches |
| Python | parses fine (arbitrary precision), then **raises** downstream | `_calculate_delay` does `float(server_retry_after)`; verified: `float(10**400)` → `OverflowError: int too large to convert to float`, on the retry path, before any sleep |
| Ruby | parses fine (arbitrary precision), then **raises** downstream | `sleep(delay)`; verified: `sleep(10**400)` → `RangeError: bignum too big to convert into 'long'` |
Python and Ruby are the sharp end: an unhandled exception on the retry path, raised out of a 429 response, where every other SDK either waits or backs off.
Rejecting means a server that asked for a very long wait is retried a few milliseconds later, which is the failure shape #795/#796 fixed on Go's raw path by another route.
Related: `1*DIGIT` excludes a sign, and parsers that delegate to a stdlib integer parse tend to accept one. Go accepted `+5` (honoured as 5) and, once it saturated range errors, would have turned `+9223372036854775808` into a 68-year wait; #796 added a digits-only guard. Worth checking the other five.
## 2. HTTP-date rounding (step 2)
| SDK | rounding |
|---|---|
| TypeScript | up — `Math.ceil(diffMs / 1000)` |
| Kotlin | up — `(remainingMs + 999) / 1000` |
| Swift | up — `.rounded(.up)` |
| Go | up — integer division plus a remainder bump (as of #796) |
| Python | **down** — `int((date - datetime.now(UTC)).total_seconds())` |
| Ruby | **down** — `(date - Time.now).to_i` |
Truncating discards a sub-second delay entirely (a date 400ms out becomes `0`, which both implementations read as "no usable value", so the request falls onto the local backoff curve), and it retries up to a second before the moment the server named. SPEC §6 step 2 now states the rule.
## Proposed
Converge Python and Ruby on rounding; decide saturate-vs-reject for over-range and converge all six, with Python's and Ruby's downstream raise fixed either way since neither behaviour is one of the two permitted readings.
Note for whoever takes the rounding half: the difference is exactly one second wide, so an equality assertion against a literal is inherently flaky — the room before the answer drops by one is `1 - frac(now)`. Go's test asserts one-sidedly instead (the parsed delay is never shorter than the time actually remaining, re-measured after the parse), which scheduling delay can only weaken toward vacuity, never turn red.
Contributor guide
Research direction
Start with the SPEC §6 changes from #796, then inspect BasecampError.parseRetryAfter, errors.ts, Pagination.kt, Python's _calculate_delay, and Ruby's sleep path. Reproduce the over-range and HTTP-date cases, including sign handling, across all six SDKs. Done means the SDKs follow one permitted over-range policy, round dates consistently, and do not raise on a 429 retry path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, kotlin, python, ruby, swift, typescript
- Domain
- api
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100