fix(github): auto-retry GitHub calls on 401 and expose unauthorized state

fetchLatestSha and queryFileContent were silently catching every error
(including expired-token 401s) and returning null, so the freshness
badge could stay on "offline" with no UI hint that re-auth was needed.
Wrap both calls in runWithAuthRetry to recover from refreshable tokens
transparently, and switch fetchLatestSha to a tagged result so the
freshness hook can distinguish auth failure from network failure.
This commit is contained in:
Julien Calixte
2026-05-17 21:10:20 +02:00
parent c412c75cfd
commit 151a4d9137
3 changed files with 44 additions and 23 deletions

View File

@@ -1,8 +1,14 @@
import { getOctokit } from "@/modules/repo/services/octo"
import { getOctokit, runWithAuthRetry } from "@/modules/repo/services/octo"
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
import { confirmMessage, errorMessage } from "@/utils/notif"
const isConflictStatus = (status: number) => status === 409 || status === 422
const isUnauthorizedStatus = (status: number | undefined) => status === 401
export type FetchShaResult =
| { kind: "ok"; sha: string | null }
| { kind: "unauthorized" }
| { kind: "offline" }
export const useGitHubContent = ({
user,
@@ -11,23 +17,23 @@ export const useGitHubContent = ({
user: string
repo: string
}) => {
const fetchLatestSha = async (path: string): Promise<string | null> => {
const fetchLatestSha = async (path: string): Promise<FetchShaResult> => {
try {
const octokit = await getOctokit()
const response = await octokit.request(
"GET /repos/{owner}/{repo}/contents/{+path}",
{
const response = await runWithAuthRetry((octokit) =>
octokit.request("GET /repos/{owner}/{repo}/contents/{+path}", {
owner: user,
repo,
path,
headers: { "X-GitHub-Api-Version": "2026-03-10" }
}
})
)
const data = response?.data
if (Array.isArray(data) || !data) return null
return "sha" in data ? data.sha : null
} catch {
return null
if (Array.isArray(data) || !data) return { kind: "ok", sha: null }
return { kind: "ok", sha: "sha" in data ? data.sha : null }
} catch (error) {
const status = (error as { status?: number })?.status
if (isUnauthorizedStatus(status)) return { kind: "unauthorized" }
return { kind: "offline" }
}
}