vite-plugin-sri4 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/dist/index.cjs +50 -101
- package/dist/index.js +50 -101
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vite-plugin-sri4
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+
[](https://www.npmjs.com/package/vite-plugin-sri4)
|
|
4
4
|
[](https://codecov.io/gh/7a6163/vite-plugin-sri4)
|
|
5
5
|

|
|
6
6
|
|
|
@@ -180,13 +180,28 @@ A blacklist would have to catch every one of the bottom rows individually, and t
|
|
|
180
180
|
|
|
181
181
|
**Pin a version in the URL.** `unpkg.com/react@18.3.1/…` answers `max-age=31536000`; `unpkg.com/react@18/…` answers `max-age=60`. Same for jsdelivr. This is the fix, not a workaround — a floating URL and an integrity attribute are contradictory by construction.
|
|
182
182
|
|
|
183
|
-
**Or vouch for the host** when you know it is stable and it just does not say so:
|
|
183
|
+
**Or vouch for the host** when you know it is stable and it just does not say so. The shape to look for is a URL that already carries a version, served by an origin that simply sends no `Cache-Control` at all:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
https://js.tappaysdk.com/sdk/tpdirect/v5.19.2
|
|
187
|
+
|
|
188
|
+
access-control-allow-origin: *
|
|
189
|
+
(no cache-control header)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The version is in the path, so those bytes are as fixed as any `immutable` response — the origin just never says so. That is what `trustDomains` is for:
|
|
184
193
|
|
|
185
194
|
```js
|
|
186
|
-
sri({ trustDomains: ['
|
|
195
|
+
sri({ trustDomains: ['js.tappaysdk.com'] })
|
|
187
196
|
```
|
|
188
197
|
|
|
189
|
-
|
|
198
|
+
**When not to use it.** `trustDomains` overrides the one check that stands between you and a hash that stops matching. Do not point it at:
|
|
199
|
+
|
|
200
|
+
- **a URL without a version in it** — `js.stripe.com/v3/`, `cdn.tailwindcss.com`, `connect.facebook.net/en_US/sdk.js`. Stripe documents that `v3/` must not be pinned; forcing a hash onto it produces a page that works until their next deploy.
|
|
201
|
+
- **a floating range** — `unpkg.com/react@18/…` resolves to whatever 18.x is current.
|
|
202
|
+
- **a host that serves per-client responses** — `fonts.googleapis.com` answers `private` for a reason.
|
|
203
|
+
|
|
204
|
+
The test is not "do I trust this vendor". It is "will these exact bytes still be at this exact URL after their next release". If the answer comes from the URL itself, `trustDomains` is right; if it comes from hope, use `bypassDomains`.
|
|
190
205
|
|
|
191
206
|
**Or accept it and silence the warning** with `bypassDomains`. Third-party analytics and widget scripts are usually this case — they are built to auto-update, and there is nothing to pin:
|
|
192
207
|
|
package/dist/index.cjs
CHANGED
|
@@ -49,20 +49,14 @@ class ResourceCache {
|
|
|
49
49
|
*/
|
|
50
50
|
class CacheManager {
|
|
51
51
|
constructor() {
|
|
52
|
-
this.urlSupportCache = new ResourceCache();
|
|
53
52
|
this.resourceCache = new ResourceCache();
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
getUrlSupportCache() {
|
|
57
|
-
return this.urlSupportCache
|
|
58
|
-
}
|
|
59
|
-
|
|
60
55
|
getResourceCache() {
|
|
61
56
|
return this.resourceCache
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
clearAll() {
|
|
65
|
-
this.urlSupportCache.clear();
|
|
66
60
|
this.resourceCache.clear();
|
|
67
61
|
}
|
|
68
62
|
}
|
|
@@ -165,35 +159,48 @@ function isImmutableResponse(cacheControl) {
|
|
|
165
159
|
/**
|
|
166
160
|
* Resource check with retry mechanism
|
|
167
161
|
*/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Fetch an external resource and return its bytes, or null if it must not be
|
|
164
|
+
* hashed. The reason is always logged - a tag that silently ships without
|
|
165
|
+
* integrity is the thing that is easy to miss.
|
|
166
|
+
*
|
|
167
|
+
* One GET, not a HEAD probe followed by a GET. The headers the gates need
|
|
168
|
+
* arrive on the response that carries the bytes anyway, so probing separately
|
|
169
|
+
* doubled the requests and threw the useful copy away - and made the plugin
|
|
170
|
+
* depend on HEAD being served at all. It often is not: js.tappaysdk.com
|
|
171
|
+
* answers 403 to HEAD and 200 to GET, which used to read as "could not be
|
|
172
|
+
* checked" on a payment SDK, exactly the kind of script SRI is for.
|
|
173
|
+
*
|
|
174
|
+
* The cost is that a rejected resource is downloaded before it is rejected.
|
|
175
|
+
* That is the right side to lose on: the accepted case, which is every build
|
|
176
|
+
* that actually ships hashes, goes from two requests to one.
|
|
177
|
+
*/
|
|
178
|
+
async function fetchVerifiedResource(url, resourceCache, logger = null, trusted = false, retries = 1) {
|
|
179
|
+
if (resourceCache.has(url)) {
|
|
180
|
+
return resourceCache.get(url)
|
|
171
181
|
}
|
|
172
182
|
|
|
183
|
+
const reject = (message) => {
|
|
184
|
+
if (logger && message) logger.warn(message);
|
|
185
|
+
resourceCache.set(url, null);
|
|
186
|
+
return null
|
|
187
|
+
};
|
|
188
|
+
|
|
173
189
|
let lastError;
|
|
174
190
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
175
191
|
try {
|
|
176
192
|
const controller = new AbortController();
|
|
177
193
|
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
178
194
|
|
|
179
|
-
const response = await fetch(url, {
|
|
180
|
-
method: 'HEAD',
|
|
181
|
-
signal: controller.signal
|
|
182
|
-
});
|
|
195
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
183
196
|
|
|
184
197
|
clearTimeout(timeoutId);
|
|
185
198
|
|
|
186
|
-
// Every path out of here that skips a resource says why. A tag that
|
|
187
|
-
// silently ships without integrity is the thing that is easy to miss.
|
|
188
199
|
if (!response.ok) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
urlSupportCache.set(url, false);
|
|
196
|
-
return false
|
|
200
|
+
return reject(
|
|
201
|
+
`Skipping SRI for ${url}: the server answered ${response.status}. ` +
|
|
202
|
+
'Add the domain to bypassDomains to silence this.'
|
|
203
|
+
)
|
|
197
204
|
}
|
|
198
205
|
|
|
199
206
|
// Only `*` can be verified at build time. Injecting integrity also means
|
|
@@ -202,16 +209,12 @@ async function checkResourceSupport(url, urlSupportCache, logger = null, trusted
|
|
|
202
209
|
// served from, that turns a working script into a blocked one.
|
|
203
210
|
const corsHeader = response.headers.get('access-control-allow-origin');
|
|
204
211
|
if (corsHeader !== '*') {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
urlSupportCache.set(url, false);
|
|
214
|
-
return false
|
|
212
|
+
return reject(
|
|
213
|
+
`Skipping SRI for ${url}: Access-Control-Allow-Origin is ` +
|
|
214
|
+
`${corsHeader ? `"${corsHeader}", not "*"` : 'absent'}, so crossorigin="anonymous" ` +
|
|
215
|
+
'cannot be verified at build time. ' +
|
|
216
|
+
'Add the domain to bypassDomains to silence this.'
|
|
217
|
+
)
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
// Reachable and CORS-eligible is not the same property as byte-stable.
|
|
@@ -228,65 +231,14 @@ async function checkResourceSupport(url, urlSupportCache, logger = null, trusted
|
|
|
228
231
|
// so a vary-based gate would let that resource straight through.
|
|
229
232
|
const cacheControl = response.headers.get('cache-control');
|
|
230
233
|
if (!trusted && !isImmutableResponse(cacheControl)) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
urlSupportCache.set(url, false);
|
|
242
|
-
return false
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
urlSupportCache.set(url, true);
|
|
246
|
-
return true
|
|
247
|
-
} catch (error) {
|
|
248
|
-
lastError = error;
|
|
249
|
-
if (error.name === 'AbortError') {
|
|
250
|
-
if (logger) {
|
|
251
|
-
logger.warn(`Resource check timed out: ${url}`);
|
|
252
|
-
}
|
|
253
|
-
break // Don't retry timeouts
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Don't wait after the last failed attempt
|
|
257
|
-
if (attempt < retries) {
|
|
258
|
-
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (logger) {
|
|
264
|
-
logger.warn(`Failed to check resource support: ${url}`, lastError);
|
|
265
|
-
}
|
|
266
|
-
urlSupportCache.set(url, false);
|
|
267
|
-
return false
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Optimized resource fetching function with retry mechanism and caching
|
|
272
|
-
*/
|
|
273
|
-
async function fetchResource(url, resourceCache, logger = null, retries = 1) {
|
|
274
|
-
// Check cache
|
|
275
|
-
if (resourceCache.has(url)) {
|
|
276
|
-
return resourceCache.get(url)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
let lastError;
|
|
280
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
281
|
-
try {
|
|
282
|
-
const controller = new AbortController();
|
|
283
|
-
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
284
|
-
|
|
285
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
286
|
-
clearTimeout(timeoutId);
|
|
287
|
-
|
|
288
|
-
if (!response.ok) {
|
|
289
|
-
throw new Error(`HTTP error! status: ${response.status}`)
|
|
234
|
+
return reject(
|
|
235
|
+
`Skipping SRI for ${url}: Cache-Control is ` +
|
|
236
|
+
`${cacheControl ? `"${cacheControl}"` : 'absent'}, so the origin does not declare ` +
|
|
237
|
+
'this URL immutable and its bytes may differ from the ones hashed here. Pin a ' +
|
|
238
|
+
'version in the URL, or add the domain to bypassDomains to accept it unprotected. ' +
|
|
239
|
+
'Only reach for trustDomains on a host you control - forcing a hash onto a ' +
|
|
240
|
+
"vendor's rolling URL ships a page that breaks on their next deploy."
|
|
241
|
+
)
|
|
290
242
|
}
|
|
291
243
|
|
|
292
244
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
@@ -295,12 +247,10 @@ async function fetchResource(url, resourceCache, logger = null, retries = 1) {
|
|
|
295
247
|
} catch (error) {
|
|
296
248
|
lastError = error;
|
|
297
249
|
if (error.name === 'AbortError') {
|
|
298
|
-
|
|
299
|
-
logger.warn(`Resource fetch timed out: ${url}`);
|
|
300
|
-
}
|
|
301
|
-
break // Don't retry timeouts
|
|
250
|
+
return reject(`Skipping SRI for ${url}: the request timed out.`)
|
|
302
251
|
}
|
|
303
252
|
|
|
253
|
+
// Don't wait after the last failed attempt
|
|
304
254
|
if (attempt < retries) {
|
|
305
255
|
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
306
256
|
}
|
|
@@ -308,8 +258,9 @@ async function fetchResource(url, resourceCache, logger = null, retries = 1) {
|
|
|
308
258
|
}
|
|
309
259
|
|
|
310
260
|
if (logger) {
|
|
311
|
-
logger.warn(`
|
|
261
|
+
logger.warn(`Skipping SRI for ${url}: the request failed.`, lastError);
|
|
312
262
|
}
|
|
263
|
+
resourceCache.set(url, null);
|
|
313
264
|
return null
|
|
314
265
|
}
|
|
315
266
|
|
|
@@ -481,11 +432,9 @@ async function calculateIntegrity(
|
|
|
481
432
|
let bundleFileName = null;
|
|
482
433
|
if (fetchUrl) {
|
|
483
434
|
const trusted = matchesDomain(fetchUrl, trustDomains, logger);
|
|
484
|
-
|
|
485
|
-
fetchUrl, cacheManager.
|
|
435
|
+
source = await fetchVerifiedResource(
|
|
436
|
+
fetchUrl, cacheManager.getResourceCache(), logger, trusted
|
|
486
437
|
);
|
|
487
|
-
if (!isSupported) return null
|
|
488
|
-
source = await fetchResource(fetchUrl, cacheManager.getResourceCache(), logger);
|
|
489
438
|
if (!source) return null
|
|
490
439
|
} else if (!ownAsset && SCHEME_RE.test(url)) {
|
|
491
440
|
// data:/blob: and unknown schemes cannot be resolved to a bundle asset
|
package/dist/index.js
CHANGED
|
@@ -45,20 +45,14 @@ class ResourceCache {
|
|
|
45
45
|
*/
|
|
46
46
|
class CacheManager {
|
|
47
47
|
constructor() {
|
|
48
|
-
this.urlSupportCache = new ResourceCache();
|
|
49
48
|
this.resourceCache = new ResourceCache();
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
getUrlSupportCache() {
|
|
53
|
-
return this.urlSupportCache
|
|
54
|
-
}
|
|
55
|
-
|
|
56
51
|
getResourceCache() {
|
|
57
52
|
return this.resourceCache
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
clearAll() {
|
|
61
|
-
this.urlSupportCache.clear();
|
|
62
56
|
this.resourceCache.clear();
|
|
63
57
|
}
|
|
64
58
|
}
|
|
@@ -161,35 +155,48 @@ function isImmutableResponse(cacheControl) {
|
|
|
161
155
|
/**
|
|
162
156
|
* Resource check with retry mechanism
|
|
163
157
|
*/
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Fetch an external resource and return its bytes, or null if it must not be
|
|
160
|
+
* hashed. The reason is always logged - a tag that silently ships without
|
|
161
|
+
* integrity is the thing that is easy to miss.
|
|
162
|
+
*
|
|
163
|
+
* One GET, not a HEAD probe followed by a GET. The headers the gates need
|
|
164
|
+
* arrive on the response that carries the bytes anyway, so probing separately
|
|
165
|
+
* doubled the requests and threw the useful copy away - and made the plugin
|
|
166
|
+
* depend on HEAD being served at all. It often is not: js.tappaysdk.com
|
|
167
|
+
* answers 403 to HEAD and 200 to GET, which used to read as "could not be
|
|
168
|
+
* checked" on a payment SDK, exactly the kind of script SRI is for.
|
|
169
|
+
*
|
|
170
|
+
* The cost is that a rejected resource is downloaded before it is rejected.
|
|
171
|
+
* That is the right side to lose on: the accepted case, which is every build
|
|
172
|
+
* that actually ships hashes, goes from two requests to one.
|
|
173
|
+
*/
|
|
174
|
+
async function fetchVerifiedResource(url, resourceCache, logger = null, trusted = false, retries = 1) {
|
|
175
|
+
if (resourceCache.has(url)) {
|
|
176
|
+
return resourceCache.get(url)
|
|
167
177
|
}
|
|
168
178
|
|
|
179
|
+
const reject = (message) => {
|
|
180
|
+
if (logger && message) logger.warn(message);
|
|
181
|
+
resourceCache.set(url, null);
|
|
182
|
+
return null
|
|
183
|
+
};
|
|
184
|
+
|
|
169
185
|
let lastError;
|
|
170
186
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
171
187
|
try {
|
|
172
188
|
const controller = new AbortController();
|
|
173
189
|
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
174
190
|
|
|
175
|
-
const response = await fetch(url, {
|
|
176
|
-
method: 'HEAD',
|
|
177
|
-
signal: controller.signal
|
|
178
|
-
});
|
|
191
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
179
192
|
|
|
180
193
|
clearTimeout(timeoutId);
|
|
181
194
|
|
|
182
|
-
// Every path out of here that skips a resource says why. A tag that
|
|
183
|
-
// silently ships without integrity is the thing that is easy to miss.
|
|
184
195
|
if (!response.ok) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
urlSupportCache.set(url, false);
|
|
192
|
-
return false
|
|
196
|
+
return reject(
|
|
197
|
+
`Skipping SRI for ${url}: the server answered ${response.status}. ` +
|
|
198
|
+
'Add the domain to bypassDomains to silence this.'
|
|
199
|
+
)
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
// Only `*` can be verified at build time. Injecting integrity also means
|
|
@@ -198,16 +205,12 @@ async function checkResourceSupport(url, urlSupportCache, logger = null, trusted
|
|
|
198
205
|
// served from, that turns a working script into a blocked one.
|
|
199
206
|
const corsHeader = response.headers.get('access-control-allow-origin');
|
|
200
207
|
if (corsHeader !== '*') {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
urlSupportCache.set(url, false);
|
|
210
|
-
return false
|
|
208
|
+
return reject(
|
|
209
|
+
`Skipping SRI for ${url}: Access-Control-Allow-Origin is ` +
|
|
210
|
+
`${corsHeader ? `"${corsHeader}", not "*"` : 'absent'}, so crossorigin="anonymous" ` +
|
|
211
|
+
'cannot be verified at build time. ' +
|
|
212
|
+
'Add the domain to bypassDomains to silence this.'
|
|
213
|
+
)
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
// Reachable and CORS-eligible is not the same property as byte-stable.
|
|
@@ -224,65 +227,14 @@ async function checkResourceSupport(url, urlSupportCache, logger = null, trusted
|
|
|
224
227
|
// so a vary-based gate would let that resource straight through.
|
|
225
228
|
const cacheControl = response.headers.get('cache-control');
|
|
226
229
|
if (!trusted && !isImmutableResponse(cacheControl)) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
urlSupportCache.set(url, false);
|
|
238
|
-
return false
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
urlSupportCache.set(url, true);
|
|
242
|
-
return true
|
|
243
|
-
} catch (error) {
|
|
244
|
-
lastError = error;
|
|
245
|
-
if (error.name === 'AbortError') {
|
|
246
|
-
if (logger) {
|
|
247
|
-
logger.warn(`Resource check timed out: ${url}`);
|
|
248
|
-
}
|
|
249
|
-
break // Don't retry timeouts
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Don't wait after the last failed attempt
|
|
253
|
-
if (attempt < retries) {
|
|
254
|
-
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (logger) {
|
|
260
|
-
logger.warn(`Failed to check resource support: ${url}`, lastError);
|
|
261
|
-
}
|
|
262
|
-
urlSupportCache.set(url, false);
|
|
263
|
-
return false
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Optimized resource fetching function with retry mechanism and caching
|
|
268
|
-
*/
|
|
269
|
-
async function fetchResource(url, resourceCache, logger = null, retries = 1) {
|
|
270
|
-
// Check cache
|
|
271
|
-
if (resourceCache.has(url)) {
|
|
272
|
-
return resourceCache.get(url)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
let lastError;
|
|
276
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
277
|
-
try {
|
|
278
|
-
const controller = new AbortController();
|
|
279
|
-
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
280
|
-
|
|
281
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
282
|
-
clearTimeout(timeoutId);
|
|
283
|
-
|
|
284
|
-
if (!response.ok) {
|
|
285
|
-
throw new Error(`HTTP error! status: ${response.status}`)
|
|
230
|
+
return reject(
|
|
231
|
+
`Skipping SRI for ${url}: Cache-Control is ` +
|
|
232
|
+
`${cacheControl ? `"${cacheControl}"` : 'absent'}, so the origin does not declare ` +
|
|
233
|
+
'this URL immutable and its bytes may differ from the ones hashed here. Pin a ' +
|
|
234
|
+
'version in the URL, or add the domain to bypassDomains to accept it unprotected. ' +
|
|
235
|
+
'Only reach for trustDomains on a host you control - forcing a hash onto a ' +
|
|
236
|
+
"vendor's rolling URL ships a page that breaks on their next deploy."
|
|
237
|
+
)
|
|
286
238
|
}
|
|
287
239
|
|
|
288
240
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
@@ -291,12 +243,10 @@ async function fetchResource(url, resourceCache, logger = null, retries = 1) {
|
|
|
291
243
|
} catch (error) {
|
|
292
244
|
lastError = error;
|
|
293
245
|
if (error.name === 'AbortError') {
|
|
294
|
-
|
|
295
|
-
logger.warn(`Resource fetch timed out: ${url}`);
|
|
296
|
-
}
|
|
297
|
-
break // Don't retry timeouts
|
|
246
|
+
return reject(`Skipping SRI for ${url}: the request timed out.`)
|
|
298
247
|
}
|
|
299
248
|
|
|
249
|
+
// Don't wait after the last failed attempt
|
|
300
250
|
if (attempt < retries) {
|
|
301
251
|
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
302
252
|
}
|
|
@@ -304,8 +254,9 @@ async function fetchResource(url, resourceCache, logger = null, retries = 1) {
|
|
|
304
254
|
}
|
|
305
255
|
|
|
306
256
|
if (logger) {
|
|
307
|
-
logger.warn(`
|
|
257
|
+
logger.warn(`Skipping SRI for ${url}: the request failed.`, lastError);
|
|
308
258
|
}
|
|
259
|
+
resourceCache.set(url, null);
|
|
309
260
|
return null
|
|
310
261
|
}
|
|
311
262
|
|
|
@@ -477,11 +428,9 @@ async function calculateIntegrity(
|
|
|
477
428
|
let bundleFileName = null;
|
|
478
429
|
if (fetchUrl) {
|
|
479
430
|
const trusted = matchesDomain(fetchUrl, trustDomains, logger);
|
|
480
|
-
|
|
481
|
-
fetchUrl, cacheManager.
|
|
431
|
+
source = await fetchVerifiedResource(
|
|
432
|
+
fetchUrl, cacheManager.getResourceCache(), logger, trusted
|
|
482
433
|
);
|
|
483
|
-
if (!isSupported) return null
|
|
484
|
-
source = await fetchResource(fetchUrl, cacheManager.getResourceCache(), logger);
|
|
485
434
|
if (!source) return null
|
|
486
435
|
} else if (!ownAsset && SCHEME_RE.test(url)) {
|
|
487
436
|
// data:/blob: and unknown schemes cannot be resolved to a bundle asset
|