fix(github): add a per-request timeout to all Octokit calls

Octokit v22 uses native fetch, which has no built-in timeout, so a stuck
GitHub request hung until the PR-status route's 12s overall budget fired —
and one slow request could consume the entire budget. Wrap fetch with an 8s
AbortSignal.timeout via a shared createOctokit() factory, and route the inline
Octokit instantiations through it too.
This commit is contained in:
Bohdan Triapitsyn
2026-06-29 01:56:56 +03:00
parent f45089ccce
commit e5773662b3
3 changed files with 30 additions and 9 deletions
+1
View File
@@ -21,6 +21,7 @@ export {
export { export {
getOctokitOrNull, getOctokitOrNull,
createOctokit,
} from './octokit.js'; } from './octokit.js';
export { export {
+21 -1
View File
@@ -2,6 +2,26 @@ import { Octokit } from '@octokit/rest';
import { getGitHubAuth, isGhCliActive, isGhCliDisabled } from './auth.js'; import { getGitHubAuth, isGhCliActive, isGhCliDisabled } from './auth.js';
import { getGhCliToken } from './gh-cli-credential.js'; import { getGhCliToken } from './gh-cli-credential.js';
// Per-request timeout for every GitHub call. Octokit v22 uses native fetch,
// which has no built-in timeout — without this, a stuck connection hangs until
// some outer bound (the PR-status route's 12s overall budget) fires, and a
// single slow request can eat the whole budget. Bounding each request lets the
// caller fail fast and fall back to cached state instead.
const OCTOKIT_REQUEST_TIMEOUT_MS = 8000;
const timeoutFetch = (url, options = {}) => {
// Respect a caller-provided signal if present; otherwise attach our timeout.
if (options.signal) {
return fetch(url, options);
}
return fetch(url, { ...options, signal: AbortSignal.timeout(OCTOKIT_REQUEST_TIMEOUT_MS) });
};
/** Create an Octokit instance with a per-request timeout applied. */
export function createOctokit(token) {
return new Octokit({ auth: token, request: { fetch: timeoutFetch } });
}
export function getOctokitOrNull() { export function getOctokitOrNull() {
const auth = getGitHubAuth(); const auth = getGitHubAuth();
const ghToken = !isGhCliDisabled() ? getGhCliToken() : null; const ghToken = !isGhCliDisabled() ? getGhCliToken() : null;
@@ -9,5 +29,5 @@ export function getOctokitOrNull() {
if (!token) { if (!token) {
return null; return null;
} }
return new Octokit({ auth: token }); return createOctokit(token);
} }
+8 -8
View File
@@ -108,8 +108,8 @@ export function registerGitHubRoutes(app) {
if (ghToken !== null && !ghCliDisabled) { if (ghToken !== null && !ghCliDisabled) {
try { try {
const { Octokit } = await import('@octokit/rest'); const { createOctokit } = await import('./octokit.js');
ghCliUser = await getGitHubUserSummary(new Octokit({ auth: ghToken })); ghCliUser = await getGitHubUserSummary(createOctokit(ghToken));
} catch { } catch {
ghCliUser = null; ghCliUser = null;
} }
@@ -246,8 +246,8 @@ export function registerGitHubRoutes(app) {
return res.status(500).json({ error: 'Missing access_token from GitHub' }); return res.status(500).json({ error: 'Missing access_token from GitHub' });
} }
const { Octokit } = await import('@octokit/rest'); const { createOctokit } = await import('./octokit.js');
const octokit = new Octokit({ auth: accessToken }); const octokit = createOctokit(accessToken);
const user = await getGitHubUserSummary(octokit); const user = await getGitHubUserSummary(octokit);
setGitHubAuth({ setGitHubAuth({
@@ -283,8 +283,8 @@ export function registerGitHubRoutes(app) {
return res.status(404).json({ error: 'GitHub CLI account not found' }); return res.status(404).json({ error: 'GitHub CLI account not found' });
} }
const { Octokit } = await import('@octokit/rest'); const { createOctokit } = await import('./octokit.js');
const user = await getGitHubUserSummary(new Octokit({ auth: ghToken })); const user = await getGitHubUserSummary(createOctokit(ghToken));
setGhCliActive(true); setGhCliActive(true);
const accounts = getGitHubAuthAccounts() const accounts = getGitHubAuthAccounts()
.map((account) => ({ ...account, current: false })) .map((account) => ({ ...account, current: false }))
@@ -319,8 +319,8 @@ export function registerGitHubRoutes(app) {
let ghCliUser = null; let ghCliUser = null;
if (ghToken) { if (ghToken) {
try { try {
const { Octokit } = await import('@octokit/rest'); const { createOctokit } = await import('./octokit.js');
ghCliUser = await getGitHubUserSummary(new Octokit({ auth: ghToken })); ghCliUser = await getGitHubUserSummary(createOctokit(ghToken));
accounts = accounts.concat({ accounts = accounts.concat({
id: GH_CLI_ACCOUNT_ID, id: GH_CLI_ACCOUNT_ID,
user: ghCliUser, user: ghCliUser,