vercel/turborepo · warning
GitHub auth failed (HTTP ${res.status}). Check GITHUB_TOKEN/
Error message
GitHub auth failed (HTTP ${res.status}). Check GITHUB_TOKEN/GH_TOKEN permissions. What it means
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.
Source
Thrown at packages/turbo-utils/src/examples.ts:178
}
}
export interface RepoInfo {
username: string;
name: string;
branch: string;
filePath: string;
}
export async function isUrlOk(url: string): Promise<boolean> {
try {
const res = await fetchWithTimeout(url, { method: "HEAD" });
if (
!res.ok &&
getGitHubToken() &&
(res.status === 401 || res.status === 403)
) {
warn(
`GitHub auth failed (HTTP ${res.status}). Check GITHUB_TOKEN/GH_TOKEN permissions.`
);
}
return res.ok;
} catch {
return false;
}
}
export async function getRepoInfo(
url: URL,
examplePath?: string
): Promise<RepoInfo | undefined> {
const [, username, name, tree, sourceBranch, ...file] = url.pathname.split(
"/"
) as Array<string | undefined>;
const filePath = examplePath
? examplePath.replace(/^\//, "")View on GitHub (pinned to f9245100cf)
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.
Example fix
# before: fine-grained PAT without contents:read -> HTTP 403 curl -i -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/vercel/turborepo # HTTP/2 403 # after: re-create token with 'Contents: Read-only' permission curl -i -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/vercel/turborepo # HTTP/2 200
Defensive patterns
Strategy: validation
Validate before calling
async function tokenWorks(token: string): Promise<boolean> {
const res = await fetch("https://api.github.com/repos/vercel/turborepo", {
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
});
return res.ok; // 200 = token valid with sufficient read access
}
// run before scaffolding:
const t = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
if (t && !(await tokenWorks(t))) throw new Error("GitHub token rejected; check scopes/expiry"); Try / catch
// isUrlOk never throws — it returns false on network errors and on 401/403.
// Treat a false result as 'unavailable', and only surface auth advice when a token is set:
const ok = await isUrlOk(exampleUrl);
if (!ok) {
const hasToken = !!(process.env.GITHUB_TOKEN || process.env.GH_TOKEN);
console.error(hasToken ? "Example check failed — verify token scopes (repo / contents:read)" : "Example check failed — network or example name");
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- GITHUB_TOKEN/GH_TOKEN contains invalid characters, ignoring.
- Unable to write .gitignore
- Unable to write package.json
- Unable to update README.md
- Failed to download: ${response.status}
AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17).
Data as JSON: /api/errors/0f08863081914c52.
Report an issue: GitHub.