{"record":{"id":"b9247f855a85b68b","repo":"bmad-code-org/BMAD-METHOD","slug":"unexpected-response-from-url","errorCode":null,"errorMessage":"Unexpected response from ${url}","messagePattern":"Unexpected response from (.+?)","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"tools/installer/modules/channel-resolver.js","lineNumber":112,"sourceCode":"}\n\n/**\n * Fetch pure-semver tags (highest first) from a GitHub repo.\n * Cached per-process per owner/repo.\n *\n * @returns {Promise<Array<{tag: string, version: string}>>}\n *   tag is the original ref name (e.g. \"v1.7.0\"), version is the cleaned\n *   semver (e.g. \"1.7.0\").\n */\nasync function fetchStableTags(owner, repo, { timeout } = {}) {\n  const cacheKey = `${owner}/${repo}`;\n  if (tagCache.has(cacheKey)) return tagCache.get(cacheKey);\n\n  // GitHub returns up to 100 tags per page; one page is plenty for our modules.\n  const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/tags?per_page=100`;\n  const raw = await fetchJson(url, { timeout });\n  if (!Array.isArray(raw)) {\n    throw new TypeError(`Unexpected response from ${url}`);\n  }\n\n  const stable = [];\n  for (const entry of raw) {\n    const version = normalizeStableTag(entry?.name);\n    if (version) stable.push({ tag: entry.name, version });\n  }\n  stable.sort((a, b) => semver.rcompare(a.version, b.version));\n\n  tagCache.set(cacheKey, stable);\n  return stable;\n}\n\n/**\n * Resolve a channel plan for a single module into a git-clonable ref.\n *\n * @param {Object} args\n * @param {'stable'|'next'|'pinned'} args.channel","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/tools/installer/modules/channel-resolver.js#L94-L130","documentation":"Thrown (as TypeError) by fetchStableTags when the GitHub tags API response is not a JSON array. The resolver expects an array of tag objects; a non-array indicates the endpoint returned an unexpected shape (e.g. an error object, a rate-limit message, or an HTML page).","triggerScenarios":"fetchStableTags(owner, repo) calls fetchJson on the GitHub `/repos/{owner}/{repo}/tags` URL and the parsed body is not an Array. Typically a 2xx with a non-list body, or an upstream proxy returning JSON that is not a list.","commonSituations":"GitHub returns a rate-limit/secondary-rate-limit response with a 200 and a JSON object, a corporate proxy rewrites the response, or the API contract changes.","solutions":["Wait and retry if GitHub rate-limited the request; set GITHUB_TOKEN to raise limits.","If behind a proxy, ensure it forwards api.github.com responses unchanged.","Switch the module channel to 'next' or 'pinned' to avoid the tags API entirely."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isTagArray(raw) {\n  return Array.isArray(raw) && raw.every((e) => e && typeof e.name === 'string');\n}","tryCatchPattern":"try {\n  const tags = await fetchStableTags(owner, repo);\n} catch (error) {\n  if (error instanceof TypeError && error.message.startsWith('Unexpected response from')) {\n    // fall back to 'next' channel or retry after backoff\n  }\n  throw error;\n}","preventionTips":["Set GITHUB_TOKEN to reduce the chance of rate-limit-shaped responses.","Have callers fall back to the 'next' channel when stable tag fetch fails."],"tags":["network","github-api","channel-resolver"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}