# Backup download cannot resume after interruption because email token has already been consumed

**URL:** https://meta.discourse.org/t/backup-download-cannot-resume-after-interruption-because-email-token-has-already-been-consumed/411277
**Category:** Self-hosting
**Tags:** backups, nginx, ios, install
**Created:** [31 augustus 2026 om 09:38 UTC](https://meta.discourse.org/t/backup-download-cannot-resume-after-interruption-because-email-token-has-already-been-consumed/411277 "2026-08-31T09:38:41Z")
**Posts on this page:** 1
**Showing post:** 3

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [31 augustus 2026 om 14:28 UTC](https://meta.discourse.org/t/backup-download-cannot-resume-after-interruption-because-email-token-has-already-been-consumed/411277/3 "2026-08-31T14:28:37Z")

</div>

I have now put together a PR for this:

> <https://github.com/discourse/discourse/pull/43041>
>
> \## Summary
> 
> Allows interrupted \*\*local\*\* backup downloads to resume with an HT…TP \`Range\` request after the one-use emailed backup token has been consumed.
> 
> This addresses the case reported on Meta where a multi-GB local backup download was interrupted and the browser retried with \`Range\`, but the retry received \`422\` because \`EmailBackupToken.del\` had already consumed the token:
> 
> https://meta.discourse.org/t/backup-download-cannot-resume-after-interruption-because-email-token-has-already-been-consumed/411277
> 
> \## Approach
> 
> The existing emailed token remains the only way to initiate a backup download. When resumable local downloads are enabled, the first authorized \*\*local\*\* download creates a narrower resume grant which is scoped to:
> 
> \* the same authenticated administrator
> \* the same backup
> \* the same original emailed token value
> \* requests carrying a \`Range\` header
> \* a bounded expiry window
> 
> The original \`EmailBackupToken\` is still consumed after the initial request. Ordinary reuse of that token for another full download remains rejected, and the resume grant cannot be used for a different backup or user.
> 
> Remote/S3 backup downloads keep their existing presigned-URL redirect behavior and do not receive a local resume grant.
> 
> A resumed request is treated as a continuation of the same authorized download, so it does not create a second \`backup\_download\` staff action entry.
> 
> \## Resume window setting
> 
> Adds \`backup\_download\_resume\_window\` as an enum rather than an unrestricted numeric duration, to avoid an administrator accidentally configuring an excessively long authorization window.
> 
> Options are:
> 
> \* \`disabled\`
> \* \`1\_hour\`
> \* \`6\_hours\` — shown as \*\*6 hours (recommended)\*\* in the admin UI
> \* \`12\_hours\`
> \* \`until\_email\_token\_expires\`
> 
> The effective resume TTL is capped to the remaining lifetime of the original emailed token when the initial download begins.
> 
> The setting integrates with the existing backup settings and is only applicable to local storage:
> 
> \<img width="960" height="850" alt="Backup download resume window shown beneath the local backup location setting" src="https://github.com/user-attachments/assets/abbbdf04-e2f5-4d7d-b66a-1c73caddd583" /\>
> 
> The available values are deliberately bounded, with the compatibility-preserving default disabled and 6 hours identified as the recommended enabled option:
> 
> \<img width="341" height="426" alt="Backup download resume window enum showing Disabled, 1 hour, 6 hours recommended, 12 hours, and until the original email token expires" src="https://github.com/user-attachments/assets/4dd3dd5c-50e0-4df3-ac6e-9732b7878227" /\>
> 
> \### Default vs recommended value
> 
> This PR intentionally distinguishes the \*\*default\*\* from the \*\*recommended enabled option\*\*.
> 
> The current Discourse behavior is equivalent to \`disabled\`: once the emailed token is consumed by the initial request, a subsequent \`Range\` retry is rejected. To preserve existing behavior for current sites, this PR therefore keeps \`disabled\` as the default.
> 
> For admins who choose to enable resumable local downloads, \`6\_hours\` is labelled as the recommended enum option.
> 
> There is precedent for preserving existing behavior when introducing an enum setting in #36014: the previously hard-coded calendar view remained the default value of the new enum setting.
> 
> \*\*Maintainer question:\*\* given the concrete interrupted-download case in the linked Meta topic, would you prefer the default to remain \`disabled\` for backwards compatibility, or should the recommended \`6\_hours\` option become the default as part of fixing the issue?
> 
> By “recommended” here I mean the admin-facing recommendation attached to one enum option, not a separate setting or an unrestricted duration value.
> 
> \## Tests
> 
> Added coverage for:
> 
> \* default/disabled behavior remaining unchanged
> \* successful same-backup \`Range\` resume when enabled
> \* rejection of ordinary token reuse
> \* rejection of cross-backup resume
> \* immediate revocation when the setting is disabled
> \* no local resume grant for S3 backups
> \* one audit entry across the initial download and resume
> \* resume-token user/backup scoping and Redis expiry
> \* enum validation and TTL capping
> 
> Local focused test run:
> 
> \`94 examples, 0 failures\`
> 
> Ruby, YAML, and i18n linters also pass.

The implementation preserves the existing one-use emailed token for **initiating** the download, while allowing local backups to create a separate bounded resume authorization scoped to:

- the same authenticated administrator;

- the same backup;

- the same original token value; and

- a subsequent request carrying a `Range` header.

An ordinary second full `GET` using the consumed token is still rejected, as is a `Range` request for another backup. Remote/S3 backups retain their existing behaviour.

I made the resume period an enum rather than an unrestricted duration:

- `Disabled`

- `1 hour`

- `6 hours (recommended)`

- `12 hours`

- `Until the original email token expires`

The effective resume period is also capped to the remaining lifetime of the original emailed token when the initial download begins.

The current Discourse behaviour remains the default (`Disabled`) for backwards compatibility, while `6 hours` is presented as the recommended option when resumable downloads are enabled.

The PR asks whether maintainers would prefer to retain that compatibility-preserving default, or make the recommended resumable value the default as part of fixing this issue.

I’ve also added regression coverage for the successful resume path and the main authorization boundaries, including:

- ordinary token reuse remaining rejected;

- cross-backup resume remaining rejected;

- disabling the setting immediately preventing further resume;

- S3 not receiving a local resume grant; and

- the initial download and its resume producing only one backup-download audit entry.

The full CI suite is passing and the PR is ready for review.

Feedback on the security model, and particularly on the recommendation of 6 hours and the default setting, would be welcome.

---

_[View the full topic](https://meta.discourse.org/t/backup-download-cannot-resume-after-interruption-because-email-token-has-already-been-consumed/411277)._
