이메일 토큰이 이미 사용되었기 때문에 중단된 백업 다운로드를 재개할 수 없습니다

이전 백업 다운로드 문제가 2026.01.0-latest에서 수정된 것으로 보입니다. 현재 발생하는 문제는 과거의 초기 다운로드 실패와는 별개의 문제로, 중단된 백업 다운로드를 복구하는 것과 관련된 것으로 생각됩니다.

이전 논의에서 제기된 가능성 중 하나는 일회성 백업 링크가 브라우저가 중단된 다운로드를 재개하는 것을 방해할 수 있다는 것이었습니다. 첫 번째 요청이 링크를 소모한 후, 후속 재개 요청은 더 이상 인증되지 않을 수 있습니다.

현재 Discourse에서 해당 동작을 정확히 재현할 수 있었습니다.

재현 방법

저는 다음 크기의 로컬 Discourse 백업을 다운로드 중이었습니다:

4,231,639,143 바이트

iOS의 Safari를 사용했습니다.

다운로드가 정상적으로 진행되고 있었습니다. 대략 다음과 같이 증가하는 것을 보여주는 스크린 레코딩이 있습니다:

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

313.3 MB에서 317.8 MB

그 후 저는 약 15초 동안만 Safari에서 다른 곳으로 전환했습니다.

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

따라서 초기 인증된 요청은 수 기가바이트의 전송이 실제로 완료되기 전에 이메일 토큰을 소모합니다.

그 후 전송이 중단되면, Safari는 다음과 같은 또 다른 요청을 보냅니다:

Range: bytes=319799904-

그러나 해당 요청은 /admin/backups/…를 다시 통과하며, 이메일 토큰은 이미 삭제된 상태입니다.

이는 제가 포착한 422 오류를 설명하는 것으로 보입니다.

인증된 세션과 백업 토큰 모두 여전히 필요하므로, 기존 토큰을 무기한 재사용 가능하게 만드는 것을 제안하는 것은 아닙니다.

대신, 이미 인증된 동일한 관리자가 이미 권한이 부여된 동일한 백업 다운로드를 재개할 수 있는 제한적인 방식이 존재해야 하는지 궁금합니다.

예를 들어, 동일한 사용자와 백업과 연관된 단기 다운로드 인증일 수 있으며, 유지보수 담당자가 이 문제를 처리하는 더 나은 방법이 있을 수도 있습니다.

iOS에서 재현이 쉬워지고 있는 이유

이 문제를 드러낸 중단은 최근 iOS 버전 사용자의 보고 사항과도 유사해 보입니다.

Apple 커뮤니티에 호스팅된 보고 사항이 적어도 3건 있습니다:

구 보고 사항

“Safari Background Downloads Failing After IOS 26 update” — 2025년 10월

Security Verification

해당 사용자는 iOS 26로 업그레이드 후 백그라운드에서 실행될 때 큰 Safari 다운로드가 실패한다고 보고했습니다. 또한 iPhone 15 Pro를 iOS 26으로 업그레이드하고 동일한 동작을 보고했습니다.

“Safari iOS Background Download Issue - Cancels on Minimize” — 2025년 12월

Security Verification

iOS 26.1이 설치된 iPhone 15에서 보고되었습니다. 다운로드가 정상적으로 시작되지만 다른 앱으로 전환하거나 Safari를 최소화하면 취소됩니다.

“Safari can’t complete a large download” — 2026년 4월

iOS 26.4가 설치된 iPhone 17 Pro Max에서 보고되었습니다. 큰 다운로드가 reportedly 장치가 잠금된 직후 또는 셀룰러와 Wi-Fi 사이를 전환할 때 잠시 멈춥니다. 작성자는 특히 신호가 좋으며 동일한 다운로드가 PC, Mac, Android에서는 완료된다고 구체적으로 보고했습니다.

저는 해당 커뮤니티 보고 사항만으로는 Apple이 iOS 회귀(regression)를 확인했다고 단정할 수 없다고 생각합니다.

그러나 그것들은 제 스크린 레코딩과 일치합니다: 백업이 정상적으로 다운로드되고 있었고, 저는 잠시 Safari에서 다른 곳으로 전환했으며, 돌아왔을 때 전송이 중단되어 있었습니다.

중단이 발생한 이유와 무관하게, Discourse 측의 중요한 지점은 다음과 같습니다:

큰 백업 전송이 중단됨

        ↓

브라우저가 표준 기반 Range 재개를 시도

        ↓

백업 토큰이 이미 소모됨

        ↓

재개 요청이 422 수신

서버 측 관찰 사항

이것은 현재 nginx/sendfile 경로와 관련된 일반적인 문제로 보이지 않습니다.

동일한 수 기가바이트 백업 경로는 성공적으로 완료되었습니다:

  • Firefox/Linux에서 HTTP/3를 통해; 그리고
  • 동일한 iPhone에서 Wi-Fi를 사용하여 HTTP/3를 통해.

현재 nginx도 바이트 범위 지원을 광고하고 있습니다.

따라서 HTTP/3, Caddy 또는 nginx가 본질적으로 백업을 전달할 수 없다는 것을 제안하는 것은 아닙니다.

이 특정 재현 사례에서, 원본 전송은 약 320 MB 후에 중단되었고, Safari는 이후 유효해 보이는 Range 요청을 보냈으며, 그 두 번째 요청은 Discourse에 의해 거부되었습니다.

이미 권한이 부여된 백업 다운로드가 중단된 전송 후 동일한 인증된 관리자가 동일한 파일을 재개할 수 있도록 하는 단기 메커니즘이 존재하는 것이 합리적일까요?

히스토리와 현재 테스트 커버리지를 살펴보고, 가능한 수정 사항이 무엇을 보존해야 하는지 파악했습니다.

1회용 동작은 2017년 3월의 보안 강화 변경 사항으로 거슬러 올라갑니다:

해당 커밋은 이메일로 전송되는 백업 다운로드 토큰을 도입했으며, 다운로드 요청이 승인되는 즉시 토큰을 소모하도록 했습니다.

현재 Admin::BackupsController#show에도 동일한 순서가 유지되고 있습니다:

기반이 되는 토큰 구현도 여전히 매우 단순합니다:

EmailBackupToken은 1일 만료 기간을 가진 사용자당 하나의 Redis 토큰을 저장합니다. 이는 특정 백업에 묶여 있지 않고 사용자 범위(user-scoped)로 설정되어 있습니다.

해당 파일의 히스토리를 살펴본 결과, 2017년 이후의 변경 사항은 인가 모델의 변경이 아닌 기계적/스타일 변경으로 보입니다.

따라서 기존 이메일 토큰을 재사용 가능하도록 두는 것은 바람직하지 않아 보입니다. 이는 원래의 1회용 제한을 약화시킬 뿐만 아니라, 토큰 자체가 URL의 특정 백업에 범위 지정되어 있지 않기 때문입니다.

현재 요청 테스트는 여기에 있습니다:

이 테스트들은 초기 유효한 다운로드, 잘못된 토큰, 유효하지 않거나 누락된 백업 ID 및 권한을 다루지만, 중단된 전송 후 Range: 요청에 대한 커버리지는 찾지 못했습니다.

API 요청 스펙도 마찬가지로 일반적인 파일명 + 토큰 다운로드 경로를 문서화하고 있습니다:

따라서 가능한 패치 방향은 기존 1회용 이메일 토큰을 백업 다운로드 시작에 유지하는 한편, 이미 인가된 로컬 백업에 대해 별도의 단기 재개(resume) 인가를 제공하는 것으로 보입니다.

이러한 재개 인가는 인증된 사용자와 특정 백업에 좁게 범위 지정되고, 원래의 1일 사용자 전체 이메일 토큰을 일반적으로 재사용 가능하게 만들지 않으면서 후속 바이트 범위(byte-range) 요청을 허용할 것으로 기대됩니다.

저희가 보존/추가하려는 동작은 다음과 같습니다:

  • 유효한 이메일 토큰이 포함된 초기 GET은 여전히 허용됩니다;
  • 소모된 이메일 토큰을 사용한 또 다른 일반 전체 GET은 여전히 거부됩니다;
  • 동일한 인증된 관리자가 동일한 백업에 대해 바이트 범위 재시도를 수행하면 짧은 제한된 기간 동안 재개할 수 있습니다;
  • 다른 백업에 대한 재개 시도는 여전히 거부됩니다;
  • 만료된 재개 인가는 여전히 거부됩니다.

이것이 유지보수자들이 선호할 구현 방식인지에 대해서는 가정하지 않았습니다만, 코드/테스트 범위는 비교적 제한적으로 보입니다.

이 일반적인 보안 모델이 합리적이라고 생각된다면, Safari Range: 재현을 위한 초안 패치와 회귀 테스트를 작성하는 데 기꺼이 참여하겠습니다.

이와 관련하여 PR을 작성했습니다:

이 구현은 다운로드 시작을 위해 기존에 사용되던 일회성 이메일 토큰을 유지하면서, 로컬 백업에 대해 다음 요소로 범위가 제한된 별도의 재개(resume) 인가를 생성할 수 있도록 합니다.

  • 동일한 인증된 관리자;

  • 동일한 백업;

  • 동일한 원래 토큰 값; 그리고

  • Range 헤더를 포함하는 후속 요청.

소비된 토큰을 사용하여 일반 2차 전체 GET 요청을 하는 경우와, 다른 백업에 대한 Range 요청은 여전히 거부됩니다. 원격/S3 백업은 기존 동작을 유지합니다.

재개 기간을 제한 없는 기간이 아닌 열거형(enum)으로 설정했습니다.

  • Disabled

  • 1 hour

  • 6 hours (recommended)

  • 12 hours

  • Until the original email token expires

초기 다운로드가 시작될 때, 실효 재개 기간은 원래 이메일 토큰의 남은 유효기간으로 제한됩니다.

현재 Discourse의 동작(Disabled)은 하위 호환성을 위해 기본값으로 유지되며, 재개 가능한 다운로드가 활성화되면 6 hours가 권장 옵션으로 제시됩니다.

이 PR은 유지보수자들이 이 호환성 유지 기본값을 유지하는 것을 선호하는지, 아니면 이 문제를 해결하는 과정의 일환으로 권장되는 재개 가능 값을 기본값으로 설정하는 것을 선호하는지 묻고 있습니다.

성공적인 재개 경로와 주요 인가 경계(예: 다음과 같은 경우)에 대한 회귀 테스트 커버리지도 추가했습니다.

  • 일반 토큰 재사용이 여전히 거부됨;

  • 백업 간 재개가 여전히 거부됨;

  • 설정을 비활성화하면 즉시 추가 재개가 차단됨;

  • S3에는 로컬 재개 인가가 부여되지 않음; 그리고

  • 초기 다운로드와 그 재개는 백업 다운로드 감사 항목(audit entry)을 하나만 생성함.

전체 CI 스위트가 통과하고 있으며, PR은 리뷰를 받을 준비가 되었습니다.

보안 모델, 특히 6시간 권장 사항과 기본 설정에 대한 피드백을 환영합니다.