由于邮件令牌已被使用,备份下载中断后无法继续

之前曾出现过备份下载问题,该问题似乎在 2026.01.0-latest 版本中已得到修复。我认为这是一个独立的当前问题,涉及从中断的备份下载中恢复,而非旧的初始下载失败。

在之前的讨论中,有人提出一种可能性:一次性备份链接可能会阻止浏览器恢复中断的下载:一旦第一个请求消耗了该链接,后续的恢复请求可能不再被授权。

我目前已在当前的 Discourse 上成功复现了这种行为。

复现步骤

我正在下载一个本地 Discourse 备份,大小为:

4,231,639,143 字节

使用的是 iOS 上的 Safari 浏览器。

下载正在正常进行。我有一段屏幕录像,显示其从大约:

https://youtube.com/shorts/_Pjc-kxJ1OE?feature=shared

313.3 MB 增加到 317.8 MB

然后我离开 Safari 仅约 15 秒。

当我返回 Safari 时,下载已停止在大约:

319.8 MB

并且 Safari 显示了其重试控件。


原始请求的 Caddy 访问日志显示:

GET /admin/backups/...

HTTP/3

Range: none

status: 200

Content-Length: 4231639143

size: 319864024

然后我使用了 Safari 自带的重试控件。

Safari 发出了一个真正的 HTTP 字节范围请求:

GET /admin/backups/...

HTTP/3

Range: bytes=319799904-

status: 422

因此,Safari 并不是简单地尝试进行另一次完整下载。它正确地试图从上次传输停止的大约位置恢复现有文件。

Discourse 以 422 状态码拒绝了该 Range 请求。

当前 Discourse 代码

查看当前的 Admin::BackupsController#show,其流程实际上为:

EmailBackupToken.compare(current_user.id, token)

        ↓

find backup

        ↓

EmailBackupToken.del(current_user.id)

        ↓

send_file

因此,初始的经过身份验证的请求会在多 GB 传输实际完成之前消耗电子邮件令牌。

如果该传输随后被中断,Safari 会发出另一个请求,例如:

Range: bytes=319799904-

但该请求再次通过 /admin/backups/… 处理,而电子邮件令牌已经被删除。

这似乎解释了我捕获到的 422 错误。

除了备份令牌外,仍然需要经过身份验证的会话,因此我并不是建议简单地让现有令牌无限期可重用。

相反,我在思考是否应该有一种有限制的方式,让同一位经过身份验证的管理员能够恢复同一个已授权备份的下载。

例如,这可能是一个与同一用户和备份相关联的短时效下载授权,尽管维护者可能有更好的处理此问题的方法。

为什么这在 iOS 上变得更容易复现

暴露此问题的中断似乎与来自最近 iOS 版本用户的报告类似。

Apple Community 上至少有三个相关报告:

较早的报告

“Safari 在 iOS 26 更新后后台下载失败” — 2025 年 10 月

Safari Background Downloads Failing After… - Apple Community

该用户报告称,升级到 iOS 26 后,Safari 在后台运行时较大的下载会失败。他们还将 iPhone 15 Pro 升级到 iOS 26,并报告了相同的行为。

“Safari iOS 后台下载问题 - 最小化时取消” — 2025 年 12 月

Safari iOS Background Download Issue - Ca… - Apple Community

在运行 iOS 26.1 的 iPhone 15 上报告。下载开始时正常,但在切换到另一个应用或最小化 Safari 后取消。

“Safari 无法完成大型下载” — 2026 年 4 月

在运行 iOS 26.4 的 iPhone 17 Pro Max 上报告。据报告,大型下载在锁定设备后不久暂停,或者在蜂窝数据和 Wi-Fi 之间切换时暂停。作者特别报告信号良好,并表示相同的下载在 PC、Mac 和 Android 上可以完成。

我认为这些社区报告不足以说明 Apple 已确认 iOS 存在回归问题。

然而,它们与我的屏幕录像是一致的:备份正在正常下载,我短暂离开了 Safari,当我返回时传输已中断。

Discourse 端的重要观点与中断发生的原因无关:

大型备份传输被中断

        ↓

浏览器尝试基于标准的 Range 恢复

        ↓

备份令牌已被消耗

        ↓

恢复请求收到 422

服务器端观察

这似乎不是当前 nginx/sendfile 路径的一般性问题。

相同的几 GB 备份路径已成功完成:

  • 通过 HTTP/3 从 Firefox/Linux;以及
  • 通过 Wi-Fi 使用 HTTP/3 从同一部 iPhone。

当前的 nginx 也宣传支持字节范围。

因此,我并不是建议 HTTP/3、Caddy 或 nginx 本质上无法提供备份。

在这次特定的复现中,原始传输在大约 320 MB 后中断,Safari 随后发出了一个看似有效的 Range 请求,而该第二个请求被 Discourse 拒绝。

对于已经授权的备份下载,是否应该有一种短时效机制,允许同一位经过身份验证的管理员在中断传输后恢复该同一文件的下载?

我查阅了历史记录和当前的测试覆盖率,以更好地了解可能的修复方案需要保留哪些内容。

这种一次性使用的行为可以追溯到 2017 年 3 月的安全加固更改:

该提交引入了通过电子邮件发送的备份下载令牌,并且在下载请求获得授权后立即消耗该令牌。

当前的 Admin::BackupsController#show 中仍然存在相同的顺序:

底层令牌实现也仍然非常简单:

EmailBackupToken 为每个用户在 Redis 中存储一个令牌,有效期为一天。它是基于用户范围的,而不是与特定的备份绑定。

查看该文件的历史记录,自 2017 年以来的更改似乎是机械式/风格上的更改,而不是授权模型的更改。

因此,简单地让现有的电子邮件令牌可重复使用似乎并不可取:这会削弱原始的一次性限制,而且令牌本身并未限定于 URL 中的特定备份。

当前的请求测试位于此处:

它们涵盖了初始有效下载、无效令牌、无效/缺失的备份 ID 和权限,但我没有找到针对中断传输后跟随 Range: 请求的测试覆盖。

API 请求规范同样记录了普通的文件名 + 令牌下载路径:

因此,一个可能的补丁方向似乎是保留现有的电子邮件令牌用于发起备份下载,同时为已获授权的本地备份提供单独的短期续传授权。

我预计这种续传授权将严格限定于经过身份验证的用户和特定备份,并允许后续的字节范围请求,而不会使原始的一天用户范围电子邮件令牌普遍可重复使用。

我旨在保留/添加的行为是:

  • 使用有效电子邮件令牌的初始 GET 请求仍然被允许;
  • 使用已消耗的电子邮件令牌进行的另一次普通完整 GET 请求仍然被拒绝;
  • 同一经过身份验证的管理员针对同一备份的字节范围重试可以在短暂的有限时间内恢复;
  • 针对其他备份的续传尝试仍然被拒绝;
  • 过期的续传授权仍然被拒绝。

我并没有假设这一定是维护者首选的实现方式,但代码/测试范围似乎相当有限。

如果这种通用的安全模型听起来合理,我很乐意为 Safari Range: 复现场景起草一个补丁和回归测试。