fix: add retry, timeout, and rate-limit handling to GitHub API scripts#37084
Open
WYSIATI wants to merge 3 commits intoanthropics:mainfrom
Open
fix: add retry, timeout, and rate-limit handling to GitHub API scripts#37084WYSIATI wants to merge 3 commits intoanthropics:mainfrom
WYSIATI wants to merge 3 commits intoanthropics:mainfrom
Conversation
Extract a shared `githubRequest` utility in `scripts/github-request.ts` that wraps `fetch()` with: - Exponential backoff retry (3 attempts) for transient errors (ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT, etc.) - Retry on retryable HTTP status codes (408, 429, 500, 502, 503, 504) - Respect for `Retry-After` headers from GitHub's rate limiter - Per-request timeout (30s default) via AbortController - 204 No Content handling - Jitter to avoid thundering herd on concurrent retries Migrate all 4 scripts to use the shared utility: - auto-close-duplicates.ts - backfill-duplicate-comments.ts - lifecycle-comment.ts - sweep.ts Previously, a single ECONNRESET or GitHub 502 would crash these CI scripts with no recovery. This is especially problematic for the sweep and auto-close scripts which make hundreds of sequential API calls during batch processing.
24 tests covering: - Basic GET/POST functionality and 204 No Content handling - Non-retryable HTTP errors (401, 403, 422) — no retry, immediate throw - Transient network error retry (ECONNRESET, EPIPE, ETIMEDOUT, ECONNREFUSED, Bun socket close) - Retryable HTTP status codes (429, 500, 502, 503) - Retry exhaustion — throws after max attempts - maxRetries=0 disables all retry - Mixed error scenarios (ECONNRESET → 502 → success) - Request timeout via AbortController - Custom User-Agent header
Test architecture improvements: - Export isTransientError, calculateDelay, parseRetryAfter for direct unit testing — follows common practice of testing internal logic independently from the integration layer - Split tests into unit tests (internal helpers) and integration tests (githubRequest end-to-end) - Nest describe blocks by concern: basic requests, non-retryable errors, transient network errors, retryable HTTP codes, maxRetries=0, mixed scenarios, timeout - Use test.each() for parameterized tests — reduces boilerplate for error code and status code coverage - Move beforeEach/afterEach inside the githubRequest describe block - Extract mockFetch() and failThenSucceed() helpers to eliminate repeated globalThis.fetch reassignment Test count: 24 → 52 (added unit tests for internal helpers)
This was referenced Mar 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
githubRequestutility (scripts/github-request.ts) that wrapsfetch()with retry, timeout, and rate-limit handlinggithubRequestfunctionsscripts/github-request.test.ts)ECONNRESETor GitHub 502 would crash these CI scripts with no recoveryWhat's included
The shared utility adds:
ECONNRESET,ECONNREFUSED,EPIPE,ETIMEDOUT,EAI_AGAIN, etc.)Retry-Afterheaders from GitHub's rate limiterAbortController{}instead of crashing on.json()parseTest architecture
Tests use Bun's native test runner (
bun:test) and are structured as:Unit tests (internal helpers exported for testability):
isTransientError— parameterized viatest.each()across all 10 transient error codes + Bun socket message + AbortError/TimeoutError + negative casescalculateDelay— exponential backoff curve, jitter range, cap at 32sparseRetryAfter— numeric seconds, HTTP-date, edge cases (0, negative, >300s cap, missing header)Integration tests (end-to-end
githubRequest):test.each([401, 403, 422])— immediate throw, 1 attempttest.each(["ECONNRESET", "EPIPE", "ETIMEDOUT", "ECONNREFUSED"])— recovers after N failurestest.each([429, 500, 502, 503, 504])— retries and recoversmaxRetries=0— disables retry for both network and HTTP errorsScripts updated
auto-close-duplicates.tsbackfill-duplicate-comments.tslifecycle-comment.tssweep.tsTest plan
bun test scripts/github-request.test.ts— 52/52 passingbun run scripts/sweep.ts --dry-run— verify no behavioral change in normal path[RETRY]messages appear with backoff delays