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:
- Track received bytes. Tee the returned
ReadableStream<Uint8Array> into a counter that the retry layer can read on each chunk delivered to the consumer.
- 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.
- 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
Refs
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 aroundwithRetry, which today re-issues the GET on transient failure with no offset awareness. Extend it to:ReadableStream<Uint8Array>into a counter that the retry layer can read on each chunk delivered to the consumer.receivedon stream-level failures. When the stream errors after some bytes have been read (network drop, idle timeout, etc.), the next attempt issues:206 Partial Contentfrom the offset, and the SDK splices the new stream onto the consumer where the old one ended.withRetryloop only sees errors thrown fromfetch()/response.body; mid-streamReadableStreamerrors 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
Range. Verify in passing that 206s + content-range echo back as expected, but no server work needed.Acceptance criteria
RetryOptionsfrom feat(upload): retry chunks and downloads with exponential backoff #47AbortSignalaborts the whole operation, including any in-flight retry GETtests/api.test.tscover: clean download (1 attempt), mid-stream failure → resume from offset → success, persistent failure → exhausts budget then throwsRefs