Skip to content

Transient network errors are never retried; retry count equals the number of PATs #508

Description

@luojiyin1987

Summary

retryer() is named "retryer", but it does token rotation, not transient retry. The retry count equals the number of PATs:

for (let retries = 0; retries < PATs.length; retries++) {

With a single PAT_1, the fetcher runs at most once. There is no retry for transient failures.

Details

Network-level errors bail out immediately:

// network/unexpected error → let caller treat as failure
if (!e.response) {
  throw err;
}

So these common transient errors never get retried:

  • ECONNRESET
  • ETIMEDOUT
  • socket hang up
  • TLS connection reset
  • DNS temporary failure

Why it matters

One stats card is a multi-request transaction: GraphQL stats, per-repo star lookups, REST search for total commits. A single transient failure anywhere fails the whole card. The next run usually succeeds, so users see random red builds and manually re-run.

Observed pattern in GitHub Actions:

request 1 ✅ ... request 18 ✅ request 19 ❌ transient → card fails
manual re-run → all requests ✅ → card succeeds

Proposal

Token rotation and transient retry are different concerns. Keep the rotation loop over PATs, and add an inner retry with exponential backoff plus jitter for the same PAT:

  • Retry when the error has no response (network level) or the HTTP status is 429/502/503/504.
  • Do not retry permanent failures such as 401 bad credentials, 404 not found, or 422 invalid query.
  • Suggested schedule: 1s, 2s, 4s, each plus a small jitter.
  • Keep the function signature unchanged; expose delays as an optional parameter for tests.

I have a working implementation with tests and will open a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions