✨ 为 GM.download/GM_download 添加 onloadend 回调#1563
Conversation
对齐 GM_xmlhttpRequest 的 onloadend 语义:无论下载以 onload/save_cancelled/ ontimeout/onerror 中的哪一种结束,onloadend 都会紧随其后触发一次,覆盖 browser 与 native 两种 downloadMode 的所有终态分支。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- native 模式下 xhr 因 ontimeout/onerror 失败后,底层实现仍会在其 onloadend 带上已收数据拼出的 Blob;补充 xhrFailed 标志位拦截,避免把部分数据当作 下载成功经 chrome.downloads 落盘为被截断的文件,且避免用户 onloadend 被 触发两次。 - browser 模式下 a.connect() 失败此前未被捕获,导致 GM.download 返回的 promise 永远 pending;补上与 native 模式一致的 try/catch,通过 onerror/ onloadend/reject 通知调用方。 - 所有终态分支(onload/save_cancelled/ontimeout/onerror/default)新增 try/finally,防止用户 onloadend 抛错时跳过 retPromiseResolve/Reject 与 releaseResources(避免 promise 悬挂、blob URL 泄漏)。 - 补上 src/template/scriptcat.d.tpl 中 DownloadDetails.onloadend 声明, 修复内置编辑器仍将 onloadend 报告为未知属性的问题。 新增 6 个回归测试覆盖上述场景。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
- 新增 withLoadEnd 共享辅助函数,将「主回调」(onload/onerror/ontimeout) 与 onloadend 分别置于内层 try/finally:此前两者共享同一个 try 区块,一旦 主回调抛错会直接跳到外层 finally,导致 onloadend 从未被调用,违反其 “总是紧随主回调之后触发”的约定。该辅助函数应用于 browser 模式、native 模式浏览器下载阶段的所有终态分支,以及 native 模式 xhr 阶段的 ontimeout/onerror。 - native 模式 xhr 阶段的 ontimeout/onerror 新增无条件的 retPromiseReject 调用(置于 finally 内,不再依赖 GM_xmlhttpRequest 内部 retPromise 延后 reject 再经由 `if (aborted) return` 转发)。此前若使用者在 onloadend 回调内呼叫 download.abort(),aborted 旗标会被提前设为 true,导致延后的 reject 被短路跳过,GM.download 返回的 promise 永久 pending。Promise 的 resolve/reject 本身具幂等性,早/晚两处调用互不冲突。 新增 2 个回归测试:browser 模式 onerror 主回调抛错、native 模式 xhr 阶段 失败后于 onloadend 内呼叫 abort()。均已验证在修复前会失败(前者断言失败, 后者测试超时挂起),修复后转为通过。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed the two additional findings in d67f672:
Added 2 regression tests to
Verified both by temporarily reverting just the fix and confirming: the first assertion fails, and the second times out (hangs) exactly as described. Full suite (3097 tests / 290 files) + |
- xhr 阶段(ontimeout/onerror)改为在呼叫使用者回调前先清空 nativeAbort。 此前若使用者在 onloadend 内呼叫 download.abort(),会经由 nativeAbort() 无条件断开内部 GM_xmlhttpRequest 的消息连线;而该内部请求要到 details.onerror 返回后才会置位 reqDone,其 abort() 又只在提供了 onabort 回调时才合成 loadend——下载场景未提供 onabort,等于内部请求 永远收不到自己的 onloadend,其 retPromise 永久 pending,也无法完成 refCleanup。清空 nativeAbort 后,abort() 中的 nativeAbort?.() 变为 空操作,内部请求得以自然走完生命周期。 - xhr 阶段的使用者回调(含新增的 onloadend)现在被 try/catch 包裹: 例外不再同步向上传播回 GM_xmlhttpRequest 内部的消息处理循环——那里 在 code===-1 协议错误分支会在 details.onerror 返回后才安排合成 onloadend(因为该分支预期不会有真正的 onloadend 消息到达),一旦 同步抛出会跳过这个排程,同样导致内部请求永久卡住。改为透过 queueMicrotask 异步重新抛出,例外仍可被观察到,但不再阻断内部 生命周期收尾。 新增 1 个回归测试并强化既有 abort 竞态测试: - 验证 xhr 阶段失败后于 onloadend 内呼叫 abort(),不会再触发内部 GM_xmlhttpRequest 自身的 abort(即不中断其消息连线)。 - 验证 xhr 阶段使用者回调抛错时,不会同步向上传播,而是透过 queueMicrotask 异步重新抛出。 均已验证在修复前会失败(前者断言失败,后者同步抛出未被捕获), 修复后转为通过。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Checklist / 检查清单
背景
当前
GM.download/GM_download支持通过onload、onerror和ontimeout分别处理不同的下载终止状态,但缺少一个无论成功、失败还是超时都会执行的统一收尾回调。这使用户脚本不得不在多个终止回调中重复执行相同的清理、状态更新或 UI 收尾逻辑,也与
GM_xmlhttpRequest已提供的onloadend生命周期接口不一致。本次改动
为
GMTypes.DownloadDetails新增onloadend回调。同步更新以下类型声明:
src/types/scriptcat.d.tssrc/types/scriptcat.zh-CN.d.tssrc/template/scriptcat.d.tpl在以下下载路径中接入
onloadend:downloadMode: "browser":直接通过后台chrome.downloads下载;downloadMode: "native":先通过GM_xmlhttpRequest获取 Blob,再交由后台保存;在下载通过以下状态结束时,先执行对应主回调,再执行
onloadend:onloadonerrorontimeoutsave_cancelled,沿用现有兼容行为,将用户取消“另存为”视为成功。将 browser 和 native 最终保存阶段的消息处理合并为统一逻辑,避免两条路径的终止语义出现差异。
补充单元和回归测试,覆盖成功、失败、超时、后台连接失败、部分数据、回调抛错、主动中止竞态、Promise 收尾及 Blob URL 释放。
实现考虑
1. 保证主回调与
onloadend相互独立终止处理统一通过
withLoadEnd执行:onload、onerror或ontimeout;onloadend;这可以避免以下情况:
onload/onerror/ontimeout抛错后,onloadend被跳过;onloadend抛错后,GM.download返回的 Promise 永久 pending;2. 统一 browser 与 native 的最终下载阶段
browser 模式会直接连接后台执行下载;native 模式则分为两个阶段:
GM_xmlhttpRequest获取完整 Blob;两条路径最终都会收到相同的后台消息,例如
onload、save_cancelled、ontimeout和onerror,因此共用同一套终止处理逻辑。native 模式额外传入幂等的
releaseResources,在以下场景中释放 Blob URL:3. 避免将失败请求的部分数据保存为文件
native 模式下,底层 XHR 即使已经触发
onerror或ontimeout,后续onloadend仍可能携带由已接收数据组成的非空 Blob。如果仅判断
response instanceof Blob,这部分不完整数据可能被误认为下载成功,并继续交给chrome.downloads保存为截断文件,同时造成用户侧onloadend被重复触发。实现通过
xhrFailed标记记录 XHR 阶段是否已经失败。失败后收到的 XHRonloadend不再进入 Blob 保存流程。4. 避免
abort()破坏内部 XHR 的收尾流程native 模式的 XHR 阶段进入
onerror或ontimeout后,会在调用用户回调前清空nativeAbort。这样即使用户在
onloadend内调用外层download.abort(),也不会再次中止并断开内部GM_xmlhttpRequest的消息通道,避免内部请求无法收到自己的onloadend、Promise 永久 pending 或引用清理无法执行。外层
GM.download的 Promise 会直接在 XHR 失败回调的收尾阶段 reject,而不是仅依赖内部 XHR Promise 的延迟失败结果。5. 用户回调异常不能中断内部状态机
在 native XHR 的
onerror/ontimeout阶段,用户回调的异常不能同步传播回GM_xmlhttpRequest的消息处理循环。否则可能中断内部的完成状态更新、合成
onloadend排程及引用清理。因此这一路径会先完成:
onloadend调用;随后再通过微任务异步重新抛出用户回调异常,使异常仍可被全局错误处理捕获,但不会破坏内部状态机。
已知限制
1. 主动调用
abort()不会新增触发onloadend本次改动只为成功、失败、超时及下载保存阶段的终止结果补充
onloadend。主动调用返回对象的
abort()仍沿用现有行为,不额外合成onabort或onloadend。如需统一主动中止的生命周期,需要单独设计GM.download的中止事件语义。2.
onloadend参数沿用对应终止事件的数据onloadend不定义新的专用响应结构,而是尽量复用对应终止状态的数据:onload相同的下载结果;{ error: "unknown" };context。因此它不是原生 DOM
ProgressEvent的完整映射。3.
save_cancelled继续视为成功用户在“另存为”窗口中取消操作时,现有实现会将
save_cancelled视为成功,并依次调用onload和onloadend、resolveGM.downloadPromise。本 PR 保留该兼容行为,不调整取消保存的结果语义。
建议审查重点
onload/onerror/ontimeout与onloadend的调用顺序是否符合预期;xhrFailed是否能完整阻止部分 Blob 被继续保存;onloadend内调用abort()时,外层和内部 Promise 是否都能正常结束;