Skip to content

Commit fde0ac4

Browse files
committed
Fix saferFetch retry handling (replayed from bad merge)
1 parent 723eaa0 commit fde0ac4

File tree

5 files changed

+45
-33
lines changed

5 files changed

+45
-33
lines changed

src/Asset.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,4 @@ class Asset {
158158
}
159159
}
160160

161-
module.exports = Asset;
161+
module.exports = Asset;

src/FetchTool.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const {scratchFetch} = require('./scratchFetch');
22
const saferFetch = require('./safer-fetch');
3+
const isNullResponse = require('./isNullResponse');
34

45
/**
56
* @typedef {Request & {withCredentials: boolean}} ScratchSendRequest
@@ -27,7 +28,7 @@ class FetchTool {
2728
return saferFetch(url, Object.assign({method: 'GET'}, options))
2829
.then(result => {
2930
if (result.ok) return result.arrayBuffer().then(b => new Uint8Array(b));
30-
if (result.status === 404) return null;
31+
if (isNullResponse(result)) return null;
3132
return Promise.reject(result.status); // TODO: we should throw a proper error
3233
});
3334
}
@@ -57,4 +58,4 @@ class FetchTool {
5758
}
5859
}
5960

60-
module.exports = FetchTool;
61+
module.exports = FetchTool;

src/FetchWorkerTool.worker.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-env worker */
22

3+
const isNullResponse = require('./isNullResponse');
34
const saferFetch = require('./safer-fetch');
45

56
const complete = [];
@@ -37,7 +38,7 @@ const onMessage = ({data: job}) => {
3738
saferFetch(job.url, job.options)
3839
.then(result => {
3940
if (result.ok) return result.arrayBuffer();
40-
if (result.status === 404) return null;
41+
if (isNullResponse(result)) return null;
4142
return Promise.reject(result.status);
4243
})
4344
.then(buffer => complete.push({id: job.id, buffer}))
@@ -47,4 +48,4 @@ const onMessage = ({data: job}) => {
4748

4849
// crossFetch means "fetch" is now always supported
4950
postMessage({support: {fetch: true}});
50-
self.addEventListener('message', onMessage);
51+
self.addEventListener('message', onMessage);

src/isNullResponse.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @param {Response} response the response from fetch()
3+
* @returns {boolean} true if the response is a "null response" where we successfully talked to the
4+
* source, but the source has no data for us.
5+
*/
6+
const isNullResponse = response => (
7+
// can't access, eg. due to expired/missing project token
8+
response.status === 403 ||
9+
10+
// assets does not exist
11+
// assets.scratch.mit.edu also returns 503 for missing assets
12+
response.status === 404 ||
13+
response.status === 503
14+
);
15+
16+
module.exports = isNullResponse;

src/safer-fetch.js

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,31 @@
1-
/* eslint-env browser */
21
/* eslint-disable no-use-before-define */
32

4-
// This throttles and retries fetch() to mitigate the effect of random network errors and
3+
const {scratchFetch} = require('./scratchFetch');
4+
5+
// This throttles and retries scratchFetch() to mitigate the effect of random network errors and
56
// random browser errors (especially in Chrome)
67

78
let currentFetches = 0;
89
const queue = [];
910

11+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
12+
1013
const startNextFetch = ([resolve, url, options]) => {
1114
let firstError;
1215
let failedAttempts = 0;
1316

14-
const attemptToFetch = () => fetch(url, options)
15-
.then(result => {
16-
// In a macOS WKWebView, requests from file: URLs to other file: URLs always have status: 0 and ok: false
17-
// even though the requests were successful. If the requested file doesn't exist, fetch() rejects instead.
18-
// We aren't aware of any other cases where fetch() can resolve with status 0, so this should be safe.
19-
if (result.ok || result.status === 0) return result.arrayBuffer();
20-
if (result.status === 404) return null;
21-
return Promise.reject(result.status);
22-
})
23-
.then(buffer => {
24-
currentFetches--;
25-
checkStartNextFetch();
26-
return buffer;
27-
})
17+
const done = result => {
18+
currentFetches--;
19+
checkStartNextFetch();
20+
resolve(result);
21+
};
22+
23+
const attemptToFetch = () => scratchFetch(url, options)
24+
.then(done)
2825
.catch(error => {
29-
if (error === 403) {
30-
// Retrying this request will not help, so return an error now.
31-
throw error;
32-
}
26+
// If fetch() errors, it means there was a network error of some sort.
27+
// This is worth retrying, especially as some browser will randomly fail requests
28+
// if we send too many at once (as we do).
3329

3430
console.warn(`Attempt to fetch ${url} failed`, error);
3531
if (!firstError) {
@@ -38,16 +34,14 @@ const startNextFetch = ([resolve, url, options]) => {
3834

3935
if (failedAttempts < 2) {
4036
failedAttempts++;
41-
return new Promise(cb => setTimeout(cb, (failedAttempts + Math.random() - 1) * 5000))
42-
.then(attemptToFetch);
37+
sleep((failedAttempts + Math.random() - 1) * 5000).then(attemptToFetch);
38+
return;
4339
}
4440

45-
currentFetches--;
46-
checkStartNextFetch();
47-
throw firstError;
41+
done(Promise.reject(firstError));
4842
});
4943

50-
return resolve(attemptToFetch());
44+
attemptToFetch();
5145
};
5246

5347
const checkStartNextFetch = () => {
@@ -57,9 +51,9 @@ const checkStartNextFetch = () => {
5751
}
5852
};
5953

60-
const saferFetchAsArrayBuffer = (url, options) => new Promise(resolve => {
54+
const saferFetch = (url, options) => new Promise(resolve => {
6155
queue.push([resolve, url, options]);
6256
checkStartNextFetch();
6357
});
6458

65-
module.exports = saferFetchAsArrayBuffer;
59+
module.exports = saferFetch;

0 commit comments

Comments
 (0)