{"record":{"id":"b425a25bc0600550","repo":"different-ai/openwork","slug":"plugin-fetch-failed","errorCode":"plugin_fetch_failed","errorMessage":"Failed to fetch plugin data (${response.status}): ${text || url}","messagePattern":"Failed to fetch plugin data \\((.+?)\\): (.+?)","errorType":"http","errorClass":"ApiError","httpStatus":502,"severity":"error","filePath":"apps/server/src/claude-plugin-bundle.ts","lineNumber":107,"sourceCode":"  let dir: string | null = null;\n  let treeSegments: string[] | null = null;\n  if (parts[2] === \"tree\" && parts[3]) {\n    treeSegments = parts.slice(3);\n    ref = parts[3] ?? null;\n    const rest = parts.slice(4);\n    if (rest.length > 0) dir = rest.join(\"/\");\n  }\n  return { owner, repo, ref, dir, treeSegments };\n}\n\nasync function fetchGithubJson(url: string): Promise<unknown> {\n  const response = await externalFetch(url, {\n    headers: { Accept: \"application/vnd.github+json\", \"User-Agent\": \"openwork-server\" },\n    signal: AbortSignal.timeout(20_000),\n  });\n  if (!response.ok) {\n    const text = await response.text().catch(() => \"\");\n    throw new ApiError(502, \"plugin_fetch_failed\", `Failed to fetch plugin data (${response.status}): ${text || url}`);\n  }\n  return response.json();\n}\n\nasync function fetchGithubText(url: string): Promise<string> {\n  const response = await externalFetch(url, {\n    headers: { Accept: \"text/plain\", \"User-Agent\": \"openwork-server\" },\n    signal: AbortSignal.timeout(20_000),\n  });\n  if (!response.ok) {\n    const text = await response.text().catch(() => \"\");\n    throw new ApiError(502, \"plugin_fetch_failed\", `Failed to fetch plugin file (${response.status}): ${text || url}`);\n  }\n  return response.text();\n}\n\ntype TreeEntry = { path: string; sha: string };\n","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/claude-plugin-bundle.ts#L89-L125","documentation":"fetchGithubJson in apps/server/src/claude-plugin-bundle.ts:107 fetches GitHub JSON APIs (used by the 'tree' and 'info' flows) with a 20s timeout and GitHub JSON Accept headers. When the response is not ok, it reads the body text (best-effort) and throws ApiError 502 with code plugin_fetch_failed, embedding the GitHub status code and either the error body or the requested URL. It indicates GitHub (or the network path to it) failed upstream, not that the caller's input was malformed.","triggerScenarios":"GitHub API returns 403/429 (rate limit — anonymous unauthenticated requests are limited to 60/hour per IP), 404 (repo/ref/path does not exist), 5xx from GitHub, or a network failure producing a non-ok response during plugin tree/info resolution.","commonSituations":"Heavy plugin installs from one server IP exhausting the unauthenticated GitHub rate limit; a plugin pinned to a deleted repo or renamed default branch; private repos (unauthenticated fetch yields 404); transient GitHub incidents.","solutions":["Check the embedded status in the message: 403/429 → rate limited; add a GITHUB_TOKEN or wait for the rate-limit window to reset.","404 → verify the repo exists, is public, and the ref/branch/tag referenced actually exists.","5xx or network text → retry after a short delay; check GitHub status (githubstatus.com) and the server's outbound network/proxy.","If the request has an invalid pinned ref (e.g. moved branch), update the plugin source to a valid ref."],"exampleFix":"// before\nawait fetchGithubJson(\"https://api.github.com/repos/owner/repo/git/trees/main?recursive=1\"); // 403 rate-limited\n// after — authenticate to raise the rate limit\nawait fetchGithubJson(\"https://api.github.com/repos/owner/repo/git/trees/main?recursive=1\", {\n  headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` },\n});","handlingStrategy":"retry","validationCode":"// check inputs that map to GitHub API outcomes\nif (!/^owner\\/repo$/.test(shorthand)) throw new Error(\"invalid repo\");\n// optionally pre-check token/rate limit:\nconst rl = await fetch(\"https://api.github.com/rate_limit\", {\n  headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` },\n}).then((r) => r.json());\nif (rl.resources.core.remaining === 0) throw new Error(\"GitHub rate limit exhausted\");","typeGuard":null,"tryCatchPattern":"try {\n  const tree = await fetchGithubJson(treeUrl);\n} catch (err) {\n  if (err instanceof ApiError && err.code === \"plugin_fetch_failed\") {\n    if (err.message.includes(\"(403)\") || err.message.includes(\"(429)\")) {\n      // back off and retry with a GITHUB_TOKEN\n    } else if (err.message.includes(\"(404)\")) {\n      // repo/ref does not exist; correct the plugin source\n    } else {\n      // transient 5xx/network: retry with backoff\n    }\n  } else throw err;\n}","preventionTips":["Configure a GITHUB_TOKEN to avoid the 60 req/hr anonymous rate limit.","Pin plugin sources to tags or commit SHAs rather than mutable branches.","Verify the repo is public and the ref exists before installing.","Add exponential backoff for 5xx/network failures and monitor githubstatus.com."],"tags":["network","github-api","http-502","rate-limit","plugins"],"backgroundTag":"github-api-rate-limit","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}