Skip to content

Commit b8acdbf

Browse files
authored
Update R2_BUCKET calls to include a retry, (#48)
1 parent 5fa6983 commit b8acdbf

2 files changed

Lines changed: 85 additions & 52 deletions

File tree

src/index.ts

Lines changed: 78 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface Env {
1212
HIDE_HIDDEN_FILES?: boolean;
1313
DIRECTORY_CACHE_CONTROL?: string;
1414
LOGGING?: boolean;
15+
R2_RETRIES?: number;
1516
}
1617

1718
const units = ["B", "KB", "MB", "GB", "TB"];
@@ -161,6 +162,28 @@ ${htmlList.join("\n")}
161162
});
162163
}
163164

165+
async function retryAsync<T>(env: Env, fn: () => Promise<T>): Promise<T> {
166+
const maxAttempts = env.R2_RETRIES || 0;
167+
let attempts = 0;
168+
169+
while (maxAttempts == -1 || attempts <= maxAttempts) {
170+
try {
171+
return await fn();
172+
} catch (err) {
173+
attempts++;
174+
if (env.LOGGING) console.error(`Attempt ${attempts} failed:`, err);
175+
176+
if (attempts <= maxAttempts) {
177+
const delay = Math.min(1000 * Math.pow(2, attempts - 1), 30000);
178+
await new Promise((resolve) => setTimeout(resolve, delay));
179+
} else {
180+
throw err;
181+
}
182+
}
183+
}
184+
throw new Error("unreachable");
185+
}
186+
164187
export default {
165188
async fetch(
166189
request: Request,
@@ -230,7 +253,7 @@ export default {
230253
if (request.method === "GET") {
231254
const rangeHeader = request.headers.get("range");
232255
if (rangeHeader) {
233-
file = await env.R2_BUCKET.head(path);
256+
file = await retryAsync(env, () => env.R2_BUCKET.head(path));
234257
if (file === null)
235258
return new Response("File Not Found", { status: 404 });
236259
const parsedRanges = parseRange(file.size, rangeHeader);
@@ -282,15 +305,17 @@ export default {
282305
}
283306

284307
if (ifMatch || ifUnmodifiedSince) {
285-
file = await env.R2_BUCKET.get(path, {
286-
onlyIf: {
287-
etagMatches: ifMatch,
288-
uploadedBefore: ifUnmodifiedSince
289-
? new Date(ifUnmodifiedSince)
290-
: undefined,
291-
},
292-
range,
293-
});
308+
file = await retryAsync(env, () =>
309+
env.R2_BUCKET.get(path, {
310+
onlyIf: {
311+
etagMatches: ifMatch,
312+
uploadedBefore: ifUnmodifiedSince
313+
? new Date(ifUnmodifiedSince)
314+
: undefined,
315+
},
316+
range,
317+
})
318+
);
294319

295320
if (file && !hasBody(file)) {
296321
return new Response("Precondition Failed", { status: 412 });
@@ -300,15 +325,19 @@ export default {
300325
if (ifNoneMatch || ifModifiedSince) {
301326
// if-none-match overrides if-modified-since completely
302327
if (ifNoneMatch) {
303-
file = await env.R2_BUCKET.get(path, {
304-
onlyIf: { etagDoesNotMatch: ifNoneMatch },
305-
range,
306-
});
328+
file = await retryAsync(env, () =>
329+
env.R2_BUCKET.get(path, {
330+
onlyIf: { etagDoesNotMatch: ifNoneMatch },
331+
range,
332+
})
333+
);
307334
} else if (ifModifiedSince) {
308-
file = await env.R2_BUCKET.get(path, {
309-
onlyIf: { uploadedAfter: new Date(ifModifiedSince) },
310-
range,
311-
});
335+
file = await retryAsync(env, () =>
336+
env.R2_BUCKET.get(path, {
337+
onlyIf: { uploadedAfter: new Date(ifModifiedSince) },
338+
range,
339+
})
340+
);
312341
}
313342
if (file && !hasBody(file)) {
314343
return new Response(null, { status: 304 });
@@ -317,16 +346,16 @@ export default {
317346

318347
file =
319348
request.method === "HEAD"
320-
? await env.R2_BUCKET.head(path)
349+
? await retryAsync(env, () => env.R2_BUCKET.head(path))
321350
: file && hasBody(file)
322351
? file
323-
: await env.R2_BUCKET.get(path, { range });
352+
: await retryAsync(env, () => env.R2_BUCKET.get(path, { range }));
324353

325354
let notFound: boolean = false;
326355

327356
if (file === null) {
328357
if (env.INDEX_FILE && triedIndex) {
329-
// remove the index file since it doesnt exist
358+
// remove the index file since it doesn't exist
330359
path = path.substring(0, path.length - env.INDEX_FILE.length);
331360
}
332361

@@ -347,12 +376,12 @@ export default {
347376
path = env.NOTFOUND_FILE;
348377
file =
349378
request.method === "HEAD"
350-
? await env.R2_BUCKET.head(path)
351-
: await env.R2_BUCKET.get(path);
379+
? await retryAsync(env, () => env.R2_BUCKET.head(path))
380+
: await retryAsync(env, () => env.R2_BUCKET.get(path));
352381
}
353382

354-
// if its still null, either 404 is disabled or that file wasn't found either
355-
// this isn't an else because then there would have to be two of theem
383+
// if it's still null, either 404 is disabled or that file wasn't found either
384+
// this isn't an else because then there would have to be two of them
356385
if (file == null) {
357386
return new Response("File Not Found", { status: 404 });
358387
}
@@ -369,32 +398,30 @@ export default {
369398
file.body.pipeTo(writable);
370399
body = readable;
371400
}
372-
response = new Response(body,
373-
{
374-
status: notFound ? 404 : range ? 206 : 200,
375-
headers: {
376-
"accept-ranges": "bytes",
377-
"access-control-allow-origin": env.ALLOWED_ORIGINS || "",
378-
379-
etag: notFound ? "" : file.httpEtag,
380-
// if the 404 file has a custom cache control, we respect it
381-
"cache-control":
382-
file.httpMetadata?.cacheControl ??
383-
(notFound ? "" : env.CACHE_CONTROL || ""),
384-
expires: file.httpMetadata?.cacheExpiry?.toUTCString() ?? "",
385-
"last-modified": notFound ? "" : file.uploaded.toUTCString(),
386-
387-
"content-encoding": file.httpMetadata?.contentEncoding ?? "",
388-
"content-type":
389-
file.httpMetadata?.contentType ?? "application/octet-stream",
390-
"content-language": file.httpMetadata?.contentLanguage ?? "",
391-
"content-disposition": file.httpMetadata?.contentDisposition ?? "",
392-
"content-range":
393-
range && !notFound ? getRangeHeader(range, file.size) : "",
394-
"content-length": contentLength.toString(),
395-
},
396-
}
397-
);
401+
response = new Response(body, {
402+
status: notFound ? 404 : range ? 206 : 200,
403+
headers: {
404+
"accept-ranges": "bytes",
405+
"access-control-allow-origin": env.ALLOWED_ORIGINS || "",
406+
407+
etag: notFound ? "" : file.httpEtag,
408+
// if the 404 file has a custom cache control, we respect it
409+
"cache-control":
410+
file.httpMetadata?.cacheControl ??
411+
(notFound ? "" : env.CACHE_CONTROL || ""),
412+
expires: file.httpMetadata?.cacheExpiry?.toUTCString() ?? "",
413+
"last-modified": notFound ? "" : file.uploaded.toUTCString(),
414+
415+
"content-encoding": file.httpMetadata?.contentEncoding ?? "",
416+
"content-type":
417+
file.httpMetadata?.contentType ?? "application/octet-stream",
418+
"content-language": file.httpMetadata?.contentLanguage ?? "",
419+
"content-disposition": file.httpMetadata?.contentDisposition ?? "",
420+
"content-range":
421+
range && !notFound ? getRangeHeader(range, file.size) : "",
422+
"content-length": contentLength.toString(),
423+
},
424+
});
398425

399426
if (request.method === "GET" && !range && isCachingEnabled && !notFound)
400427
ctx.waitUntil(cache.put(request, response.clone()));

wrangler.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@ HIDE_HIDDEN_FILES = false
4545
# Set a cache header here, e.g. "max-age=86400", if you want to cache directory listings.
4646
DIRECTORY_CACHE_CONTROL = "no-store"
4747

48-
# Set debugging log enabled
48+
# Set debugging log enabled.
4949
LOGGING = true
5050

51+
# Set the number of retries allowed for each R2 operation (-1 for unlimited).
52+
R2_RETRIES = 0
53+
54+
[observability]
55+
enabled = true
56+
5157
[[r2_buckets]]
5258
binding = "R2_BUCKET"
5359
bucket_name = "kot" # Set this to your R2 bucket name. Required

0 commit comments

Comments
 (0)