Skip to content

Commit ae62c21

Browse files
committedMay 18, 2022
fix: pass integrityEmitter to cacache to avoid a redundant integrity stream
1 parent a88213e commit ae62c21

File tree

4 files changed

+101
-5
lines changed

4 files changed

+101
-5
lines changed
 

‎lib/cache/entry.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class CacheEntry {
256256
metadata: getMetadata(this.request, this.response, this.options),
257257
size,
258258
integrity: this.options.integrity,
259+
integrityEmitter: this.response.body.hasIntegrityEmitter && this.response.body,
259260
}
260261

261262
let body = null
@@ -273,10 +274,17 @@ class CacheEntry {
273274
return cacheWritePromise
274275
},
275276
}))
277+
// this is always true since if we aren't reusing the one from the remote fetch, we
278+
// are using the one from cacache
279+
body.hasIntegrityEmitter = true
276280

277281
const onResume = () => {
278282
const tee = new Minipass()
279283
const cacheStream = cacache.put.stream(this.options.cachePath, this.key, cacheOpts)
284+
// re-emit the integrity and size events on our new response body so they can be reused
285+
cacheStream.on('integrity', i => body.emit('integrity', i))
286+
cacheStream.on('size', s => body.emit('size', s))
287+
// stick a flag on here so downstream users will know if they can expect integrity events
280288
tee.pipe(cacheStream)
281289
// TODO if the cache write fails, log a warning but return the response anyway
282290
cacheStream.promise().then(cacheWriteResolve, cacheWriteReject)
@@ -320,6 +328,7 @@ class CacheEntry {
320328
// we're responding with a full cached response, so create a body
321329
// that reads from cacache and attach it to a new Response
322330
const body = new Minipass()
331+
const headers = { ...this.policy.responseHeaders() }
323332
const onResume = () => {
324333
const cacheStream = cacache.get.stream.byDigest(
325334
this.options.cachePath, this.entry.integrity, { memoize: this.options.memoize }
@@ -337,6 +346,9 @@ class CacheEntry {
337346
body.emit('error', err)
338347
cacheStream.resume()
339348
})
349+
// emit the integrity and size events based on our metadata so we're consistent
350+
body.emit('integrity', this.entry.integrity)
351+
body.emit('size', Number(headers['content-length']))
340352
cacheStream.pipe(body)
341353
}
342354

@@ -346,9 +358,7 @@ class CacheEntry {
346358
url: this.entry.metadata.url,
347359
counter: options.counter,
348360
status: 200,
349-
headers: {
350-
...this.policy.responseHeaders(),
351-
},
361+
headers,
352362
})
353363
}
354364

‎lib/remote.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ const remoteFetch = (request, options) => {
5353
// we got a 200 response and the user has specified an expected
5454
// integrity value, so wrap the response in an ssri stream to verify it
5555
const integrityStream = ssri.integrityStream({ integrity: _opts.integrity })
56-
res = new fetch.Response(new MinipassPipeline(res.body, integrityStream), res)
56+
const pipeline = new MinipassPipeline(res.body, integrityStream)
57+
// we also propagate the integrity and size events out to the pipeline so we can use
58+
// this new response body as an integrityEmitter for cacache
59+
integrityStream.on('integrity', i => pipeline.emit('integrity', i))
60+
integrityStream.on('size', s => pipeline.emit('size', s))
61+
res = new fetch.Response(pipeline, res)
62+
// set an explicit flag so we know if our response body will emit integrity and size
63+
res.body.hasIntegrityEmitter = true
5764
}
5865

5966
res.headers.set('x-fetch-attempts', attemptNum)

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"license": "ISC",
3838
"dependencies": {
3939
"agentkeepalive": "^4.2.1",
40-
"cacache": "^16.0.2",
40+
"cacache": "^16.1.0",
4141
"http-cache-semantics": "^4.1.0",
4242
"http-proxy-agent": "^5.0.0",
4343
"https-proxy-agent": "^5.0.0",

‎test/events.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use strict'
2+
3+
const events = require('events')
4+
const nock = require('nock')
5+
const ssri = require('ssri')
6+
const t = require('tap')
7+
8+
const fetch = require('../lib/index.js')
9+
10+
const CONTENT = Buffer.from('hello, world!', { encoding: 'utf8' })
11+
const HOST = 'https://make-fetch-happen.npm'
12+
13+
nock.disableNetConnect()
14+
t.beforeEach(() => {
15+
nock.cleanAll()
16+
})
17+
18+
t.test('emits integrity and size events', t => {
19+
t.test('when response is cacheable', async t => {
20+
const INTEGRITY = ssri.fromData(CONTENT)
21+
const CACHE = t.testdir()
22+
const srv = nock(HOST)
23+
.get('/test')
24+
.reply(200, CONTENT)
25+
26+
const res = await fetch(`${HOST}/test`, { cachePath: CACHE })
27+
t.equal(res.status, 200, 'successful status code')
28+
t.equal(res.headers.get('x-local-cache-status'), 'miss', 'is a cache miss')
29+
t.equal(res.body.hasIntegrityEmitter, true, 'flag is set on body')
30+
const gotIntegrity = events.once(res.body, 'integrity').then(i => i[0])
31+
const gotSize = events.once(res.body, 'size').then(s => s[0])
32+
const [integrity, size, buf] = await Promise.all([gotIntegrity, gotSize, res.buffer()])
33+
t.same(buf, CONTENT, 'request succeeded')
34+
t.same(integrity, INTEGRITY, 'got the right integrity')
35+
t.same(size, CONTENT.byteLength, 'got the right size')
36+
t.ok(srv.isDone())
37+
})
38+
39+
t.test('when expected integrity is provided', async t => {
40+
const INTEGRITY = ssri.fromData(CONTENT)
41+
const srv = nock(HOST)
42+
.get('/test')
43+
.reply(200, CONTENT)
44+
45+
const res = await fetch(`${HOST}/test`, { integrity: INTEGRITY })
46+
t.equal(res.status, 200, 'successful status code')
47+
t.notOk(res.headers.has('x-local-cache-status'), 'should not touch the cache')
48+
t.equal(res.body.hasIntegrityEmitter, true, 'flag is set on body')
49+
const gotIntegrity = events.once(res.body, 'integrity').then(i => i[0])
50+
const gotSize = events.once(res.body, 'size').then(s => s[0])
51+
const [integrity, size, buf] = await Promise.all([gotIntegrity, gotSize, res.buffer()])
52+
t.same(buf, CONTENT, 'request succeeded')
53+
t.same(integrity, INTEGRITY, 'got the right integrity')
54+
t.same(size, CONTENT.byteLength, 'got the right size')
55+
t.ok(srv.isDone())
56+
})
57+
58+
t.test('when both expected integrity is provided and response is cacheable', async t => {
59+
const INTEGRITY = ssri.fromData(CONTENT)
60+
const CACHE = t.testdir()
61+
const srv = nock(HOST)
62+
.get('/test')
63+
.reply(200, CONTENT)
64+
65+
const res = await fetch(`${HOST}/test`, { cachePath: CACHE, integrity: INTEGRITY })
66+
t.equal(res.status, 200, 'successful status code')
67+
t.equal(res.headers.get('x-local-cache-status'), 'miss', 'is a cache miss')
68+
t.equal(res.body.hasIntegrityEmitter, true, 'flag is set on body')
69+
const gotIntegrity = events.once(res.body, 'integrity').then(i => i[0])
70+
const gotSize = events.once(res.body, 'size').then(s => s[0])
71+
const [integrity, size, buf] = await Promise.all([gotIntegrity, gotSize, res.buffer()])
72+
t.same(buf, CONTENT, 'request succeeded')
73+
t.same(integrity, INTEGRITY, 'got the right integrity')
74+
t.same(size, CONTENT.byteLength, 'got the right size')
75+
t.ok(srv.isDone())
76+
})
77+
78+
t.end()
79+
})

0 commit comments

Comments
 (0)
Please sign in to comment.