No se puede reanudar la descarga de la copia de seguridad tras una interrupción porque el token de correo ya se ha consumido

Hubo un problema anterior con la descarga de copias de seguridad que parece haberse corregido en la versión 2026.01.0-latest. Creo que este es un problema actual y separado que involucra la recuperación de una descarga de copia de seguridad interrumpida, en lugar de la falla inicial de descarga anterior.

Una posibilidad planteada en esa discusión anterior era que el enlace de copia de seguridad de un solo uso podría impedir que el navegador reanudara una descarga interrumpida: una vez que la primera solicitud ha consumido el enlace, una solicitud de reanudación posterior podría no estar autorizada.

Ahora he sido capaz de reproducir exactamente ese comportamiento en la versión actual de Discourse.

Reproducción

Estaba descargando una copia de seguridad local de Discourse de:

4.231.639.143 bytes

usando Safari en iOS.

La descarga estaba progresando con normalidad. Tengo una grabación de pantalla que muestra cómo aumentaba aproximadamente de:

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

313,3 MB a 317,8 MB

Luego, me alejé de Safari durante solo unos 15 segundos.

Cuando volví a Safari, la descarga se había detenido en aproximadamente:

319,8 MB

y Safari mostró su control de reintento.


El registro de acceso de Caddy para la solicitud original muestra:

GET /admin/backups/...

HTTP/3

Range: none

status: 200

Content-Length: 4231639143

size: 319864024

Luego, usé el control de reintento propio de Safari.

Safari realizó una solicitud de rango de bytes HTTP genuina:

GET /admin/backups/...

HTTP/3

Range: bytes=319799904-

status: 422

Por lo tanto, Safari no estaba simplemente intentando otra descarga completa. Estaba intentando correctamente reanudar el archivo existente aproximadamente en el punto donde se detuvo la transferencia anterior.

Discourse rechazó esa solicitud de Range con 422.

Código actual de Discourse

Al examinar el código actual de Admin::BackupsController#show, la secuencia es efectivamente:

EmailBackupToken.compare(current_user.id, token)

        ↓

find backup

        ↓

EmailBackupToken.del(current_user.id)

        ↓

send_file

Por lo tanto, la solicitud autenticada inicial consume el token del correo electrónico antes de que la transferencia de varios gigabytes se haya completado realmente.

Si esa transferencia se interrumpe posteriormente, Safari realiza otra solicitud como:

Range: bytes=319799904-

pero esa solicitud pasa nuevamente por /admin/backups/… y el token del correo electrónico ya ha sido eliminado.

Eso parece explicar el 422 que capturé.

También se requiere la sesión autenticada, además del token de copia de seguridad, por lo que no sugiero simplemente hacer que el token existente sea reutilizable indefinidamente.

Más bien, me pregunto si debería haber alguna forma acotada para que el mismo administrador autenticado pueda reanudar la misma descarga de copia de seguridad ya autorizada.

Por ejemplo, podría ser una autorización de descarga de vida corta asociada con el mismo usuario y copia de seguridad, aunque los mantenedores podrían tener una mejor manera de manejar esto.

Por qué esto se está volviendo más fácil de reproducir en iOS

La interrupción que expuso esto también parece similar a los informes de usuarios de versiones recientes de iOS.

Hay al menos tres informes alojados en Apple Community:

informes anteriores

«Safari Background Downloads Failing After IOS 26 update» — octubre de 2025

Safari Background Downloads Failing After… - Apple Community

El usuario informa que las descargas grandes de Safari fallan cuando se ejecutan en segundo plano después de actualizar a iOS 26. También actualizó un iPhone 15 Pro a iOS 26 e informó el mismo comportamiento.

«Safari iOS Background Download Issue - Cancels on Minimize» — diciembre de 2025

Safari iOS Background Download Issue - Ca… - Apple Community

Informado en un iPhone 15 con iOS 26.1. Las descargas comienzan con normalidad pero se cancelan después de cambiar a otra aplicación o minimizar Safari.

«Safari can’t complete a large download» — abril de 2026

Informado en un iPhone 17 Pro Max con iOS 26.4. Se informa que las descargas grandes se pausan poco después de bloquear el dispositivo, dejar Safari o cambiar entre datos móviles y Wi-Fi. El autor informa específicamente de una buena señal y dice que las mismas descargas se completan en PC, Mac y Android.

No creo que esos informes de la comunidad sean suficientes para afirmar que Apple ha confirmado una regresión en iOS.

Sin embargo, son consistentes con mi grabación de pantalla: la copia de seguridad se estaba descargando con normalidad, me alejé de Safari brevemente y, cuando volví, la transferencia se había interrumpido.

El punto importante del lado de Discourse es independiente de la causa de la interrupción:

la transferencia de una copia de seguridad grande se interrumpe

        ↓

el navegador intenta una reanudación Range basada en estándares

        ↓

el token de copia de seguridad ya ha sido consumido

        ↓

la reanudación recibe 422

Observaciones del lado del servidor

Esto no parece ser un problema general con la ruta actual de nginx/sendfile.

La misma ruta de copia de seguridad de varios GB se ha completado con éxito:

  • desde Firefox/Linux a través de HTTP/3; y
  • desde el mismo iPhone a través de Wi-Fi usando HTTP/3.

El nginx actual también anuncia soporte para rangos de bytes.

Por lo tanto, no sugiero que HTTP/3, Caddy o nginx sean intrínsecamente incapaces de entregar la copia de seguridad.

En esta reproducción en particular, la transferencia original se interrumpió después de unos 320 MB, Safari realizó posteriormente una solicitud de Range que parecía válida, y esa segunda solicitud fue rechazada por Discourse.

¿Tendría sentido que una descarga de copia de seguridad ya autorizada tuviera algún mecanismo de vida corta que permitiera al mismo administrador autenticado reanudar ese mismo archivo después de una transferencia interrumpida?

He revisado el historial y la cobertura de pruebas actual para formarme una mejor idea de lo que una posible corrección debería preservar.

El comportamiento de uso único data del cambio de endurecimiento de seguridad de marzo de 2017:

Ese commit introdujo tokens de descarga de copias de seguridad enviados por correo electrónico y también consumía el token inmediatamente una vez que la solicitud de descarga había sido autorizada.

El mismo orden de ejecución sigue existiendo en Admin::BackupsController#show actual:

La implementación subyacente del token también sigue siendo muy simple:

EmailBackupToken almacena un token de Redis por usuario con una caducidad de un día. Está acotado al usuario en lugar de estar vinculado a una copia de seguridad específica.

Al revisar el historial de ese archivo, los cambios desde 2017 parecen haber sido mecánicos o de estilo, en lugar de cambios en el modelo de autorización.

Eso hace que dejar simplemente que el token enviado por correo electrónico existente sea reutilizable parezca indeseable: debilitaría la restricción original de uso único, y el token en sí mismo no está acotado a la copia de seguridad específica en la URL.

Las pruebas de solicitud actuales están aquí:

Cubren una descarga inicial válida, tokens inválidos, IDs de copia de seguridad inválidos o faltantes y permisos, pero no pude encontrar cobertura para una transferencia interrumpida seguida de una solicitud Range:.

La especificación de solicitud de la API también documenta la ruta de descarga ordinaria de nombre de archivo + token:

Por lo tanto, una posible dirección para el parche parece ser preservar el token de correo electrónico de uso único existente para iniciar la descarga de la copia de seguridad, mientras se proporciona una autorización de reanudación de vida corta separada para la copia de seguridad local ya autorizada.

Me esperaría que dicha autorización de reanudación estuviera estrechamente acotada al usuario autenticado y a la copia de seguridad específica, y que permitiera una solicitud posterior de rango de bytes sin hacer que el token de correo electrónico de un día y a nivel de usuario original sea generalmente reutilizable.

Los comportamientos que me gustaría preservar/agregar son:

  • la GET inicial con un token enviado por correo electrónico válido sigue permitida;

  • otra GET completa ordinaria que use el token de correo electrónico consumido sigue rechazada;

  • un reintento de rango de bytes para la misma copia de seguridad por el mismo administrador autenticado puede reanudarse durante un período corto y acotado;

  • un intento de reanudación contra otra copia de seguridad sigue rechazado;

  • una autorización de reanudación caducada sigue rechazada.

No he asumido que esto sea necesariamente la implementación que los mantenedores preferirán, pero la superficie de código/pruebas parece bastante contenida.

Estoy dispuesto a preparar un borrador del parche y pruebas de regresión para la reproducción de Range: de Safari si este modelo de seguridad general parece razonable.