basecamp / basecamp/basecamp-sdk

SPEC §6 Retry-After parsing: SDKs disagree on rounding and on over-range values

Open
#799 3 comments 0 reactions 0 assignees View on GitHub
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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.