{"record":{"id":"821d796ccb729138","repo":"infiniflow/ragflow","slug":"failed-to-fetch-github-user-info-e","errorCode":null,"errorMessage":"Failed to fetch github user info: {e}","messagePattern":"Failed to fetch github user info: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"api/apps/auth/github.py","lineNumber":52,"sourceCode":"        super().__init__(config)\n\n    def fetch_user_info(self, access_token, **kwargs):\n        \"\"\"\n        Fetch GitHub user info (synchronous).\n        \"\"\"\n        user_info = {}\n        try:\n            headers = {\"Authorization\": f\"Bearer {access_token}\"}\n            response = sync_request(\"GET\", self.userinfo_url, headers=headers, timeout=self.http_request_timeout)\n            response.raise_for_status()\n            user_info.update(response.json())\n            email_response = sync_request(\"GET\", self.userinfo_url + \"/emails\", headers=headers, timeout=self.http_request_timeout)\n            email_response.raise_for_status()\n            email_info = email_response.json()\n            user_info[\"email\"] = next((email for email in email_info if email[\"primary\"]), None)[\"email\"]\n            return self.normalize_user_info(user_info)\n        except Exception as e:\n            raise ValueError(f\"Failed to fetch github user info: {e}\")\n\n    async def async_fetch_user_info(self, access_token, **kwargs):\n        \"\"\"Async variant of fetch_user_info using httpx.\"\"\"\n        user_info = {}\n        headers = {\"Authorization\": f\"Bearer {access_token}\"}\n        try:\n            response = await async_request(\n                \"GET\",\n                self.userinfo_url,\n                headers=headers,\n                timeout=self.http_request_timeout,\n            )\n            response.raise_for_status()\n            user_info.update(response.json())\n\n            email_response = await async_request(\n                \"GET\",\n                self.userinfo_url + \"/emails\",","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/api/apps/auth/github.py#L34-L70","documentation":"GithubOAuthClient.fetch_user_info calls GET /user and GET /user/emails with the access token and wraps any failure in ValueError('Failed to fetch github user info: {e}') (api/apps/auth/github.py:52). The except is broad: it fires on HTTP errors (401 bad token, 403 scope missing), network/timeouts, JSON decode errors, and a subtle TypeError when the user has no 'primary' email - next(...) returns None and ['email'] raises inside the try.","triggerScenarios":"Access token expired or revoked (401 on /user); token lacks the 'user:email' scope so /user/emails returns [] or 403; GitHub Enterprise userinfo_url misconfigured (wrong github_api_base); user has no primary/verified email on file; transient network failure or timeout.","commonSituations":"OAuth app created without requesting user:email scope; self-hosted GitHub Enterprise with an API base URL missing /api/v3; users with private emails and empty email list; long-running sessions with expired tokens.","solutions":["Ensure the GitHub OAuth app requests the 'user:email' scope and re-authorize so the token carries it.","Test the token directly: curl -H 'Authorization: Bearer <token>' https://api.github.com/user/emails must return a list containing an entry with \"primary\": true.","For GitHub Enterprise, verify github_api_base/host config so userinfo_url resolves to <host>/api/v3/user.","Handle users with no primary email (corporate SAML-only accounts) by falling back to login-based identity or requiring an email."],"exampleFix":"# verify token + scope\ncurl -H 'Authorization: Bearer $TOKEN' https://api.github.com/user/emails\n# expect: [{\"email\": \"...\", \"primary\": true, ...}]","handlingStrategy":"try-catch","validationCode":"import requests\n\ndef github_token_ok(token, base=\"https://api.github.com\"):\n    h = {\"Authorization\": f\"Bearer {token}\"}\n    u = requests.get(f\"{base}/user\", headers=h, timeout=10)\n    e = requests.get(f\"{base}/user/emails\", headers=h, timeout=10)\n    emails = e.json() if e.ok else []\n    return u.ok and e.ok and any(x.get(\"primary\") for x in emails)","typeGuard":null,"tryCatchPattern":"try:\n    info = client.fetch_user_info(access_token)\nexcept ValueError as e:\n    msg = str(e)\n    if \"401\" in msg:\n        restart_oauth_flow()          # token expired - re-authenticate\n    elif \"NoneType\" in msg or \"primary\" in msg:\n        raise UserVisibleError(\"Your GitHub account has no primary email; add one and retry\")\n    else:\n        retry_or_alert(msg)","preventionTips":["Request the user:email scope in the GitHub OAuth app from day one.","Handle users without a primary email gracefully (fall back to login).","For GitHub Enterprise, smoke-test /api/v3/user reachability from the server.","Keep access tokens short-lived and re-exchange rather than reusing stale ones."],"tags":["auth","github","oauth","network","http"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}