@@ -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
1718const 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+
164187export 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 ( ) ) ) ;
0 commit comments