O download do backup não pode ser retomado após a interrupção porque o token de e-mail já foi consumido

Lembro-me de um problema semelhante que parece ter sido corrigido na versão 2026.01.0-latest, portanto, acredito que esta seja uma questão atual e separada, envolvendo a recuperação de um download de backup interrompido, em vez da falha antiga no download inicial.

É plausível pensar que o link de backup de uso único possa impedir o navegador de retomar um download interrompido, mas não é possível baixar um backup duas vezes com um único link. Portanto, quando ele falha uma vez, a retomada do download provavelmente é cancelada pelo próprio Discourse.

Agora consegui reproduzir exatamente esse comportamento no Discourse atual.

Reprodução

Eu estava baixando um backup local do Discourse de:

4.231.639.143 bytes

usando o Safari no iOS.

O download estava progredindo normalmente. Tenho uma gravação de tela mostrando o aumento de aproximadamente:

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

313,3 MB para 317,8 MB

Em seguida, saí do Safari por apenas cerca de 15 segundos.

Quando voltei ao Safari, o download havia parado em aproximadamente:

319,8 MB

e o Safari exibiu seu controle de nova tentativa.


O log de acesso do Caddy para a solicitação original mostra:

GET /admin/backups/...

HTTP/3

Range: none

status: 200

Content-Length: 4231639143

size: 319864024

Em seguida, usei o próprio controle de nova tentativa do Safari.

O Safari fez uma verdadeira solicitação de intervalo de bytes HTTP:

GET /admin/backups/...

HTTP/3

Range: bytes=319799904-

status: 422

Portanto, o Safari não estava simplesmente tentando outro download completo. Ele estava tentando corretamente retomar o arquivo existente aproximadamente no ponto em que a transferência anterior parou.

O Discourse rejeitou essa solicitação de Range com 422.

Código atual do Discourse

Olhando para o Admin::BackupsController#show atual, a sequência é efetivamente:

EmailBackupToken.compare(current_user.id, token)

        ↓

find backup

        ↓

EmailBackupToken.del(current_user.id)

        ↓

send_file

Portanto, a solicitação autenticada inicial consome o token do e-mail antes que a transferência de vários gigabytes tenha sido realmente concluída.

Se essa transferência for interrompida em seguida, o Safari faz outra solicitação, como:

Range: bytes=319799904-

mas essa solicitação passa novamente por /admin/backups/… e o token do e-mail já foi excluído.

Isso parece explicar o 422 que capturei.

A sessão autenticada ainda é necessária, bem como o token de backup, portanto, não estou sugerindo simplesmente tornar o token existente reutilizável indefinidamente.

Em vez disso, estou me perguntando se deveria haver alguma maneira limitada para o mesmo administrador autenticado retomar o mesmo download de backup já autorizado.

Por exemplo, isso poderia ser uma autorização de download de vida curta associada ao mesmo usuário e backup, embora os mantenedores possam ter uma maneira melhor de lidar com isso.

Por que isso está se tornando mais fácil de reproduzir no iOS

A interrupção que expôs isso também parece semelhante a relatórios de usuários de versões recentes do iOS.

Há pelo menos três relatórios hospedados na Apple Community:

relatórios mais antigos

“Falhas em downloads em segundo plano do Safari após atualização para IOS 26” — outubro de 2025

Safari Background Downloads Failing After… - Apple Community

O usuário relata que downloads maiores do Safari falham quando executados em segundo plano após a atualização para o iOS 26. Eles também atualizaram um iPhone 15 Pro para o iOS 26 e relataram o mesmo comportamento.

“Problema de download em segundo plano do Safari iOS - Cancela ao minimizar” — dezembro de 2025

Safari iOS Background Download Issue - Ca… - Apple Community

Relatado em um iPhone 15 executando o iOS 26.1. Os downloads começam normalmente, mas são cancelados após alternar para outro aplicativo ou minimizar o Safari.

“Safari não consegue concluir um download grande” — abril de 2026

Relatado em um iPhone 17 Pro Max executando o iOS 26.4. Relata-se que downloads grandes pausam pouco tempo depois de bloquear o dispositivo, deixar o Safari ou alternar entre celular e Wi-Fi. O autor relata especificamente bom sinal e diz que os mesmos downloads são concluídos em PC, Mac e Android.

Não acho que esses relatórios da comunidade sejam suficientes para afirmar que a Apple confirmou uma regressão no iOS.

No entanto, eles são consistentes com minha gravação de tela: o backup estava sendo baixado normalmente, saí do Safari brevemente e, quando voltei, a transferência havia sido interrompida.

O ponto importante do lado do Discourse é independente do motivo da interrupção:

transferência de backup grande é interrompida

        ↓

navegador tenta retomar com Range baseado em padrões

        ↓

token de backup já foi consumido

        ↓

retomada recebe 422

Observações do lado do servidor

Isso não parece ser um problema geral com o caminho atual nginx/sendfile.

O mesmo caminho de backup de vários GB foi concluído com sucesso:

  • do Firefox/Linux via HTTP/3; e
  • do mesmo iPhone via Wi-Fi usando HTTP/3.

O nginx atual também anuncia suporte a intervalos de bytes.

Portanto, não estou sugerindo que HTTP/3, Caddy ou nginx seja intrinsecamente incapaz de entregar o backup.

Nesta reprodução específica, a transferência original foi interrompida após cerca de 320 MB, o Safari fez posteriormente uma solicitação de Range aparentemente válida e essa segunda solicitação foi rejeitada pelo Discourse.

Faria sentido que um download de backup já autorizado tivesse algum mecanismo de vida curta que permita ao mesmo administrador autenticado retomar aquele mesmo arquivo após uma transferência interrompida?

Dê uma olhada no histórico e na cobertura de testes atual para ter uma melhor ideia do que uma possível correção precisaria preservar.

O comportamento de uso único remonta à mudança de endurecimento de segurança de março de 2017:

Esse commit introduziu tokens de download de backup enviados por e-mail e também consumia o token imediatamente assim que a solicitação de download era autorizada.

A mesma ordem ainda existe no Admin::BackupsController#show atual:

A implementação subjacente do token também é ainda muito simples:

O EmailBackupToken armazena um token Redis por usuário com validade de um dia. Ele é limitado ao usuário e não está vinculado a um backup específico.

Ao examinar o histórico desse arquivo, as mudanças desde 2017 parecem ter sido mecânicas/de estilo, em vez de alterações no modelo de autorização.

Isso torna indesejável simplesmente deixar o token de e-mail existente reutilizável: isso enfraqueceria a restrição original de uso único, e o próprio token não é limitado ao backup específico na URL.

Os testes de solicitação atuais estão aqui:

Eles cobrem um download inicial válido, tokens inválidos, IDs de backup inválidos/ausentes e permissões, mas não encontrei cobertura para uma transferência interrompida seguida de uma solicitação Range:.

O spec de solicitação da API também documenta o caminho comum de download de nome de arquivo + token:

Portanto, uma direção possível para o patch parece ser preservar o token de e-mail de uso único existente para iniciar o download do backup, enquanto fornece uma autorização de retomada de curta duração para o backup local já autorizado.

Eu esperaria que essa autorização de retomada fosse estritamente limitada ao usuário autenticado e ao backup específico, e permitisse uma solicitação subsequente de intervalo de bytes sem tornar o token de e-mail geral de um dia por usuário reutilizável.

Os comportamentos que eu pretendia preservar/adicionar são:

  • o GET inicial com um token de e-mail válido continua permitido;

  • outro GET completo comum usando o token de e-mail consumido continua sendo rejeitado;

  • uma repetição de intervalo de bytes para o mesmo backup pelo mesmo administrador autenticado pode ser retomada por um período limitado e curto;

  • uma tentativa de retomada contra outro backup continua sendo rejeitada;

  • uma autorização de retomada expirada continua sendo rejeitada.

Não assumi que esta seja necessariamente a implementação que os mantenedores preferirão, mas a superfície de código/testes parece bastante contida.

Estou disposto a montar um patch de rascunho e testes de regressão para a reprodução do Range: do Safari se esse modelo de segurança geral parecer razoável.

Agora criei um PR para isso:

A implementação preserva o token de e-mail de uso único existente para iniciar o download, permitindo que backups locais criem uma autorização de retomada separada e limitada, escopada para:

  • o mesmo administrador autenticado;

  • o mesmo backup;

  • o mesmo valor de token original; e

  • uma solicitação subsequente que contenha um cabeçalho Range.

Um segundo GET completo comum usando o token consumido ainda é rejeitado, assim como uma solicitação Range para outro backup. Backups remotos/S3 mantêm seu comportamento existente.

Tornei o período de retomada um enum em vez de uma duração ilimitada:

  • Disabled (Desativado)

  • 1 hour (1 hora)

  • 6 hours (recommended) (6 horas (recomendado))

  • 12 hours (12 horas)

  • Until the original email token expires (Até a expiração do token de e-mail original)

O período de retomada efetivo também é limitado à vida útil restante do token de e-mail original quando o download inicial começa.

O comportamento atual do Discourse permanece como padrão (Disabled) por compatibilidade reversa, enquanto 6 hours (6 horas) é apresentado como a opção recomendada quando os downloads retomáveis estão habilitados.

O PR pergunta se os mantenedores prefeririam reter esse padrão que preserva a compatibilidade ou fazer com que o valor de retomada recomendado se torne o padrão como parte da correção deste problema.

Também adicionei cobertura de regressão para o caminho de retomada bem-sucedido e para os principais limites de autorização, incluindo:

  • rejeição contínua do reuso comum do token;

  • rejeição contínua da retomada entre backups;

  • desativação da configuração impedindo imediatamente novas retomadas;

  • S3 não recebendo uma concessão de retomada local; e

  • o download inicial e sua retomada gerando apenas uma entrada de auditoria de download de backup.

A suíte completa de CI está passando e o PR está pronto para revisão.

Feedback sobre o modelo de segurança, e particularmente sobre a recomendação de 6 horas e a configuração padrão, seria bem-vindo.