1- import axios , { AxiosInstance , AxiosRequestConfig } from 'axios' ;
1+ import axios , {
2+ AxiosError ,
3+ AxiosInstance ,
4+ AxiosRequestConfig ,
5+ AxiosResponse ,
6+ } from 'axios' ;
27
38// Description of a part from initializeUpload()
49interface PartInfo {
@@ -36,6 +41,7 @@ export enum S3FileFieldProgressState {
3641 Sending ,
3742 Finalizing ,
3843 Done ,
44+ Retrying ,
3945}
4046
4147export interface S3FileFieldProgress {
@@ -49,7 +55,41 @@ export type S3FileFieldProgressCallback = (progress: S3FileFieldProgress) => voi
4955export interface S3FileFieldClientOptions {
5056 readonly baseUrl : string ;
5157 readonly onProgress ?: S3FileFieldProgressCallback ;
52- readonly apiConfig ?: AxiosRequestConfig
58+ readonly apiConfig ?: AxiosRequestConfig ;
59+ }
60+
61+ function sleep ( ms : number ) : Promise < void > {
62+ return new Promise ( ( resolve ) => {
63+ window . setTimeout ( ( ) => { resolve ( ) ; } , ms ) ;
64+ } ) ;
65+ }
66+
67+ function shouldRetry ( error : Error ) : boolean {
68+ // We only retry requests under certain failure modes. Namely, either
69+ // network errors, or a subset of HTTP error codes.
70+ const axiosErr = ( error as AxiosError ) ;
71+ return axiosErr . isAxiosError && (
72+ ! axiosErr . response
73+ || [ 429 , 500 , 502 , 503 , 504 ] . includes ( axiosErr . response . status )
74+ ) ;
75+ }
76+
77+ async function retry < T > (
78+ fn : ( ) => Promise < T > , onRetry : ( ) => void , condition : ( error : Error ) => boolean = shouldRetry ,
79+ interval = 5000 ,
80+ ) : Promise < T > {
81+ while ( true ) { // eslint-disable-line no-constant-condition
82+ try {
83+ return await fn ( ) ; // eslint-disable-line no-await-in-loop
84+ } catch ( error ) {
85+ if ( condition ( error ) ) {
86+ onRetry ( ) ;
87+ await sleep ( interval ) ; // eslint-disable-line no-await-in-loop
88+ } else {
89+ throw error ;
90+ }
91+ }
92+ }
5393}
5494
5595export default class S3FileFieldClient {
@@ -108,17 +148,23 @@ export default class S3FileFieldClient {
108148 let fileOffset = 0 ;
109149 for ( const part of parts ) {
110150 const chunk = file . slice ( fileOffset , fileOffset + part . size ) ;
111- // eslint-disable-next-line no-await-in-loop
112- const response = await axios . put ( part . upload_url , chunk , {
113- // eslint-disable-next-line @typescript-eslint/no-loop-func
151+ // eslint-disable-next-line @typescript-eslint/no-loop-func, no-await-in-loop
152+ const response = await retry < AxiosResponse > ( ( ) => axios . put ( part . upload_url , chunk , {
114153 onUploadProgress : ( e ) => {
115154 this . onProgress ( {
116155 uploaded : fileOffset + e . loaded ,
117156 total : file . size ,
118157 state : S3FileFieldProgressState . Sending ,
119158 } ) ;
120159 } ,
160+ } ) , ( ) => { // eslint-disable-line @typescript-eslint/no-loop-func
161+ this . onProgress ( {
162+ uploaded : fileOffset ,
163+ total : file . size ,
164+ state : S3FileFieldProgressState . Retrying ,
165+ } ) ;
121166 } ) ;
167+
122168 uploadedParts . push ( {
123169 part_number : part . part_number ,
124170 size : part . size ,
@@ -140,15 +186,18 @@ export default class S3FileFieldClient {
140186 protected async completeUpload (
141187 multipartInfo : MultipartInfo , parts : UploadedPart [ ] ,
142188 ) : Promise < void > {
143- const response = await this . api . post ( 'upload-complete/' , {
189+ const response = await retry < AxiosResponse > ( ( ) => this . api . post ( 'upload-complete/' , {
144190 upload_signature : multipartInfo . upload_signature ,
145191 upload_id : multipartInfo . upload_id ,
146192 parts,
193+ } ) , ( ) => {
194+ this . onProgress ( { state : S3FileFieldProgressState . Retrying } ) ;
147195 } ) ;
148196 const { complete_url : completeUrl , body } = response . data ;
149197
198+ // TODO support HTTP 200 error: https://github.com/girder/django-s3-file-field/issues/209
150199 // Send the CompleteMultipartUpload operation to S3
151- await axios . post ( completeUrl , body , {
200+ await retry < AxiosResponse > ( ( ) => axios . post ( completeUrl , body , {
152201 headers : {
153202 // By default, Axios sets "Content-Type: application/x-www-form-urlencoded" on POST
154203 // requests. This causes AWS's API to interpret the request body as additional parameters
@@ -157,6 +206,8 @@ export default class S3FileFieldClient {
157206 // CompleteMultipartUpload docs.
158207 'Content-Type' : null ,
159208 } ,
209+ } ) , ( ) => {
210+ this . onProgress ( { state : S3FileFieldProgressState . Retrying } ) ;
160211 } ) ;
161212 }
162213
@@ -168,8 +219,10 @@ export default class S3FileFieldClient {
168219 * @param multipartInfo Signed information returned from /upload-complete/.
169220 */
170221 protected async finalize ( multipartInfo : MultipartInfo ) : Promise < string > {
171- const response = await this . api . post ( 'finalize/' , {
222+ const response = await retry < AxiosResponse > ( ( ) => this . api . post ( 'finalize/' , {
172223 upload_signature : multipartInfo . upload_signature ,
224+ } ) , ( ) => {
225+ this . onProgress ( { state : S3FileFieldProgressState . Retrying } ) ;
173226 } ) ;
174227 return response . data . field_value ;
175228 }
0 commit comments