Skip to content

Commit cbd61b0

Browse files
authored
Merge pull request #334 from happo/retry-signed-url
Retry S3 signed URL PUT requests
2 parents 8f8345b + 06e71e4 commit cbd61b0

File tree

2 files changed

+118
-13
lines changed

2 files changed

+118
-13
lines changed

src/uploadAssets.js

+38-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import retry from 'async-retry';
2+
13
import { logTag } from './Logger';
24
import makeRequest from './makeRequest';
35

@@ -104,20 +106,43 @@ async function uploadAssetsWithSignedUrl(
104106
return signedUrlRes.path;
105107
}
106108

107-
// Upload the assets to the signed URL using node's built-in fetch.
108-
const putRes = await fetch(signedUrlRes.signedUrl, {
109-
method: 'PUT',
110-
body: streamOrBuffer,
111-
headers: {
112-
'Content-Type': 'application/zip',
113-
},
114-
});
109+
// Upload the assets to the signed URL using node's built-in fetch with
110+
// retries
111+
await retry(
112+
async (bail) => {
113+
const res = await fetch(signedUrlRes.signedUrl, {
114+
method: 'PUT',
115+
body: streamOrBuffer,
116+
headers: {
117+
'Content-Type': 'application/zip',
118+
},
119+
});
115120

116-
if (!putRes.ok) {
117-
throw new Error(
118-
`Failed to upload assets to S3 signed URL: ${putRes.status} ${putRes.statusText}`,
119-
);
120-
}
121+
if (!res.ok) {
122+
const error = new Error(
123+
`Failed to upload assets to S3 signed URL: ${res.status} ${res.statusText}`,
124+
);
125+
126+
if (res.status < 500 || res.status >= 600) {
127+
// If it's not a 5xx error, bail immediately instead of retrying
128+
bail(error);
129+
return;
130+
}
131+
132+
throw error;
133+
}
134+
135+
return res;
136+
},
137+
{
138+
retries: 3,
139+
onRetry: (error, attempt) => {
140+
logger.warn(
141+
`${logTag(project)}PUT request attempt ${attempt} failed: ${error.message}. Retrying...`,
142+
);
143+
},
144+
},
145+
);
121146

122147
// Finally, we need to tell Happo that we've uploaded the assets.
123148
const finalizeRes = await makeRequest(

test/integrations/react-test.js

+80
Original file line numberDiff line numberDiff line change
@@ -500,4 +500,84 @@ describe('when HAPPO_SIGNED_URL is set', () => {
500500
expect(receivedRequests.length).toBe(2);
501501
});
502502
});
503+
504+
describe('when the signed URL 500s the first time', () => {
505+
beforeEach(() => {
506+
let counter = 0;
507+
mockResponses['PUT /a-signed-url'] = {
508+
method: 'PUT',
509+
url: /\/a-signed-url$/,
510+
handler: (req, res) => {
511+
counter++;
512+
if (counter === 1) {
513+
res.statusCode = 500;
514+
res.end('Internal Server Error');
515+
} else {
516+
res.statusCode = 200;
517+
res.end('OK');
518+
}
519+
},
520+
};
521+
});
522+
523+
it('retries the request and proceeds', async () => {
524+
await expect(subject()).resolves.not.toThrow();
525+
526+
expect(receivedRequests[0].method).toBe('GET');
527+
expect(receivedRequests[0].url).toMatch(/\/assets\/[a-f0-9]+\/signed-url$/);
528+
529+
expect(receivedRequests[1].method).toBe('PUT');
530+
expect(receivedRequests[1].url).toMatch(/\/a-signed-url$/);
531+
532+
expect(receivedRequests[2].method).toBe('PUT');
533+
expect(receivedRequests[2].url).toMatch(/\/a-signed-url$/);
534+
535+
expect(receivedRequests[3].method).toBe('POST');
536+
expect(receivedRequests[3].url).toMatch(
537+
/\/assets\/[a-f0-9]+\/signed-url\/finalize$/,
538+
);
539+
540+
// The request after the asset has been finalized is posting the report
541+
expect(receivedRequests[4].method).toBe('POST');
542+
expect(receivedRequests[4].url).toEqual('/api/reports/foobar');
543+
544+
expect(receivedRequests.length).toBe(5);
545+
});
546+
});
547+
548+
describe('when the signed URL 500s lots of times', () => {
549+
beforeEach(() => {
550+
mockResponses['PUT /a-signed-url'] = {
551+
method: 'PUT',
552+
url: /\/a-signed-url$/,
553+
handler: (req, res) => {
554+
res.statusCode = 500;
555+
res.end('Internal Server Error');
556+
},
557+
};
558+
});
559+
560+
it('retries the request a few times and then throws', async () => {
561+
await expect(subject()).rejects.toThrow(
562+
'Failed to upload assets to S3 signed URL',
563+
);
564+
565+
expect(receivedRequests[0].method).toBe('GET');
566+
expect(receivedRequests[0].url).toMatch(/\/assets\/[a-f0-9]+\/signed-url$/);
567+
568+
expect(receivedRequests[1].method).toBe('PUT');
569+
expect(receivedRequests[1].url).toMatch(/\/a-signed-url$/);
570+
571+
expect(receivedRequests[2].method).toBe('PUT');
572+
expect(receivedRequests[2].url).toMatch(/\/a-signed-url$/);
573+
574+
expect(receivedRequests[3].method).toBe('PUT');
575+
expect(receivedRequests[3].url).toMatch(/\/a-signed-url$/);
576+
577+
expect(receivedRequests[4].method).toBe('PUT');
578+
expect(receivedRequests[4].url).toMatch(/\/a-signed-url$/);
579+
580+
expect(receivedRequests.length).toBe(5);
581+
}, 20000);
582+
});
503583
});

0 commit comments

Comments
 (0)