{"record":{"id":"d947627dd7cbe2a4","repo":"chatboxai/chatbox","slug":"failed-to-fetch-file-filepath","errorCode":null,"errorMessage":"Failed to fetch file: ${filePath}","messagePattern":"Failed to fetch file: (.+?)","errorType":"exception","errorClass":"GitHubApiError","httpStatus":null,"severity":"error","filePath":"src/main/skills/github-fetcher.ts","lineNumber":324,"sourceCode":"  }\n\n  return detected\n}\n\nexport async function fetchFileContent(owner: string, repo: string, filePath: string): Promise<string> {\n  // Git allows `#`/`?` in filenames — encode per segment so they don't truncate the URL\n  const encodedPath = filePath.split('/').map(encodeURIComponent).join('/')\n  const url = `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${encodedPath}`\n\n  const cached = getCached<string>(url)\n  if (cached !== undefined) return cached\n\n  const response = await fetch(url, {\n    headers: { 'User-Agent': USER_AGENT },\n  })\n\n  if (!response.ok) {\n    throw new GitHubApiError(`Failed to fetch file: ${filePath}`, response.status)\n  }\n\n  const content = await response.text()\n  setCache(url, content)\n  return content\n}\n\nexport async function downloadSkillFiles(\n  owner: string,\n  repo: string,\n  skillPath: string,\n  targetDir: string\n): Promise<void> {\n  try {\n    const downloaded = await downloadSkillFilesViaTree(owner, repo, skillPath, targetDir)\n    if (downloaded) return\n  } catch (error) {\n    if (error instanceof GitHubApiError && (error.statusCode === 403 || error.statusCode === 429)) {","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/skills/github-fetcher.ts#L306-L342","documentation":"Thrown by fetchFileContent() when raw.githubusercontent.com returns a non-OK status for a single file. Unlike githubFetch (which special-cases 404 and 403), this raw-URL path has no status-specific branches — any failure becomes a GitHubApiError with the original status code attached. The function powers SKILL.md reads during detection and individual file downloads.","triggerScenarios":"Requesting raw.githubusercontent.com/{owner}/{repo}/HEAD/{encodedPath} where the path does not exist at HEAD (404), the repo is private (404), the file is git-LFS tracked (pointer served, or 404 on media), or raw CDN returns 5xx. Also triggered when filePath contains characters that encodeURIComponent mishandles for the raw endpoint.","commonSituations":"A SKILL.md listed in the tree was deleted before the content fetch (HEAD advanced); private repo accessed without a token; LFS-backed file; transient raw CDN error. The per-segment encodeURIComponent handles # and ? in filenames, but a leading slash or unusual encoding can still produce a 404.","solutions":["Verify the file exists at the repo's default branch by opening the raw URL in a browser.","If the repo is private, supply authentication — raw URLs need a token in the URL or use the contents API with an Authorization header.","Retry once after a short delay for transient CDN 5xx; clearCache() is not needed since this URL was never cached on failure.","For LFS files, fetch via the contents/blobs API instead of raw."],"exampleFix":"// before\nif (!response.ok) {\n  throw new GitHubApiError(`Failed to fetch file: ${filePath}`, response.status)\n}\n\n// after — surface the status so callers can distinguish 404 (gone) from 5xx (retry)\nif (!response.ok) {\n  throw new GitHubApiError(\n    `Failed to fetch file: ${filePath} (HTTP ${response.status} ${response.statusText})`,\n    response.status\n  )\n}","handlingStrategy":"retry","validationCode":"// Validate the file path shape before fetching.\nfunction isValidFilePath(p: string): boolean {\n  return typeof p === 'string' && p.length > 0 && p.length < 4096 && !p.includes('\\\\')\n}\nif (!isValidFilePath(filePath)) throw new Error('Invalid file path')","typeGuard":"function isGitHubApiError(e: unknown): e is { statusCode: number; message: string } {\n  return e instanceof Error && typeof (e as any).statusCode === 'number'\n}","tryCatchPattern":"try {\n  return await fetchFileContent(owner, repo, filePath)\n} catch (error) {\n  if (isGitHubApiError(error) && (error.statusCode === 404)) return null // file gone\n  if (isGitHubApiError(error) && error.statusCode >= 500) {\n    return await backoffRetry(() => fetchFileContent(owner, repo, filePath))\n  }\n  throw error\n}","preventionTips":["Expect 404 from raw URLs when HEAD has moved — do not cache failure, just return null.","Use the contents/blobs API for private repos or LFS files instead of raw.githubusercontent.com.","Encode each path segment with encodeURIComponent to handle # and ? in filenames."],"tags":["github-api","network","skills","raw-content","http"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}