{"record":{"id":"0f08863081914c52","repo":"vercel/turborepo","slug":"github-auth-failed-http-res-status-check-git","errorCode":null,"errorMessage":"GitHub auth failed (HTTP ${res.status}). Check GITHUB_TOKEN/GH_TOKEN permissions.","messagePattern":"GitHub auth failed \\(HTTP (.+?)\\)\\. Check GITHUB_TOKEN/GH_TOKEN permissions\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/turbo-utils/src/examples.ts","lineNumber":178,"sourceCode":"  }\n}\n\nexport interface RepoInfo {\n  username: string;\n  name: string;\n  branch: string;\n  filePath: string;\n}\n\nexport async function isUrlOk(url: string): Promise<boolean> {\n  try {\n    const res = await fetchWithTimeout(url, { method: \"HEAD\" });\n    if (\n      !res.ok &&\n      getGitHubToken() &&\n      (res.status === 401 || res.status === 403)\n    ) {\n      warn(\n        `GitHub auth failed (HTTP ${res.status}). Check GITHUB_TOKEN/GH_TOKEN permissions.`\n      );\n    }\n    return res.ok;\n  } catch {\n    return false;\n  }\n}\n\nexport async function getRepoInfo(\n  url: URL,\n  examplePath?: string\n): Promise<RepoInfo | undefined> {\n  const [, username, name, tree, sourceBranch, ...file] = url.pathname.split(\n    \"/\"\n  ) as Array<string | undefined>;\n  const filePath = examplePath\n    ? examplePath.replace(/^\\//, \"\")","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/vercel/turborepo/blob/f9245100cf0d31d96628804ead485f6bf226e55a/packages/turbo-utils/src/examples.ts#L160-L196","documentation":"isUrlOk() sends a HEAD request (10s timeout) to check whether an example URL exists. When the response is not ok AND a GitHub token is configured AND the status is 401 or 403, it warns that the authenticated request was rejected — the token exists but GitHub refused it. The function still returns res.ok (false), so callers treat the example as unavailable; the warning tells you the failure was token-related, not a missing example.","triggerScenarios":"fetchWithTimeout(url, {method: 'HEAD'}) on a github.com/codeload.github.com URL returns HTTP 401 (bad/expired/revoked PAT) or 403 (classic PAT missing `repo` scope, fine-grained PAT missing `contents:read`, no access to a private/SAML-enforced org, or secondary rate limit) while getGitHubToken() returns a syntactically valid token.","commonSituations":"Expired or revoked classic PAT still exported in the shell; fine-grained PAT created without the `contents:read` permission; classic PAT with only `public_repo` scope used for a private repo; token issued by an org with SAML enforcement that was never SSO-authorized; clock-skewed or IP-rate-limited CI runners getting 403 from api.github.com.","solutions":["Check the token's validity and scopes directly: `curl -i -H \"Authorization: Bearer $GITHUB_TOKEN\" https://api.github.com/repos/vercel/turborepo` and inspect X-OAuth-Scopes / the response body.","For a fine-grained PAT, edit it at github.com/settings/personal-access-tokens and grant `contents:read` on the target repo/org.","For a classic PAT, ensure it has the `repo` scope (or at minimum `public_repo` for public repos); regenerate if expired.","If the org uses SAML SSO, click 'Configure SSO' on the token page and authorize it for the org.","If 403 comes with rate-limit headers (X-RateLimit-Remaining: 0), wait for the reset window or unexport the broken token so requests go out unauthenticated from CI with per-IP limits."],"exampleFix":"# before: fine-grained PAT without contents:read -> HTTP 403\ncurl -i -H \"Authorization: Bearer $GITHUB_TOKEN\" https://api.github.com/repos/vercel/turborepo\n# HTTP/2 403\n\n# after: re-create token with 'Contents: Read-only' permission\ncurl -i -H \"Authorization: Bearer $GITHUB_TOKEN\" https://api.github.com/repos/vercel/turborepo\n# HTTP/2 200","handlingStrategy":"validation","validationCode":"async function tokenWorks(token: string): Promise<boolean> {\n  const res = await fetch(\"https://api.github.com/repos/vercel/turborepo\", {\n    headers: { Authorization: `Bearer ${token}`, Accept: \"application/vnd.github+json\" },\n  });\n  return res.ok; // 200 = token valid with sufficient read access\n}\n// run before scaffolding:\nconst t = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;\nif (t && !(await tokenWorks(t))) throw new Error(\"GitHub token rejected; check scopes/expiry\");","typeGuard":null,"tryCatchPattern":"// isUrlOk never throws — it returns false on network errors and on 401/403.\n// Treat a false result as 'unavailable', and only surface auth advice when a token is set:\nconst ok = await isUrlOk(exampleUrl);\nif (!ok) {\n  const hasToken = !!(process.env.GITHUB_TOKEN || process.env.GH_TOKEN);\n  console.error(hasToken ? \"Example check failed — verify token scopes (repo / contents:read)\" : \"Example check failed — network or example name\");\n}","preventionTips":["Preflight the PAT with one authenticated curl/fetch to api.github.com before running create-turbo in CI.","Create fine-grained PATs with exactly `contents:read` on the needed repos; classic PATs with `repo` scope for private repos.","Set a calendar reminder before PAT expiry, or use org-level App/OIDC tokens in CI instead of long-lived PATs.","Remember 403 can also mean rate limiting — check X-RateLimit-Remaining headers before rotating a perfectly good token."],"tags":["github","authentication","http-403","http-401","api-rate-limit","permissions"],"backgroundTag":"github-api-auth-failure","analyzedSha":"f9245100cf0d31d96628804ead485f6bf226e55a","analyzedAt":"2026-08-17T10:46:15.696Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}