Skip to content

[Bug] Vercel missing 文件解析脆弱,二次上传无循环校验,部分大文件可能被吞 #48

@Tespera

Description

@Tespera

Gridea Pro 版本

main 分支 HEAD (b84db26)

操作系统

macOS (Apple Silicon)

系统版本

Darwin 25.4.0(问题与平台无关)

优先级

P1(Vercel 部署正确性)

Bug 描述

backend/internal/deploy/vercel_deployer.go:253-304createDeployment 实现:

if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
    var errResp struct {
        Error struct {
            Code    string   `json:\"code\"`
            Missing []string `json:\"missing\"`
        } `json:\"error\"`
    }
    if json.Unmarshal(bodyBytes, &errResp) == nil && len(errResp.Error.Missing) > 0 {
        return errResp.Error.Missing, nil
    }
    return nil, fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, string(bodyBytes))
}

return nil, nil

以及调用方 Deploy (71-142):

missing, err := p.createDeployment(ctx, projectName, fileResults, token)
...
if len(missing) > 0 {
    ... 上传 missing 集合 ...
    // 4. 重新创建部署(文件已上传完毕)
    if _, err := p.createDeployment(ctx, projectName, fileResults, token); err != nil {
        return fmt.Errorf(\"触发最终部署失败: %w\", err)
    }
}

问题细节

  1. missing 只从非 2xx 响应解析
    • Vercel v13 在不同场景下,有时会用 200 OK + body 里 id + missing 的形式返回(文档里也暗示了这种情况)。目前代码对 2xx 直接 return nil, nil,默认"没有缺失",但真实情况可能是"有一部分依然缺失"。
  2. 第二次 createDeployment 仅调用一次,假设 100% 成功
    • 如果第二次调用再度返回 missing(例如有文件上传时网络抖动,服务端没收全),代码不再重试;最终 deploy 就带着缺文件触发了——可能成功也可能半挂。
  3. 不能识别"部分上传成功但对端认为缺失"uploadFiles 里单个文件失败会让 errgroup 整组失败,但若服务端因 digest 不匹配认为"没收到",客户端并不会知道。
  4. projectSettings.framework 写死为 nil(258-263 行):用户若想以"Next.js 项目"部署,强制走 static 会出错。

复现步骤

  1. 对一个较大的站(几十 MB)发布 Vercel。
  2. 模拟部分文件上传超时 / 被服务端拒绝(比如文件名含特殊字符)。
  3. 观察:Deploy 最终可能假装成功("✅ Vercel 部署成功"),但访问站点时部分页面 404。

或更直接:

  1. 配合一个代理,把 1 个 /v2/files PUT 改成 502 响应。
  2. uploadSingleFile 返回错误,但因为 p.uploadFiles 里的 errgroup 已经接收了这条错误,其它上传也被取消。
  3. 最终整盘失败——但如果把 502 改成 200 但 body 清空,服务端 digest 校验失败,客户端就看不见问题。

期望行为

  • createDeployment 无论 2xx / 非 2xx,都尝试解析 missing 字段;有即返回,没有即 ok。
  • Deploy 改为循环最多 N 次(例如 3 次):每次基于上次返回的 missing 做差量上传,直到 missing 为空或达到上限。
  • 每轮之间失败的文件要能报告给用户。
  • framework 由设置决定,而非硬编码。

实际行为

解析脆弱、只重试一次、大文件 / 网络差的场景下可能静默漏文件。

截图 / 日志

无。

补充信息 — 最佳解决方案

重构 createDeploymentDeploy

type createDeployResp struct {
    Id      string   `json:\"id\"`
    Missing []string `json:\"missing\"`
}

func (p *VercelProvider) createDeployment(ctx context.Context, project string, files []VercelFileResult, token string, fw *string) (*createDeployResp, error) {
    ...
    // 无论 2xx/非 2xx,都解析 body
    var top struct {
        Id      string   `json:\"id\"`
        Missing []string `json:\"missing\"`
        Error   *struct {
            Code    string   `json:\"code\"`
            Missing []string `json:\"missing\"`
            Message string   `json:\"message\"`
        } `json:\"error\"`
    }
    _ = json.Unmarshal(bodyBytes, &top)

    if resp.StatusCode >= 200 && resp.StatusCode < 300 {
        return &createDeployResp{Id: top.Id, Missing: top.Missing}, nil
    }
    // 非 2xx,若有 missing 按特殊情况返回
    if top.Error != nil && len(top.Error.Missing) > 0 {
        return &createDeployResp{Missing: top.Error.Missing}, nil
    }
    // 真正的错误
    return nil, fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, string(bodyBytes))
}

Deploy 循环:

const maxRounds = 3
for round := 0; round < maxRounds; round++ {
    resp, err := p.createDeployment(ctx, projectName, fileResults, token, framework)
    if err != nil {
        return err
    }
    if len(resp.Missing) == 0 {
        break
    }
    logger(fmt.Sprintf(\" %d 需要上传 %d 个文件\", round+1, len(resp.Missing)))
    if err := p.uploadMissing(ctx, outputDir, fileResults, resp.Missing, token); err != nil {
        return err
    }
    if round == maxRounds-1 {
        return fmt.Errorf(\"Vercel 始终有 %d 个文件未能入库请重试\", len(resp.Missing))
    }
}

其它改动

单测

  • 伪造 API:第一次返回 { id:"", missing:[sha1,sha2] } → 第二次返回 { id:"dep_xx" } → 验证流程成功 + 仅上传 2 文件。
  • 连续 3 轮都返回 missing → 验证报错。
  • 2xx 响应里含 missing → 验证能识别。

影响文件:

  • backend/internal/deploy/vercel_deployer.go
  • backend/internal/domain/setting.go(framework 字段)
  • 新增单测 vercel_deployer_test.go

检查清单

  • 我已搜索过现有 Issue,确认此 Bug 尚未被报告过。
  • 我正在使用最新版本的 Gridea Pro。

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1中优先级 / 强烈建议处理bugSomething isn't workingtriage待分诊

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions