Skip to content

feat(download): resume downloads via Range on transient failure #48

Description

@rubenhensen

Background

#47 added retry-with-backoff for chunk PUTs and download GETs. Chunk uploads benefit fully thanks to cryptify's idempotent-retry contract (encryption4all/cryptify#145): a retry replays the cached response without re-writing or double-counting. Downloads only retry the GET handshake — once the response stream starts and a transient failure interrupts it mid-read, the SDK currently has to abandon everything received and re-issue the request from byte zero.

cryptify's `FileServer` already supports HTTP Range requests out of the box, so the server side is essentially free. This issue tracks the SDK-side work to actually use it.

Proposed design

downloadFileWithRetry (src/api/cryptify.ts) is wrapped around withRetry, which today re-issues the GET on transient failure with no offset awareness. Extend it to:

  1. Track received bytes. Tee the returned ReadableStream<Uint8Array> into a counter that the retry layer can read on each chunk delivered to the consumer.
  2. Retry from received on stream-level failures. When the stream errors after some bytes have been read (network drop, idle timeout, etc.), the next attempt issues:
    GET /filedownload/{uuid}
    Range: bytes={received}-
    
    The server returns 206 Partial Content from the offset, and the SDK splices the new stream onto the consumer where the old one ended.
  3. Re-classify mid-stream errors as retriable (today the withRetry loop only sees errors thrown from fetch()/response.body; mid-stream ReadableStream errors propagate to the caller without ever entering the loop). Pipe the stream through a transform that surfaces stream errors to the retry classifier.

Out of scope

  • Cross-refresh download resume (persisting the partial-blob in IndexedDB so a refresh can pick up where the previous session left off). Could be a follow-up if it turns out useful — most downloads are short enough that in-session retry covers the common case.
  • Cryptify-side changes — `FileServer` already does the right thing on Range. Verify in passing that 206s + content-range echo back as expected, but no server work needed.

Acceptance criteria

  • Mid-stream network failure on a 100 MB download retries from the byte offset reached, not from zero
  • Consumer sees a single contiguous stream (no duplicates, no gaps) regardless of how many internal retries happened
  • Retry budget still bounded by the existing RetryOptions from feat(upload): retry chunks and downloads with exponential backoff #47
  • Caller-driven AbortSignal aborts the whole operation, including any in-flight retry GET
  • New mocked-network tests in tests/api.test.ts cover: clean download (1 attempt), mid-stream failure → resume from offset → success, persistent failure → exhausts budget then throws
  • Manual verification: against a local cryptify, kill the server mid-download → SDK resumes when cryptify comes back

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions