Skip to content

Commit 9e1da2e

Browse files
committed
github-stats-action: add jittered backoff and modern GitHub headers; retry on Premature close
1 parent 1f8d54b commit 9e1da2e

File tree

1 file changed

+29
-8
lines changed
  • apps/shared-backend/github-actions/github-stats-action

1 file changed

+29
-8
lines changed

apps/shared-backend/github-actions/github-stats-action/getStats.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,21 +128,40 @@ async function makeRequest(url, options = {}) {
128128
}
129129
}
130130

131-
// Retry helper for GitHub API calls
131+
// Retry helper for GitHub API calls (handles transient network errors like "Premature close")
132132
async function retryWithBackoff(fn, maxRetries = 5, initialDelayMs = 1000) {
133133
let attempt = 0
134134
let delay = initialDelayMs
135+
const maxDelayMs = 30000
135136
// eslint-disable-next-line no-constant-condition
136137
while (true) {
137138
try {
138139
return await fn()
139140
} catch (error) {
140-
const status = error.status || error.code || error?.response?.status
141-
if (attempt < maxRetries && (status === 403 || status === 429 || (typeof status === 'number' && status >= 500))) {
141+
const status = error?.status || error?.response?.status
142+
const code = error?.code || error?.errno
143+
const message = typeof error?.message === 'string' ? error.message : ''
144+
145+
const isStatusRetriable = status === 403 || status === 429 || (typeof status === 'number' && status >= 500)
146+
const isNetworkRetriable =
147+
message.includes('Premature close') ||
148+
message.includes('socket hang up') ||
149+
message.includes('ECONNRESET') ||
150+
message.includes('ETIMEDOUT') ||
151+
message.includes('EAI_AGAIN') ||
152+
message.includes('network timeout') ||
153+
message.includes('NetworkError') ||
154+
message.includes('fetch failed') ||
155+
message.includes('Invalid response body')
156+
157+
const shouldRetry = attempt < maxRetries && (isStatusRetriable || isNetworkRetriable)
158+
159+
if (shouldRetry) {
142160
attempt += 1
143-
console.warn(`Retrying after error ${status} (attempt ${attempt}/${maxRetries})...`)
144-
await new Promise((r) => setTimeout(r, delay))
145-
delay *= 2
161+
const jitter = Math.floor(Math.random() * 250)
162+
console.warn(`Retrying after error ${status || code || message} (attempt ${attempt}/${maxRetries})...`)
163+
await new Promise((r) => setTimeout(r, delay + jitter))
164+
delay = Math.min(delay * 2, maxDelayMs)
146165
continue
147166
}
148167
throw error
@@ -152,8 +171,10 @@ async function retryWithBackoff(fn, maxRetries = 5, initialDelayMs = 1000) {
152171

153172
function ghHeaders() {
154173
return {
155-
'Accept': 'application/vnd.github.v3+json',
156-
'Authorization': `token ${parsedGithubToken}`
174+
'Accept': 'application/vnd.github+json',
175+
'Authorization': `Bearer ${parsedGithubToken}`,
176+
'User-Agent': 'mesh-gov-stats-action/1.0 (+https://gov.meshjs.dev)',
177+
'X-GitHub-Api-Version': '2022-11-28'
157178
}
158179
}
159180

0 commit comments

Comments
 (0)