(
url: string,
opts: FetchOptions = {}
)
| 113 | * gives up produces a partial diff that looks like real data loss. |
| 114 | */ |
| 115 | export async function fetchJSON<T = any>( |
| 116 | url: string, |
| 117 | opts: FetchOptions = {} |
| 118 | ): Promise<T | null> { |
| 119 | const cacheFile = join( |
| 120 | CACHE_DIR, |
| 121 | createHash("sha256").update(url).digest("hex").slice(0, 32) + ".json" |
| 122 | ); |
| 123 | |
| 124 | if (!opts.noCache && existsSync(cacheFile)) { |
| 125 | try { |
| 126 | const cached = JSON.parse(readFileSync(cacheFile, "utf-8")); |
| 127 | if (Date.now() - cached.at < DEFAULT_TTL_MS) return cached.body as T; |
| 128 | } catch { |
| 129 | // A corrupt cache entry is never worth failing over: refetch. |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | const headers: Record<string, string> = { |
| 134 | accept: "application/json", |
| 135 | "user-agent": "stack-hub-maintenance (+https://stack.lol)", |
| 136 | }; |
| 137 | if (url.startsWith("https://api.github.com/")) { |
| 138 | const token = resolveGithubToken(); |
| 139 | if (token) headers.authorization = `Bearer ${token}`; |
| 140 | headers.accept = "application/vnd.github+json"; |
| 141 | headers["x-github-api-version"] = "2022-11-28"; |
| 142 | } |
| 143 | |
| 144 | let lastError = ""; |
| 145 | for (let attempt = 0; attempt < 5; attempt++) { |
| 146 | let res: Response; |
| 147 | try { |
| 148 | res = await fetch(url, { headers }); |
| 149 | } catch (e) { |
| 150 | lastError = String(e); |
| 151 | await sleep(1000 * 2 ** attempt); |
| 152 | continue; |
| 153 | } |
| 154 | |
| 155 | if (res.ok) { |
| 156 | const body = (await res.json()) as T; |
| 157 | mkdirSync(CACHE_DIR, { recursive: true }); |
| 158 | writeFileSync(cacheFile, JSON.stringify({ at: Date.now(), url, body })); |
| 159 | return body; |
| 160 | } |
| 161 | |
| 162 | // 422 is GitHub's answer to "that ref does not exist" on commit lookups, |
| 163 | // and 451 is a DMCA takedown: both are answers, not failures. |
| 164 | if (res.status === 404 || res.status === 422 || res.status === 451) { |
| 165 | if (opts.allow404) return null; |
| 166 | throw new HttpError(res.status, url, `${res.status} on ${url}`); |
| 167 | } |
| 168 | |
| 169 | // Primary (x-ratelimit-remaining: 0) and secondary (retry-after) limits. |
| 170 | const retryAfter = Number(res.headers.get("retry-after") ?? 0); |
| 171 | const reset = Number(res.headers.get("x-ratelimit-reset") ?? 0); |
| 172 | const remaining = res.headers.get("x-ratelimit-remaining"); |
nothing calls this directly
no test coverage detected