现象
v0.23.0(PR #782)引入的文件页「压缩并下载」在真实链路上必然失败:archive.build 成功后,轮询任务对 archive.status 的第一次调用(250ms 内)就被宿主以 400 拒绝:
missing or invalid "sessionId"
错误条随即显示 zipFailed,state === 'ready' 永远到不了,下载(save)不会发生——整条链只有 build 这一步是成功的。
根因:客户端载荷缺 sessionId,宿主要求两个参数
客户端 src/client/api.ts:470-472 只发 { id }:
export function archiveStatus(id: string): Promise<ArchiveBuildStatus> {
return call<ArchiveBuildStatus>('archive.status', { id })
}
宿主 src/index.ts:601-602 两个参数都 requireString:
'archive.status': (payload) =>
archiveTasks.status(requireString(payload, 'id'), requireString(payload, 'sessionId')),
requireString(src/wire.ts:95-102)对缺失键抛 bad-request。
对照同族两个入口,这里明显是漏带:
archive.build(api.ts:453)用 scopePayload(scope, …) 携带 sessionId;
archiveDownloadUrl(api.ts:477-479)的 GET 也带 sessionId(注释:the host answers only the session that built it)。
而 call() 本身是裸 fetch,不会自动附加 sessionId——文件头注释「Every call posts … with the sessionId」实际由各调用方负责放进去。
触发路径
文件树多选(或单目录)右键「压缩并下载」→ build 成功 → 轮询任务(src/client/FileTree.tsx:1378)await archiveStatus(job.id) → 400 → catch → failArchive(...)(FileTree.tsx:1393-1395)。100% 复现。
为什么测试全绿
tests/zip.spec.ts 的 status 用例全部直发 { id, sessionId }(如 :590)——测的是宿主侧,不是客户端实际载荷;
tests/file-tree-archive.spec.tsx:34 把 api.archiveStatus 整个 mock 掉了。
客户端实际发出的载荷形状没有任何测试覆盖。
修复方向
archiveStatus 改收 scope 并走 scopePayload(与 build / download URL 一致);宿主语义不变——sessionId 本来就是任务归属校验。顺带补一个「客户端真实载荷」形状的测试即可拦住回归。
顺带:本条修复后会显形的第二个问题(建议同批处理)
FileTree.tsx:1376-1397:轮询看到 state === 'ready' 后调 save(job.id, job.name)——saveArchive(:1340)是 void、不 await、也不先 settleArchive()(:1326),archiveJobId 保持非 null,250ms 轮询继续跑。而下载路由是消费式的:src/archive-route.ts:267 "A download consumes the task" + tasks.release,首次 GET 服务后任务即删。当首个 GET 往返超过一个轮询间隔(大包 / SSH 远端——正是本插件支持的形态)时:
- 首个 GET 未完成 → 第二跳
status 仍 ready → 并发第二个 save(双下载,anchor.click 两次);
- 首个 GET 已服务 → 第二跳
status 404 → failArchive('unknown or expired archive'),而 GET 成功回调又会 setActionError(null),两个回调落点顺序不定——常见终态是文件下载成功 + 错误条 zipFailed 并存。
本机小文件(<250ms)侥幸干净。建议:轮询看到 ready 后先 settleArchive() 停轮询(或在 save 前置重入护栏),再做恰好一次下载。
环境:v0.24.1(492661a)。v0.22.0 无此代码(整条 archive 链为 #782 新增)。
现象
v0.23.0(PR #782)引入的文件页「压缩并下载」在真实链路上必然失败:
archive.build成功后,轮询任务对archive.status的第一次调用(250ms 内)就被宿主以 400 拒绝:错误条随即显示 zipFailed,
state === 'ready'永远到不了,下载(save)不会发生——整条链只有 build 这一步是成功的。根因:客户端载荷缺
sessionId,宿主要求两个参数客户端
src/client/api.ts:470-472只发{ id }:宿主
src/index.ts:601-602两个参数都requireString:requireString(src/wire.ts:95-102)对缺失键抛bad-request。对照同族两个入口,这里明显是漏带:
archive.build(api.ts:453)用scopePayload(scope, …)携带 sessionId;archiveDownloadUrl(api.ts:477-479)的 GET 也带sessionId(注释:the host answers only the session that built it)。而
call()本身是裸 fetch,不会自动附加 sessionId——文件头注释「Every call posts … with the sessionId」实际由各调用方负责放进去。触发路径
文件树多选(或单目录)右键「压缩并下载」→ build 成功 → 轮询任务(
src/client/FileTree.tsx:1378)await archiveStatus(job.id)→ 400 → catch →failArchive(...)(FileTree.tsx:1393-1395)。100% 复现。为什么测试全绿
tests/zip.spec.ts的 status 用例全部直发{ id, sessionId }(如 :590)——测的是宿主侧,不是客户端实际载荷;tests/file-tree-archive.spec.tsx:34把api.archiveStatus整个 mock 掉了。客户端实际发出的载荷形状没有任何测试覆盖。
修复方向
archiveStatus改收 scope 并走scopePayload(与 build / download URL 一致);宿主语义不变——sessionId 本来就是任务归属校验。顺带补一个「客户端真实载荷」形状的测试即可拦住回归。顺带:本条修复后会显形的第二个问题(建议同批处理)
FileTree.tsx:1376-1397:轮询看到state === 'ready'后调save(job.id, job.name)——saveArchive(:1340)是void、不 await、也不先settleArchive()(:1326),archiveJobId保持非 null,250ms 轮询继续跑。而下载路由是消费式的:src/archive-route.ts:267"A download consumes the task" +tasks.release,首次 GET 服务后任务即删。当首个 GET 往返超过一个轮询间隔(大包 / SSH 远端——正是本插件支持的形态)时:status仍 ready → 并发第二个save(双下载,anchor.click 两次);status404 →failArchive('unknown or expired archive'),而 GET 成功回调又会setActionError(null),两个回调落点顺序不定——常见终态是文件下载成功 + 错误条 zipFailed 并存。本机小文件(<250ms)侥幸干净。建议:轮询看到 ready 后先
settleArchive()停轮询(或在 save 前置重入护栏),再做恰好一次下载。环境:v0.24.1(492661a)。v0.22.0 无此代码(整条 archive 链为 #782 新增)。