1
- import axios , { AxiosInstance , AxiosRequestConfig } from 'axios' ;
1
+ import axios , {
2
+ AxiosError ,
3
+ AxiosInstance ,
4
+ AxiosRequestConfig ,
5
+ AxiosResponse ,
6
+ } from 'axios' ;
2
7
3
8
// Description of a part from initializeUpload()
4
9
interface PartInfo {
@@ -36,6 +41,7 @@ export enum S3FileFieldProgressState {
36
41
Sending ,
37
42
Finalizing ,
38
43
Done ,
44
+ Retrying ,
39
45
}
40
46
41
47
export interface S3FileFieldProgress {
@@ -49,7 +55,41 @@ export type S3FileFieldProgressCallback = (progress: S3FileFieldProgress) => voi
49
55
export interface S3FileFieldClientOptions {
50
56
readonly baseUrl : string ;
51
57
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
+ }
53
93
}
54
94
55
95
export default class S3FileFieldClient {
@@ -108,17 +148,23 @@ export default class S3FileFieldClient {
108
148
let fileOffset = 0 ;
109
149
for ( const part of parts ) {
110
150
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 , {
114
153
onUploadProgress : ( e ) => {
115
154
this . onProgress ( {
116
155
uploaded : fileOffset + e . loaded ,
117
156
total : file . size ,
118
157
state : S3FileFieldProgressState . Sending ,
119
158
} ) ;
120
159
} ,
160
+ } ) , ( ) => { // eslint-disable-line @typescript-eslint/no-loop-func
161
+ this . onProgress ( {
162
+ uploaded : fileOffset ,
163
+ total : file . size ,
164
+ state : S3FileFieldProgressState . Retrying ,
165
+ } ) ;
121
166
} ) ;
167
+
122
168
uploadedParts . push ( {
123
169
part_number : part . part_number ,
124
170
size : part . size ,
@@ -140,15 +186,18 @@ export default class S3FileFieldClient {
140
186
protected async completeUpload (
141
187
multipartInfo : MultipartInfo , parts : UploadedPart [ ] ,
142
188
) : Promise < void > {
143
- const response = await this . api . post ( 'upload-complete/' , {
189
+ const response = await retry < AxiosResponse > ( ( ) => this . api . post ( 'upload-complete/' , {
144
190
upload_signature : multipartInfo . upload_signature ,
145
191
upload_id : multipartInfo . upload_id ,
146
192
parts,
193
+ } ) , ( ) => {
194
+ this . onProgress ( { state : S3FileFieldProgressState . Retrying } ) ;
147
195
} ) ;
148
196
const { complete_url : completeUrl , body } = response . data ;
149
197
198
+ // TODO support HTTP 200 error: https://github.com/girder/django-s3-file-field/issues/209
150
199
// Send the CompleteMultipartUpload operation to S3
151
- await axios . post ( completeUrl , body , {
200
+ await retry < AxiosResponse > ( ( ) => axios . post ( completeUrl , body , {
152
201
headers : {
153
202
// By default, Axios sets "Content-Type: application/x-www-form-urlencoded" on POST
154
203
// requests. This causes AWS's API to interpret the request body as additional parameters
@@ -157,6 +206,8 @@ export default class S3FileFieldClient {
157
206
// CompleteMultipartUpload docs.
158
207
'Content-Type' : null ,
159
208
} ,
209
+ } ) , ( ) => {
210
+ this . onProgress ( { state : S3FileFieldProgressState . Retrying } ) ;
160
211
} ) ;
161
212
}
162
213
@@ -168,8 +219,10 @@ export default class S3FileFieldClient {
168
219
* @param multipartInfo Signed information returned from /upload-complete/.
169
220
*/
170
221
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/' , {
172
223
upload_signature : multipartInfo . upload_signature ,
224
+ } ) , ( ) => {
225
+ this . onProgress ( { state : S3FileFieldProgressState . Retrying } ) ;
173
226
} ) ;
174
227
return response . data . field_value ;
175
228
}
0 commit comments