Skip to content

Commit 85c59be

Browse files
committed
refactor: use shared fetch logic for github api
1 parent f64fd04 commit 85c59be

File tree

3 files changed

+75
-78
lines changed

3 files changed

+75
-78
lines changed

server/api/github/contributors-evolution/[owner]/[repo].get.ts

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { setTimeout } from 'node:timers/promises'
21
import { CACHE_MAX_AGE_ONE_DAY } from '#shared/utils/constants'
32

43
type GitHubContributorWeek = {
@@ -26,38 +25,13 @@ export default defineCachedEventHandler(
2625
}
2726

2827
const url = `https://api.github.com/repos/${owner}/${repo}/stats/contributors`
29-
const headers = {
30-
'User-Agent': 'npmx',
31-
'Accept': 'application/vnd.github+json',
32-
}
33-
34-
const maxAttempts = 6
35-
let delayMs = 1000
3628

3729
try {
38-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
39-
const response = await $fetch.raw<GitHubContributorStats[]>(url, { headers })
40-
const status = response.status
41-
42-
if (status === 200) {
43-
return Array.isArray(response._data) ? response._data : []
44-
}
45-
46-
if (status === 204) {
47-
return []
48-
}
49-
50-
if (status === 202) {
51-
if (attempt === maxAttempts - 1) return []
52-
await setTimeout(delayMs)
53-
delayMs = Math.min(delayMs * 2, 16_000)
54-
continue
55-
}
56-
57-
return []
58-
}
30+
const data = await fetchGitHubWithRetries<GitHubContributorStats[]>(url, {
31+
maxAttempts: 6,
32+
})
5933

60-
return []
34+
return Array.isArray(data) ? data : []
6135
} catch {
6236
return []
6337
}

server/api/github/issues/[owner]/[repo].get.ts

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
import { setTimeout } from 'node:timers/promises'
21
import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants'
32

4-
const GITHUB_HEADERS = {
5-
'Accept': 'application/vnd.github.v3+json',
6-
'User-Agent': 'npmx',
7-
'X-GitHub-Api-Version': '2022-11-28',
8-
} as const
9-
103
interface GitHubSearchResponse {
114
total_count: number
125
}
@@ -32,50 +25,23 @@ export default defineCachedEventHandler(
3225
const query = `repo:${owner}/${repo} is:issue is:open`
3326
const url = `https://api.github.com/search/issues?q=${encodeURIComponent(query)}&per_page=1`
3427

35-
const maxAttempts = 3
36-
let delayMs = 1000
37-
38-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
39-
try {
40-
const response = await $fetch.raw<GitHubSearchResponse>(url, {
41-
headers: GITHUB_HEADERS,
42-
timeout: 10000,
43-
})
44-
45-
if (response.status === 200) {
46-
return {
47-
owner,
48-
repo,
49-
issues:
50-
typeof response._data?.total_count === 'number' ? response._data.total_count : null,
51-
}
52-
}
53-
54-
if (response.status === 202) {
55-
if (attempt === maxAttempts - 1) break
56-
await setTimeout(delayMs)
57-
delayMs = Math.min(delayMs * 2, 16_000)
58-
continue
59-
}
28+
try {
29+
const data = await fetchGitHubWithRetries<GitHubSearchResponse>(url, {
30+
maxAttempts: 3,
31+
timeout: 10000,
32+
})
6033

61-
break
62-
} catch (error: any) {
63-
if (attempt === maxAttempts - 1) {
64-
throw createError({
65-
statusCode: error.response?.status || 500,
66-
statusMessage:
67-
error.response?._data?.message || 'Failed to fetch issue count from GitHub',
68-
})
69-
}
70-
await setTimeout(delayMs)
71-
delayMs = Math.min(delayMs * 2, 16_000)
34+
return {
35+
owner,
36+
repo,
37+
issues: typeof data?.total_count === 'number' ? data.total_count : null,
7238
}
39+
} catch {
40+
throw createError({
41+
statusCode: 500,
42+
statusMessage: 'Failed to fetch issue count from GitHub',
43+
})
7344
}
74-
75-
throw createError({
76-
statusCode: 500,
77-
statusMessage: 'Failed to fetch issue count from GitHub after retries',
78-
})
7945
},
8046
{
8147
maxAge: CACHE_MAX_AGE_ONE_HOUR,

server/utils/github.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { setTimeout } from 'node:timers/promises'
2+
import type { NitroFetchOptions } from 'nitropack'
3+
4+
export interface GitHubFetchOptions extends NitroFetchOptions<string> {
5+
maxAttempts?: number
6+
}
7+
8+
export async function fetchGitHubWithRetries<T>(
9+
url: string,
10+
options: GitHubFetchOptions = {},
11+
): Promise<T | null> {
12+
const { maxAttempts = 3, ...fetchOptions } = options
13+
let delayMs = 1000
14+
15+
const defaultHeaders = {
16+
'Accept': 'application/vnd.github+json',
17+
'User-Agent': 'npmx',
18+
'X-GitHub-Api-Version': '2026-03-10',
19+
}
20+
21+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
22+
try {
23+
const response = await $fetch.raw(url, {
24+
...fetchOptions,
25+
headers: {
26+
...defaultHeaders,
27+
...fetchOptions.headers,
28+
},
29+
})
30+
31+
if (response.status === 200) {
32+
return (response._data as T) ?? null
33+
}
34+
35+
if (response.status === 204) {
36+
return null
37+
}
38+
39+
if (response.status === 202) {
40+
if (attempt === maxAttempts - 1) break
41+
await setTimeout(delayMs)
42+
delayMs = Math.min(delayMs * 2, 16_000)
43+
continue
44+
}
45+
46+
break
47+
} catch (error: unknown) {
48+
if (attempt === maxAttempts - 1) {
49+
throw error
50+
}
51+
await setTimeout(delayMs)
52+
delayMs = Math.min(delayMs * 2, 16_000)
53+
}
54+
}
55+
56+
throw new Error(`Failed to fetch from GitHub after ${maxAttempts} attempts`)
57+
}

0 commit comments

Comments
 (0)