{"record":{"id":"4fd7fdca8559c819","repo":"Budibase/budibase","slug":"error-err-resperr","errorCode":null,"errorMessage":"Error: ${err} - ${respErr}","messagePattern":"Error: (.+?) - (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/api/controllers/plugin/github.ts","lineNumber":31,"sourceCode":"    throw new Error(\"Invalid Github URL\")\n  }\n\n  if (parsed.protocol !== \"https:\" || parsed.hostname !== \"github.com\") {\n    throw new Error(\"The plugin origin must be from Github\")\n  }\n\n  return parsed\n}\n\nexport async function request(\n  url: string,\n  headers: Record<string, string> = {},\n  err: string\n) {\n  const response = await coreUtils.fetchWithBlacklist(url, { headers })\n  if (response.status >= 300) {\n    const respErr = await response.text()\n    throw new Error(`Error: ${err} - ${respErr}`)\n  }\n  return response.json()\n}\n\nexport async function githubUpload(url: string, name = \"\", token = \"\") {\n  let githubUrl = parseGithubUrl(url).toString()\n  let path: string | undefined\n\n  if (url.includes(\".git\")) {\n    githubUrl = url.replace(\".git\", \"\")\n  }\n\n  githubUrl = parseGithubUrl(githubUrl).toString()\n\n  const githubApiUrl = githubUrl.replace(\n    \"https://github.com/\",\n    \"https://api.github.com/repos/\"\n  )","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/plugin/github.ts#L13-L49","documentation":"request() wraps fetchWithBlacklist and treats any HTTP status >= 300 as a failure, re-throwing the upstream response body inside 'Error: <context> - <body>'. The context string (e.g. 'Repository not found') comes from the caller, so this error means the GitHub API call itself failed, not local validation.","triggerScenarios":"githubUpload calling the GitHub API repo endpoint or latest-release endpoint and receiving 3xx/4xx/5xx — most commonly 404 for a missing/private repo without a token, or 403 rate-limit from unauthenticated GitHub API usage (60 req/hour).","commonSituations":"Private repository without supplying a GITHUB token; typo in org/repo name; repository renamed or deleted; unauthenticated rate limiting on shared IPs; GitHub API downtime.","solutions":["Verify the repo exists by opening the URL in a browser and check the response body in the error for the real reason","Supply a GitHub personal access token for private repos or rate limits","Re-authenticate / regenerate an expired or revoked token","Retry later if the body indicates 403 rate limit or 5xx"],"exampleFix":"// before\nconst details = await githubUpload('https://github.com/org/private-plugin')\n// after\nconst details = await githubUpload('https://github.com/org/private-plugin', 'plugin', process.env.GITHUB_TOKEN)","handlingStrategy":"try-catch","validationCode":"const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`)\nif (res.status === 404) throw new Error('Repo not found or private')\nif (res.status === 403 && res.headers.get('x-ratelimit-remaining') === '0') throw new Error('GitHub rate limit exceeded')","typeGuard":null,"tryCatchPattern":"try {\n  await installPlugin({ source: 'GITHUB', url })\n} catch (err) {\n  if (/^Error: .* - /.test(err.message)) {\n    const body = err.message.split(' - ')[1]\n    if (body.includes('Not Found')) console.error('Repo missing or needs a token')\n    else if (body.includes('rate limit')) console.error('Retry later or add a token')\n  }\n}","preventionTips":["Supply a GITHUB token for private repos and to avoid 60 req/hour unauthenticated limits","Verify repo URL resolves before installing","Handle 403 rate-limit bodies by backing off","Check x-ratelimit-remaining before batch operations"],"tags":["network","http","github-api"],"backgroundTag":"upstream-http-error","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}