Skip to content

Commit c6c9fe8

Browse files
committed
fix: resume pinned Drive downloads after transient failures
1 parent 644dbda commit c6c9fe8

2 files changed

Lines changed: 62 additions & 9 deletions

File tree

apps/media-server/src/__tests__/lib/media-transfer.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,42 @@ describe("bounded revision downloads", () => {
115115
expect(received).toBe(content.length);
116116
expect(calls).toBe(2);
117117
});
118+
test.each(["connection", "unavailable"])(
119+
"retains downloaded bytes across a %s failure before response headers",
120+
async (fault) => {
121+
const path = await destination();
122+
let calls = 0;
123+
let received = 0;
124+
await downloadDriveRevision(target(), path, {
125+
fetcher: fetcher((_input, init) => {
126+
calls++;
127+
if (calls === 1)
128+
return new Response(content.subarray(0, 12), {
129+
headers: { "Content-Length": String(content.length) },
130+
});
131+
expect(new Headers(init?.headers).get("range")).toBe("bytes=12-");
132+
if (calls === 2) {
133+
if (fault === "connection")
134+
throw new TypeError("Connection failed");
135+
return new Response(null, { status: 503 });
136+
}
137+
return new Response(content.subarray(12), {
138+
status: 206,
139+
headers: {
140+
"Content-Length": String(content.length - 12),
141+
"Content-Range": `bytes 12-${content.length - 1}/${content.length}`,
142+
},
143+
});
144+
}),
145+
onBytes: (bytes) => {
146+
received += bytes;
147+
},
148+
});
149+
expect(await readFile(path)).toEqual(content);
150+
expect(received).toBe(content.length);
151+
expect(calls).toBe(3);
152+
},
153+
);
118154
test("rejects corruption and removes the incomplete file", async () => {
119155
const path = await destination();
120156
await expect(

apps/media-server/src/lib/media-transfer.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,32 @@ export async function downloadDriveRevision(
219219
for (let attempt = 0; attempt < 3 && bytes < target.size; attempt++) {
220220
options.signal?.throwIfAborted();
221221
const start = bytes;
222-
const response = await fetcher(target.url, {
223-
headers: {
224-
Authorization: target.authorization,
225-
"Accept-Encoding": "identity",
226-
...(start ? { Range: `bytes=${start}-` } : {}),
227-
},
228-
signal: options.signal,
229-
redirect: "error",
230-
});
222+
let response: Response;
223+
try {
224+
response = await fetcher(target.url, {
225+
headers: {
226+
Authorization: target.authorization,
227+
"Accept-Encoding": "identity",
228+
...(start ? { Range: `bytes=${start}-` } : {}),
229+
},
230+
signal: options.signal,
231+
redirect: "error",
232+
});
233+
} catch (error) {
234+
options.signal?.throwIfAborted();
235+
if (error instanceof MediaTransferBudgetError || attempt === 2)
236+
throw error;
237+
await Bun.sleep(250 * 2 ** attempt);
238+
continue;
239+
}
240+
if (
241+
[408, 429, 500, 502, 503, 504].includes(response.status) &&
242+
attempt < 2
243+
) {
244+
await response.body?.cancel();
245+
await Bun.sleep(250 * 2 ** attempt);
246+
continue;
247+
}
231248
const valid = start
232249
? response.status === 206 &&
233250
response.headers.get("content-range") ===

0 commit comments

Comments
 (0)