Skip to content

fix(czdev): 网络截断时重试,不再直接 traceback - #19

Closed
eggfly wants to merge 1 commit into
mainfrom
fix/czdev-retry-truncated-responses
Closed

fix(czdev): 网络截断时重试,不再直接 traceback#19
eggfly wants to merge 1 commit into
mainfrom
fix/czdev-retry-truncated-responses

Conversation

@eggfly

@eggfly eggfly commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

起因

同事跑 czdev publish 传一个 72.6 MB 的 deb,预检全过之后崩在这里:

File "scripts/czdev/github_client.py", line 50, in _request
    return json.loads(resp.read().decode())
http.client.IncompleteRead: IncompleteRead(4813 bytes read, 1558 more expected)

这是 GET /repos/CardputerZero/packages。响应头声明 6371 字节,连接在读到 4813 字节时断了。同一个接口我这边实测返回 content-length: 6286,稳定正常,所以是他那条链路(代理 / VPN / 网关)把连接掐了。

问题在于 _request() 每个调用都是一次性、无防护的尝试,任何抖动都会变成一个看不出所以然的 Python traceback。他上午那次 czdev loginJSONDecodeError 是同一类问题,已经在 #18 修过 auth.py;这个 PR 把 github_client.py 也补齐。

改了什么

  • 所有请求统一走 _fetch():小心地读完 body,对截断读取、socket 错误、以及 GitHub 明示可重试的状态码(429、5xx,遵守 Retry-After)做指数退避重试。
  • 只有 GET 会在网络失败后重放。 POST 如果是读响应时断的,服务端可能已经执行了,重发会多出一个 commit / ref / PR,所以不重放,直接报错。
  • 上传 asset 单独一套重试。 它是慢网络下最容易断的一步(72 MB 的 deb),而且只要先把半截的 asset 删掉就能安全重传——现在每一轮都会重新拉一次 release 并清理同名残留,避免 422 already_exists。上传超时也从"无"改成独立的 1 小时预算,不再和普通 API 调用共用。
  • 返回的根本不是 JSON 时(HTML 限流页、门户拦截页、空 body),抛 GitHubError,带上状态码、content-type 和 body 前 200 字;main() 捕获后打印成一行普通错误,不再吐 traceback。Ctrl-C 也一样。
  • 保留 HTTPError 语义:check_permission 靠 404/403、ensure_release 靠 404 分支判断,这些行为不变,只是错误信息里现在带上了响应 body。

验证

truncated -> connection closed after 4813 of 6371 bytes
check_permission after 2 truncations -> 3 (attempts=3)     # 正是他崩的那个调用,现在能自愈
POST not replayed -> POST .../git/refs failed: closed mid-response
GET exhausted -> GET .../user failed 4 times: ... A proxy or VPN cutting connections to GitHub looks exactly like this
HTML body -> HTTP 200 from .../user returned text/html instead of JSON. Body: <html>go away</html>
real get_user -> eggfly
real perm on packages -> 3
404 repo -> perm 0
missing file -> file not found: definitely/missing.json
get_file_content -> 3976 bytes
upload: attempts 2, timeout 3600s, 删除了 [7, 99](已存在的 + 上次失败残留的)

需要说明的

重试只能扛住抖动。如果他那条链路是稳定地截断,重试 4 次一样会失败,只不过现在的报错会直接说清是网络断了。让他先跑这个确认(正常应该每次都是 200 6286B):

TOKEN=$(python3 -c 'import json,pathlib;print(json.loads((pathlib.Path.home()/".czdev/credentials").read_text())["github_token"])')
for i in 1 2 3 4 5; do
  curl -sS -w '%{http_code} %{size_download}B %{time_total}s\n' -o /dev/null \
    -H "Authorization: Bearer $TOKEN" -H 'Accept: application/vnd.github+json' \
    -H 'User-Agent: czdev/0.1' https://api.github.com/repos/CardputerZero/packages
done
env | grep -i proxy

Made with Cursor

A publish died with `http.client.IncompleteRead(4813 bytes read, 1558
more expected)` from inside `json.loads(resp.read().decode())`: the
connection to api.github.com was cut three quarters of the way through a
6 KB response, which a proxy or a flaky uplink does routinely. Every API
call was a single unguarded attempt, so any hiccup surfaced as a Python
traceback with no hint of what went wrong.

Route every call through `_fetch()`, which reads the body defensively and
retries with exponential backoff on truncated reads, socket errors and the
statuses GitHub uses to say "not my final answer" (429, 5xx, honouring
Retry-After). Only GETs are replayed after a network failure -- a POST
that died mid-response may still have been applied, and re-sending it
could create a second commit, ref or pull request.

Asset uploads get their own retry loop: they are the most likely thing to
break on a slow link (a 72 MB .deb over the API's timeout), and they can
be replayed safely as long as a half-uploaded asset is deleted first,
which now happens on every round.

Replies that are not JSON at all -- an HTML rate-limit page, a captive
portal, an empty body -- now raise GitHubError carrying the status,
content type and a body snippet, and `main()` prints it as a plain error
instead of a traceback. Mirrors what #18 did for the device-flow login.

Co-authored-by: Cursor <cursoragent@cursor.com>
@eggfly eggfly closed this Aug 5, 2026
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