メールトークンが既に消費されているため、中断後のバックアップダウンロード再開ができません

2026.01.0-latest で修正されたような類似の問題を思い出しました。したがって、これは旧来の初期ダウンロード失敗ではなく、中断されたバックアップダウンロードからの復旧に関わる別の現在進行中の問題だと考えられます。

一度限りのバックアップリンクが、ブラウザによる中断されたダウンロードの再開を妨げていると考えるのは妥当ですが、単一のリンクでバックアップを2回ダウンロードすることはできません。そのため、一度失敗すると、ダウンロードの再開は Discourse 自身によってキャンセルされている可能性があります。

現在、この動作を現在の Discourse で正確に再現することができました。

再現手順

Safari (iOS) を使用して、以下のローカル Discourse バックアップをダウンロードしていました:

4,231,639,143 バイト

「ダウンロードは正常に進行していました。以下の値から増加している様子のスクリーンレコーディングがあります:」

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 はその Range リクエストを 422 で拒否しました。

現在の 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 に少なくとも3つの報告が投稿されています:

古い報告

「Safari Background Downloads Failing After IOS 26 update」 — 2025年10月

Safari Background Downloads Failing After… - Apple Community

ユーザーは、iOS 26 にアップグレードした後、バックグラウンドで実行中に大きな Safari ダウンロードが失敗すると報告しています。また、iPhone 15 Pro を iOS 26 にアップグレードし、同じ動作を報告しています。

「Safari iOS Background Download Issue - Cancels on Minimize」 — 2025年12月

Safari iOS Background Download Issue - Ca… - Apple Community

iOS 26.1 を実行する iPhone 15 で報告されています。ダウンロードは正常に開始されますが、別のアプリに切り替えたり Safari を最小化したりするとキャンセルされます。

「Safari can’t complete a large download」 — 2026年4月

iOS 26.4 を実行する iPhone 17 Pro Max で報告されています。大きなダウンロードは、デバイスをロックした直後、またはモバイル通信と Wi-Fi を切り替えた際に、 reportedly 一時停止するとのことです。著者は特に信号が良好であり、同じダウンロードは PC、Mac、Android では完了すると述べています。

これらのコミュニティ報告だけでは、Apple が iOS のリグレッションを確認したとは言えないと思います。

しかし、それらは私のスクリーンレコーディングと整合しています:バックアップは正常にダウンロードされており、私は一時的に Safari から離れ、戻った時点で転送が中断になっていたのです。

中断が発生した理由に関わらず、Discourse 側の重要なポイントは以下の通りです:

large backup transfer is interrupted

        ↓

browser attempts standards-based Range resume

        ↓

backup token has already been consumed

        ↓

resume receives 422

サーバーサイドの観察

これは現在の nginx/sendfile パスにおける一般的な問題ではないようです。

同じ数 GB のバックアップパスは、以下で正常に完了しています:

  • HTTP/3 を通じた Firefox/Linux から;および
  • 同じ iPhone から Wi-Fi を使用した HTTP/3 を通じて。

現在の nginx もバイト範囲サポートを宣伝しています。

したがって、HTTP/3、Caddy、または nginx が本質的にバックアップを配信できないと提案しているわけではありません。

この特定の再現において、元の転送は約 320 MB で中断され、その後 Safari は有効なように見える Range リクエストを行い、その2番目のリクエストが Discourse によって拒否されました。

すでに認可されたバックアップダウンロードに対して、中断された転送後に同じファイルを同じ認証済み管理者が再開できるようにする、短命なメカニズムを持つのは理にかなっているでしょうか?

I had a look through the history and current test coverage to get a better idea of what a possible fix would need to preserve.

The one-use behaviour dates back to the March 2017 security-hardening change:

That commit introduced emailed backup-download tokens and also consumed the token immediately once the download request had been authorized.

The same ordering still exists in current Admin::BackupsController#show:

The underlying token implementation is also still very simple:

EmailBackupToken stores one Redis token per user with a one-day expiry. It is user-scoped rather than being tied to a particular backup.

Looking through that file’s history, the changes since 2017 appear to have been mechanical/style changes rather than changes to the authorization model.

That makes simply leaving the existing emailed token reusable seem undesirable: it would weaken the original one-use restriction, and the token itself is not scoped to the particular backup in the URL.

The current request tests are here:

They cover an initial valid download, invalid tokens, invalid/missing backup IDs and permissions, but I couldn’t find coverage for an interrupted transfer followed by a Range: request.

The API request spec likewise documents the ordinary filename + token download path:

A possible patch direction therefore seems to be to preserve the existing one-use email token for initiating the backup download, while providing a separate short-lived resume authorization for the already-authorized local backup.

I would expect such a resume authorization to be narrowly scoped to the authenticated user and specific backup, and to permit a subsequent byte-range request without making the original one-day user-wide email token generally reusable.

The behaviours I’d aim to preserve/add are:

  • initial GET with a valid emailed token remains allowed;

  • another ordinary full GET using the consumed email token remains rejected;

  • a byte-range retry for the same backup by the same authenticated admin can be resumed for a short bounded period;

  • a resume attempt against another backup remains rejected;

  • an expired resume authorization remains rejected.

I haven’t assumed that this is necessarily the implementation maintainers will prefer, but the code/test surface appears fairly contained.

I’m happy to put together a draft patch and regression tests for the Safari Range: reproduction if this general security model sounds reasonable.