Repository navigation
Description
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