{"record":{"id":"e4e1a7ee9ddf66b0","repo":"chatboxai/chatbox","slug":"not-found-url","errorCode":null,"errorMessage":"Not found: ${url}","messagePattern":"Not found: (.+?)","errorType":"exception","errorClass":"GitHubApiError","httpStatus":404,"severity":"warning","filePath":"src/main/skills/github-fetcher.ts","lineNumber":87,"sourceCode":"    super(message)\n    this.name = 'GitHubApiError'\n  }\n}\n\nasync function githubFetch<T>(url: string): Promise<T> {\n  const cached = getCached<T>(url)\n  if (cached !== undefined) return cached\n\n  const response = await fetch(url, {\n    headers: {\n      'User-Agent': USER_AGENT,\n      Accept: 'application/vnd.github.v3+json',\n    },\n  })\n\n  if (!response.ok) {\n    if (response.status === 404) {\n      throw new GitHubApiError(`Not found: ${url}`, 404)\n    }\n    if (response.status === 403) {\n      throw new GitHubApiError('GitHub API rate limit exceeded. Try again later.', 403)\n    }\n    throw new GitHubApiError(`GitHub API error: ${response.status} ${response.statusText}`, response.status)\n  }\n\n  const data = (await response.json()) as T\n  setCache(url, data)\n  return data\n}\n\nasync function fetchRepoTree(owner: string, repo: string): Promise<GitHubTreeResponse | null> {\n  const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/git/trees/HEAD?recursive=1`\n  const result = await githubFetch<GitHubTreeResponse>(url)\n  if (!Array.isArray(result?.tree)) {\n    log.warn(`Unexpected tree response shape for ${owner}/${repo}`)\n    return null","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/skills/github-fetcher.ts#L69-L105","documentation":"GitHubApiError with status 404, thrown by githubFetch when the GitHub API responds 404 for the requested URL. It is distinct from generic API errors and rate-limit errors so callers can branch on 'missing resource' specifically. getCached is consulted first; the 404 is only set after a real network call.","triggerScenarios":"GET against a repo, tree, or content URL whose owner/repo/ref/path is wrong or inaccessible: typo in owner/repo, the repo is private and the request is unauthenticated, the branch/tag (HEAD ref) does not exist, or the file path inside the tree is wrong.","commonSituations":"Skill catalog pointing at a renamed/deleted repo; default branch changed from 'main' to something else while the fetch uses HEAD (usually fine) vs a hardcoded ref; private repo fetched without a token; case-sensitivity on owner/repo names.","solutions":["Verify owner/repo spelling and that the repo exists and is public (or provide a GITHUB token for private repos).","If using HEAD ref, confirm the repo has a default branch; if using an explicit ref, confirm it exists.","Branch on GitHubApiError status===404 to show 'skill repository not found' rather than a generic failure.","Cache negative results briefly to avoid hammering GitHub with the same bad URL."],"exampleFix":"// caller\ntry { await githubFetch(url) } catch (e) {\n  if (e instanceof GitHubApiError && e.status === 404) { showSkillNotFound(); return }\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"const repoExists = await checkRepoExists(owner, repo)\nif (!repoExists) return null","typeGuard":"function isGitHub404(e: unknown): e is GitHubApiError { return e instanceof GitHubApiError && e.status === 404 }","tryCatchPattern":"try { return await githubFetch(url) } catch (e) { if (isGitHub404(e)) { showSkillNotFound(url); return null } throw e }","preventionTips":["Verify owner/repo spelling and visibility before cataloging a skill repo.","Prefer HEAD ref over hardcoded branch names.","Provide a GITHUB token if any repos are private."],"tags":["skills","github-fetcher","http","typescript"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}