Le téléchargement de la sauvegarde ne peut pas reprendre après une interruption car le jeton d'e-mail a déjà été utilisé

Il y a eu une problème précédent avec le téléchargement des sauvegardes qui semble avoir été corrigé dans la version 2026.01.0-latest. Je pense qu’il s’agit ici d’un problème actuel distinct, lié à la récupération après un téléchargement de sauvegarde interrompu, et non de l’échec initial de téléchargement décrit précédemment.

L’une des hypothèses soulevées dans cette discussion antérieure était que le lien de sauvegarde à usage unique pourrait empêcher un navigateur de reprendre un téléchargement interrompu : une fois que la première requête a consommé le lien, une requête de reprise ultérieure pourrait ne plus être autorisée.

J’ai désormais pu reproduire exactement ce comportement sur la version actuelle de Discourse.

Reproduction

Je téléchargeais une sauvegarde locale de Discourse de :

4 231 639 143 octets

en utilisant Safari sur iOS.

Le téléchargement progressait normalement. J'ai une capture d'écran montrant qu'il passait d'environ :

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

313,3 Mo à 317,8 Mo

J’ai ensuite quitté Safari pendant seulement environ 15 secondes.

Lorsque je suis revenu à Safari, le téléchargement s’était arrêté à environ :

319,8 Mo

et Safari affichait son contrôle de réessai.


Le journal d’accès Caddy pour la requête d’origine montre :

GET /admin/backups/...

HTTP/3

Range: none

status: 200

Content-Length: 4231639143

size: 319864024

J’ai ensuite utilisé le contrôle de réessai de Safari lui-même.

Safari a effectué une véritable requête HTTP de plage d’octets :

GET /admin/backups/...

HTTP/3

Range: bytes=319799904-

status: 422

Safari ne tentait donc pas simplement un autre téléchargement complet. Il essayait correctement de reprendre le fichier existant à peu près au point où le transfert précédent s’était arrêté.

Discourse a rejeté cette requête Range avec un code 422.

Code actuel de Discourse

En examinant le code actuel de Admin::BackupsController#show, la séquence est effectivement la suivante :

EmailBackupToken.compare(current_user.id, token)

        ↓

find backup

        ↓

EmailBackupToken.del(current_user.id)

        ↓

send_file

Ainsi, la requête initiale authentifiée consomme le jeton d’e-mail avant que le transfert de plusieurs gigaoctets ne soit réellement terminé.

Si ce transfert est ensuite interrompu, Safari effectue une autre requête telle que :

Range: bytes=319799904-

mais cette requête passe à nouveau par /admin/backups/… et le jeton d’e-mail a déjà été supprimé.

Cela semble expliquer le code 422 que j’ai capturé.

La session authentifiée est toujours requise en plus du jeton de sauvegarde, je ne propose donc pas simplement de rendre le jeton existant réutilisable indéfiniment.

Plutôt, je me demande s’il devrait y avoir un moyen borné pour le même administrateur authentifié de reprendre le même téléchargement de sauvegarde déjà autorisé.

Par exemple, cela pourrait être une autorisation de téléchargement à durée de vie courte associée au même utilisateur et à la même sauvegarde, bien que les mainteneurs aient peut-être une meilleure façon de gérer cela.

Pourquoi cela devient plus facile à reproduire sur iOS

L’interruption qui a révélé ce problème semble également similaire aux rapports des utilisateurs des versions récentes d’iOS.

Il y a au moins trois rapports hébergés sur la communauté Apple :

rapports plus anciens

« Les téléchargements en arrière-plan de Safari échouent après la mise à jour d’IOS 26 » — octobre 2025

Safari Background Downloads Failing After… - Apple Community

L’utilisateur rapporte que les grands téléchargements de Safari échouent lorsqu’ils s’exécutent en arrière-plan après la mise à niveau vers iOS 26. Ils ont également mis à niveau un iPhone 15 Pro vers iOS 26 et ont rapporté le même comportement.

« Problème de téléchargement en arrière-plan de Safari iOS - Annulation lors de la minimisation » — décembre 2025

Safari iOS Background Download Issue - Ca… - Apple Community

Signalé sur un iPhone 15 exécutant iOS 26.1. Les téléchargements démarrent normalement mais sont annulés après le passage à une autre application ou la minimisation de Safari.

« Safari ne peut pas terminer un grand téléchargement » — avril 2026

Signalé sur un iPhone 17 Pro Max exécutant iOS 26.4. Les grands téléchargements sont supposés se mettre en pause peu après le verrouillage de l’appareil, le passage entre Safari, ou le changement entre données cellulaires et Wi-Fi. L’auteur signale spécifiquement une bonne réception et affirme que les mêmes téléchargements sont terminés sur PC, Mac et Android.

Je ne pense pas que ces rapports communautaires suffisent à affirmer qu’Apple a confirmé une régression d’iOS.

Cependant, ils sont cohérents avec ma capture d’écran : la sauvegarde était en cours de téléchargement normalement, je suis brièvement passé d’une autre application à Safari, et lorsque je suis revenu, le transfert avait été interrompu.

Le point important côté Discourse est indépendant de la raison de l’interruption :

le transfert de sauvegarde volumineux est interrompu

        ↓

le navigateur tente une reprise Range basée sur les normes

        ↓

le jeton de sauvegarde a déjà été consommé

        ↓

la reprise reçoit un code 422

Observations côté serveur

Cela ne semble pas être un problème général avec le chemin nginx/sendfile actuel.

Le même chemin de sauvegarde multi-Go a été complété avec succès :

  • depuis Firefox/Linux via HTTP/3 ; et
  • depuis le même iPhone via Wi-Fi en utilisant HTTP/3.

Le nginx actuel publie également le support des plages d’octets.

Je ne suggère donc pas que HTTP/3, Caddy ou nginx est intrinsèquement incapable de livrer la sauvegarde.

Dans cette reproduction particulière, le transfert d’origine a été interrompu après environ 320 Mo, Safari a ensuite effectué une requête Range apparemment valide, et cette deuxième requête a été rejetée par Discourse.

Aurait-il du sens qu’un téléchargement de sauvegarde déjà autorisé dispose d’un mécanisme à durée de vie courte permettant au même administrateur authentifié de reprendre ce même fichier après un transfert interrompu ?

J’ai examiné l’historique et la couverture de tests actuelle afin de mieux comprendre ce qu’une correction potentielle devrait préserver.

Le comportement à usage unique remonte à la modification de durcissement de la sécurité de mars 2017 :

Cet engagement a introduit les jetons de téléchargement de sauvegarde envoyés par e-mail et a également consommé le jeton immédiatement dès que la demande de téléchargement avait été autorisée.

Le même ordre de traitement existe toujours dans Admin::BackupsController#show actuel :

L’implémentation sous-jacente du jeton est également toujours très simple :

EmailBackupToken stocke un jeton Redis par utilisateur avec une expiration d’une journée. Il est lié à l’utilisateur plutôt qu’à une sauvegarde particulière.

En parcourant l’historique de ce fichier, les modifications depuis 2017 semblent être des changements mécaniques/de style plutôt que des changements au modèle d’autorisation.

Cela rend indésirable de simplement laisser le jeton e-mail existant réutilisable : cela affaiblirait la restriction d’usage unique d’origine, et le jeton lui-même n’est pas lié à la sauvegarde spécifique dans l’URL.

Les tests de requête actuels sont ici :

Ils couvrent un téléchargement initial valide, des jetons invalides, des identifiants de sauvegarde invalides/manquants et les autorisations, mais je n’ai pas trouvé de couverture pour un transfert interrompu suivi d’une requête Range:.

La spécification de requête API documente de même le chemin de téléchargement ordinaire nom de fichier + jeton :

Une direction de correctif possible semble donc être de préserver le jeton e-mail à usage unique existant pour initier le téléchargement de la sauvegarde, tout en fournissant une autorisation de reprise à durée limitée pour la sauvegarde locale déjà autorisée.

Je m’attendrais à ce qu’une telle autorisation de reprise soit strictement limitée à l’utilisateur authentifié et à la sauvegarde spécifique, et qu’elle autorise une requête de plage d’octets ultérieure sans rendre le jeton e-mail à usage unique général de l’utilisateur (valable un jour) réutilisable de manière générale.

Les comportements que je viserais à préserver/ajouter sont :

  • la GET initiale avec un jeton e-mail valide reste autorisée ;

  • une autre GET complète ordinaire utilisant le jeton e-mail consommé reste rejetée ;

  • une tentative de reprise par plage d’octets pour la même sauvegarde par le même administrateur authentifié peut être reprise pour une période courte et bornée ;

  • une tentative de reprise contre une autre sauvegarde reste rejetée ;

  • une autorisation de reprise expirée reste rejetée.

Je n’ai pas supposé que c’est nécessairement l’implémentation que les mainteneurs préféreront, mais la surface du code/du test semble assez contenue.

Je suis prêt à mettre en place un correctif de brouillon et des tests de régression pour la reproduction Range: de Safari si ce modèle de sécurité général semble raisonnable.