Gridea Pro 版本
main 分支 HEAD (b84db26)
操作系统
macOS (Apple Silicon)
系统版本
Darwin 25.4.0(问题与平台无关)
优先级
P1(可靠性)
Bug 描述
检查所有基于 HTTP 的 provider / service:
backend/internal/deploy/vercel_deployer.go 的 uploadSingleFile / createDeployment
backend/internal/deploy/netlify_deployer.go 的 uploadSingleFile / createDeploy
backend/internal/service/cdn_upload_service.go 的 uploadToGitHub / getGithubFileSHA
这些函数在遇到 5xx、429 Too Many Requests、短暂的 io.EOF、connection reset 等典型可恢复错误时,全部直接 return err 一次挂掉。errgroup 一旦拿到非 nil 错误就会取消所有其它任务,整个部署直接失败。
典型场景:
- 移动网络切换 Wi-Fi 的一瞬间丢连接。
- Vercel / Netlify 瞬时限流返回 429。
- GitHub API 偶发 502 / 503(官方文档承认会有)。
- 大文件 PUT 超过
http.Client.Timeout(60s/120s)但文件其实只是慢。
从用户视角看:"我就是网络一闪,发布就全部重来",体验很差;而实际上对幂等写(PUT、带 x-vercel-digest 的文件上传)退避几秒再试一次几乎都能成功。
此外上传 SHA 为内容哈希天然幂等,完全可以安全重试;即使重复上传到 Vercel/Netlify/GitHub 也不会产生重复资源。
复现步骤
- 打开
Network Link Conditioner 或类似工具,制造 30% 丢包。
- 触发一次 Vercel 或 Netlify 部署(文件 ≥ 50 个)。
- 观察:大概率中途某次
uploadSingleFile 因 TLS handshake 失败或 5xx 而整盘失败。
- 再次手动发布一次:同一批文件可能有一部分已经在对端 cache,效率低。
期望行为
- 对幂等的 HTTP 方法(GET、PUT、DELETE)且错误属于可重试类(网络错误、5xx、429),自动退避重试(建议 3 次,指数 + jitter)。
- 429 要尊重
Retry-After header。
- 不幂等方法(POST 创建资源)默认不重试;只有在请求能附带 Idempotency-Key 的场景才例外。
- 重试过程中向前端推进度事件,告知"第 N 次重试中..."。
实际行为
无任何重试;一次失败即整盘失败。
截图 / 日志
无。
补充信息 — 最佳解决方案
封装一个通用 retry helper
backend/internal/deploy/httputil/retry.go:
type RetryPolicy struct {
MaxAttempts int // 默认 3
BaseDelay time.Duration // 默认 500ms
MaxDelay time.Duration // 默认 30s
RetryableStatus []int // 默认 408, 429, 500, 502, 503, 504
}
func DoWithRetry(ctx context.Context, client *http.Client, buildReq func() (*http.Request, error), policy RetryPolicy) (*http.Response, error) {
var lastErr error
for attempt := 0; attempt < policy.MaxAttempts; attempt++ {
req, err := buildReq()
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err == nil && !shouldRetry(resp.StatusCode, policy.RetryableStatus) {
return resp, nil
}
if err == nil { resp.Body.Close() }
lastErr = err
delay := backoff(attempt, policy, resp) // 指数 + jitter,429 用 Retry-After
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
return nil, fmt.Errorf(\"所有重试失败: %w\", lastErr)
}
关键点:
buildReq 是工厂函数,每次重试都要重新构建(尤其是 body 是 io.Reader 时 seek 不回去)。对文件上传,传入 filePath 并在工厂里 os.Open。
shouldRetry 网络错误(net.Error 且 Timeout() 或 Temporary()) + 白名单状态码。
- 429 若含
Retry-After 头,用其值;否则指数退避。
- 整体尊重
ctx.Done(),支持取消。
各调用点接入
vercel_deployer.go:uploadSingleFile 用 DoWithRetry(PUT 幂等 + 带哈希,天然安全)。createDeployment 仅在首次返回 5xx 时重试;missing 返回不是错误不能重试。
netlify_deployer.go:uploadSingleFile(PUT)同上;createDeploy(POST)默认不重试,除非服务端 5xx。
cdn_upload_service.go:uploadToGitHub(PUT)接入;getGithubFileSHA(GET)接入;429 严格按 Retry-After。
事件
每次重试通过 DeployEvent(参见 issue #43)上报:
{ stage: \"upload\", level: \"warn\", message: \"xxx 重试 1/3\" }。
单测
httputest.NewServer 构造 503→503→200 的场景,验证重试 3 次成功。
- 429 带
Retry-After: 2,验证延迟接近 2s。
- ctx 取消中途,验证立即返回 ctx.Err()。
影响文件:
- 新增
backend/internal/deploy/httputil/retry.go + 单测
backend/internal/deploy/{vercel,netlify}_deployer.go
backend/internal/service/cdn_upload_service.go
检查清单
Gridea Pro 版本
main 分支 HEAD (b84db26)
操作系统
macOS (Apple Silicon)
系统版本
Darwin 25.4.0(问题与平台无关)
优先级
P1(可靠性)
Bug 描述
检查所有基于 HTTP 的 provider / service:
backend/internal/deploy/vercel_deployer.go的uploadSingleFile/createDeploymentbackend/internal/deploy/netlify_deployer.go的uploadSingleFile/createDeploybackend/internal/service/cdn_upload_service.go的uploadToGitHub/getGithubFileSHA这些函数在遇到
5xx、429 Too Many Requests、短暂的io.EOF、connection reset等典型可恢复错误时,全部直接return err一次挂掉。errgroup 一旦拿到非 nil 错误就会取消所有其它任务,整个部署直接失败。典型场景:
http.Client.Timeout(60s/120s)但文件其实只是慢。从用户视角看:"我就是网络一闪,发布就全部重来",体验很差;而实际上对幂等写(PUT、带
x-vercel-digest的文件上传)退避几秒再试一次几乎都能成功。此外上传 SHA 为内容哈希天然幂等,完全可以安全重试;即使重复上传到 Vercel/Netlify/GitHub 也不会产生重复资源。
复现步骤
Network Link Conditioner或类似工具,制造 30% 丢包。uploadSingleFile因 TLS handshake 失败或 5xx 而整盘失败。期望行为
Retry-Afterheader。实际行为
无任何重试;一次失败即整盘失败。
截图 / 日志
无。
补充信息 — 最佳解决方案
封装一个通用 retry helper
backend/internal/deploy/httputil/retry.go:关键点:
buildReq是工厂函数,每次重试都要重新构建(尤其是 body 是io.Reader时 seek 不回去)。对文件上传,传入filePath并在工厂里os.Open。shouldRetry网络错误(net.Error且Timeout()或Temporary()) + 白名单状态码。Retry-After头,用其值;否则指数退避。ctx.Done(),支持取消。各调用点接入
vercel_deployer.go:uploadSingleFile用DoWithRetry(PUT 幂等 + 带哈希,天然安全)。createDeployment仅在首次返回 5xx 时重试;missing 返回不是错误不能重试。netlify_deployer.go:uploadSingleFile(PUT)同上;createDeploy(POST)默认不重试,除非服务端 5xx。cdn_upload_service.go:uploadToGitHub(PUT)接入;getGithubFileSHA(GET)接入;429 严格按Retry-After。事件
每次重试通过
DeployEvent(参见 issue #43)上报:{ stage: \"upload\", level: \"warn\", message: \"xxx 重试 1/3\" }。单测
httputest.NewServer构造 503→503→200 的场景,验证重试 3 次成功。Retry-After: 2,验证延迟接近 2s。影响文件:
backend/internal/deploy/httputil/retry.go+ 单测backend/internal/deploy/{vercel,netlify}_deployer.gobackend/internal/service/cdn_upload_service.go检查清单