Skip to content

retries input retries the codes in retry-exempt-status-codes (403/404/422 are sent retries+1 times) #732

Open

Description

@pleberre

Describe the bug

When retries is set, a response with a status listed in retry-exempt-status-codes is still retried, immediately and up to retries times. With retries: 3, a 403, 404 or 422 is sent 4 times. The README and the input description say those codes are not retried.

The cause is how getRetryOptions (src/retry-options.ts) passes the count. It puts the count in request.retries and the exempt list in retry.doNotRetry. In @octokit/plugin-retry 8.1.0, errorRequest honours doNotRetry: for an exempt status it rethrows without scheduling a retry. But the failed handler in wrapRequest reads error.request.request.retries for every error. That is the request.retries value github-script set, so the limiter retries anyway. retryAfter is unset on that path, so the delay is 0.

If the count is passed as retry: { retries } instead, and request.retries is left unset, the exempt list is honoured.

The same root cause is reported upstream as octokit/plugin-retry.js#576 ("404 being retried", open since 2024-09; the reporter sets request: { retries: 9 }).

To Reproduce

This is the option shape github-script v9.0.0 builds for retries: 3 with the default exempt codes, run against @octokit/core 7 and @octokit/plugin-retry 8.1.0 (the versions in its lockfile). It uses a stub fetch:

import { Octokit } from '@octokit/core';
import { retry } from '@octokit/plugin-retry';

async function run(label, status, opts) {
  let calls = 0;
  const fetch = async () => {
    calls++;
    return new Response('{"message":"x"}', { status, headers: { 'content-type': 'application/json' } });
  };
  const O = Octokit.plugin(retry);
  const o = new O({ ...opts, request: { ...opts.request, fetch } });
  await o.request('GET /repos/a/b').catch(() => {});
  console.log(label, status, 'calls:', calls);
}

// What github-script builds for `retries: 3`
const githubScript = { retry: { enabled: true, doNotRetry: [400, 401, 403, 404, 422] }, request: { retries: 3 } };
// Count in the retry options instead
const retryOptions = { retry: { enabled: true, retries: 3, doNotRetry: [400, 401, 403, 404, 422] } };

await run('github-script shape', 403, githubScript);
await run('github-script shape', 404, githubScript);
await run('github-script shape', 422, githubScript);
await run('retry.retries shape', 403, retryOptions);

Output:

github-script shape 403 calls: 4
github-script shape 404 calls: 4
github-script shape 422 calls: 4
retry.retries shape 403 calls: 1

The retry.retries shape still retries a 502 with the 1 s / 4 s / 9 s backoff.

Expected behavior

A status in retry-exempt-status-codes is sent once. This matters most for 403. With retries set, a rate-limited or forbidden request is sent again at once, several times. It also delays every 404 a script expects and handles.

Possible fix

In getRetryOptions, put the count in the retry options (retryOptions.retries = retries) rather than in requestOptions.retries. The secondary clients from the injected getOctokit would inherit it through createConfiguredGetOctokit unchanged.

Workaround

Leave retries at 0 and build clients with the injected getOctokit(token, { retry: { enabled: true, retries: 3 } }). In v9 the plugin is always attached and the merged retry options enable it.

Versions

  • actions/github-script v9.0.0 (d746ffe35508b1917358783b479e04febd2b8f71)
  • @octokit/plugin-retry 8.1.0, @octokit/core 7.0.6, @octokit/request 10.0.8 (from the v9.0.0 package-lock.json)
  • Reproduced with Node 22

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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