Skip to content

fix(czdev): login 失败时给出真实原因,而不是 JSONDecodeError - #18

Merged
eggfly merged 1 commit into
mainfrom
fix/czdev-login-diagnostics
Aug 5, 2026
Merged

fix(czdev): login 失败时给出真实原因,而不是 JSONDecodeError#18
eggfly merged 1 commit into
mainfrom
fix/czdev-login-diagnostics

Conversation

@eggfly

@eggfly eggfly commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

现象

同事跑 czdev login,拿到验证码后崩在轮询上:

File ".../czdev/auth.py", line 91, in login
    token_resp = json.loads(resp.read().decode())
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

根因说明

这个 PR 不声称修掉了他那次的根因——根因目前未知。 从 traceback 只能确定两件事:urlopen 没抛 HTTPError,所以 HTTP 状态是 2xx;响应体第 0 个字符不是合法 JSON 开头,即 body 为空或是 HTML/纯文本。

已复现正常路径:未授权时 GitHub 返回 200 + application/json + authorization_pending,协议用法本身是对的。

可以排除「中间设备剥掉 Accept 头导致 GitHub 回落 form-urlencoded」:几秒前同机同 host 同请求头的 /login/device/code 解析成功了。

剩下的怀疑是 github.com 的限流页/代理拦截页/重定向到 HTML 页(这些端点在 github.com 而非 api.github.com,限流返回 HTML;urllib 还会静默跟随重定向)。但这是推测。

所以这个 PR 修的是真正确定存在的 bug:轮询处零错误处理,把真实原因彻底吞掉了。修完之后他重跑一次,报错里就会直接写明状态码、content-type、最终 URL 和 body 片段。

改动

  • post_form() 统一处理两个 OAuth 端点:解析失败时抛 AuthError,带上 状态码 / content-type / 重定向后的最终 URL / body 片段;顺带读取 4xx 的 body(GitHub 在那里也返回 JSON 错误);并接受 form-urlencoded 作为回退(万一 Accept 头真被剥,登录会直接成功而不是报错)。
  • 轮询循环:瞬时失败重试而不是直接崩(用户可能正在浏览器里授权),第一次失败就打印诊断,连续 MAX_SOFT_FAILURES=5 次才放弃并给出限流/代理提示。
  • 另外顺手补齐:遵守 expires_in 不再可能无限循环;slow_down 按规范永久增大 interval(原来只 sleep 5 秒,会一直被 slow_down);处理 access_denied;加请求超时;补 User-Agent(原来发的是默认 Python-urllib/3.10,github.com 的 web 端点对这种 UA 更容易触发防护);Ctrl-C 输出 Cancelled. 并 exit 130,不再吐 KeyboardInterrupt traceback。

验证

用本地假服务器覆盖了全部响应形态:HTML 页 / 空 body / form-urlencoded / 4xx+JSON / 纯文本 / 正常 token,以及轮询的重试自愈、重试耗尽放弃、slow_downaccess_denied。真实跑了一次 czdev login:正常拿到 device code、静默轮询、Ctrl-C 干净退出 130 无 traceback。

Made with Cursor

…deError

`czdev login` polled the token endpoint and fed the body straight to
json.loads() with no error handling, so any non-JSON reply surfaced as a bare
`JSONDecodeError: Expecting value: line 1 column 1 (char 0)` traceback that
hid the actual cause. These endpoints are on github.com, not api.github.com,
so a rate-limit page, a proxy/captive-portal interception, or a redirect to an
HTML page all arrive as HTTP 200 with HTML — and urllib follows redirects
silently, so the landing URL was invisible too.

post_form() now decodes the reply defensively and raises an AuthError naming
the status, content-type, post-redirect URL and a body snippet. It also reads
the body of 4xx responses (GitHub returns JSON errors there) and accepts the
endpoint's default form-urlencoded encoding, which is what we'd get if
anything stripped the Accept header.

The poll loop retries transient failures instead of aborting a login the user
may already be authorizing, prints the diagnostic on the first failure, and
gives up after MAX_SOFT_FAILURES with a hint. Also: honor expires_in so it
can't spin forever, grow the interval on slow_down as the spec requires,
handle access_denied, add request timeouts, send a real User-Agent (github.com
is more likely to throttle the default Python-urllib one), and turn Ctrl-C
into "Cancelled." rather than a KeyboardInterrupt traceback.

Co-authored-by: Cursor <cursoragent@cursor.com>
@eggfly
eggfly merged commit cfdaaf0 into main Aug 5, 2026
3 checks passed
@eggfly
eggfly deleted the fix/czdev-login-diagnostics branch August 5, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant