{"record":{"id":"6666fc492245efc1","repo":"666ghj/MiroFish","slug":"github-api-redirect-was-refused","errorCode":null,"errorMessage":"GitHub API redirect was refused","messagePattern":"GitHub API redirect was refused","errorType":"exception","errorClass":"FetchError","httpStatus":null,"severity":"error","filePath":"scripts/fetch_star_count.py","lineNumber":101,"sourceCode":"        },\n        method=\"GET\",\n    )\n    client = opener or _build_opener()\n    try:\n        response = client.open(request, timeout=TIMEOUT_SECONDS)\n    except urllib.error.HTTPError as exc:\n        status = exc.code\n        exc.close()\n        raise _status_error(status) from None\n    except (urllib.error.URLError, TimeoutError, OSError):\n        raise FetchError(\"GitHub API network request failed\") from None\n    except Exception:\n        raise FetchError(\"GitHub API request could not be started\") from None\n\n    try:\n        with response:\n            if response.geturl() != API_URL:\n                raise FetchError(\"GitHub API redirect was refused\")\n            status = response.getcode()\n            if status != 200:\n                raise _status_error(status)\n            payload = _read_response(response)\n    except FetchError:\n        raise\n    except (TimeoutError, OSError):\n        raise FetchError(\"GitHub API response could not be read\") from None\n    except Exception:\n        raise FetchError(\"GitHub API response could not be processed\") from None\n\n    try:\n        document = json.loads(payload)\n    except (UnicodeDecodeError, json.JSONDecodeError, ValueError):\n        raise FetchError(\"GitHub API returned malformed JSON\") from None\n    if not isinstance(document, dict):\n        raise FetchError(\"GitHub API response had an unexpected shape\")\n","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/fetch_star_count.py#L83-L119","documentation":"Security refusal, not a normal error path. The production opener installs NoRedirectHandler (redirect_request returns None), so a 3xx would surface as HTTPError instead; this geturl() != API_URL check (line 101) fires when a caller passes their own opener via fetch_star_count(token, opener=...) that followed a redirect — the final URL no longer matches https://api.github.com/repos/666ghj/MiroFish. The design goal (per the NoRedirectHandler docstring) is that the Bearer credential must never be forwarded to a different host.","triggerScenarios":"Calling fetch_star_count with a default urllib opener that follows redirects while GitHub answers 301/302/307 — typically because the repository was renamed/transferred, or api.github.com redirects to another canonical host.","commonSituations":"Repository 666ghj/MiroFish renamed or transferred so /repos/666ghj/MiroFish 301s to the new name; custom test openers that transparently follow redirects; GitHub sunsetting an API host.","solutions":["Update REPOSITORY/API_URL in scripts/fetch_star_count.py to the repository's new canonical full_name after a rename or transfer (check curl -sI https://api.github.com/repos/666ghj/MiroFish for the Location header).","If you pass a custom opener, build it with the same NoRedirectHandler so 3xx responses fail fast with a status error instead of silently following and then tripping this check.","Never 'fix' this by allowing the redirect — it exists to keep the Authorization header off any other host."],"exampleFix":"# before: caller-supplied opener follows redirects\nopener = urllib.request.build_opener()  # default follows 3xx\ncount = fetch_star_count(token, opener=opener)  # -> redirect was refused\n\n# after: reuse the script's no-redirect policy\nfrom scripts.fetch_star_count import _build_opener\ncount = fetch_star_count(token, opener=_build_opener())","handlingStrategy":"try-catch","validationCode":"import urllib.request\nfrom scripts.fetch_star_count import API_URL\n\n# resolve redirects WITHOUT credentials first; the credentialed call then never 3xx's\nreq = urllib.request.Request(API_URL, method=\"HEAD\")  # no Authorization header\nwith urllib.request.urlopen(req, timeout=10) as probe:\n    canonical = probe.geturl()\nif canonical != API_URL:\n    raise SystemExit(f\"repo moved; update API_URL to {canonical}\")","typeGuard":null,"tryCatchPattern":"try:\n    count = fetch_star_count(token)\nexcept FetchError as exc:\n    if exc.args[0] == \"GitHub API redirect was refused\":\n        # repository likely renamed: look up the new full_name unauthenticated\n        raise SystemExit(\n            \"api.github.com redirected; check if 666ghj/MiroFish was renamed \"\n            \"and update REPOSITORY in scripts/fetch_star_count.py\"\n        )\n    raise","preventionTips":["After renaming/transferring the repo, immediately update REPOSITORY in both scripts (fetch_star_count.py and star_history.py share it).","Always use the script's _build_opener() (no-redirect) when supplying a custom opener — never a default opener that forwards the Bearer token.","Monitor for 301s with a credential-free HEAD check in CI so a rename fails loudly outside the credentialed path."],"tags":["security","redirect","header-injection","repository-renamed","fetch-star-count"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}