Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/web/cache/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,10 @@ class Cache {
}
}
},
processResponseEndOfBody (response) {
// Possible spec bug. If the body is never read, `processResponseEndOfBody` (which is attached to a TransformStream's flush hook)
// never runs, so this would hang. This hook, on the other hand, always reads the body.
// https://github.com/nodejs/undici/issues/5615
processResponseConsumeBody (response) {
// 1.
if (response.aborted) {
responsePromise.reject(new DOMException('aborted', 'AbortError'))
Expand Down
52 changes: 38 additions & 14 deletions lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1030,18 +1030,18 @@ function fetchFinale (fetchParams, response) {
// `Server-Timing` from response’s internal response’s header list.
// TODO

// 3. Let processResponseEndOfBody be the following steps:
// 3. If fetchParams’s request’s destination is "document", then set fetchParams’s controller’s
// full timing info to fetchParams’s timing info.
if (fetchParams.request.destination === 'document') {
fetchParams.controller.fullTimingInfo = timingInfo
}

// 4. Let processResponseEndOfBody be the following steps:
const processResponseEndOfBody = () => {
// 1. Let unsafeEndTime be the unsafe shared current time.
const unsafeEndTime = Date.now() // ?

// 2. If fetchParams’s request’s destination is "document", then set fetchParams’s controller’s
// full timing info to fetchParams’s timing info.
if (fetchParams.request.destination === 'document') {
fetchParams.controller.fullTimingInfo = timingInfo
}

// 3. Set fetchParams’s controller’s report timing steps to the following steps given a global object global:
// 2. Set fetchParams’s controller’s report timing steps to the following steps given a global object global:
fetchParams.controller.reportTimingSteps = () => {
// 1. If fetchParams’s request’s URL’s scheme is not an HTTP(S) scheme, then return.
if (!urlIsHttpHttpsScheme(fetchParams.request.url)) {
Expand Down Expand Up @@ -1090,7 +1090,7 @@ function fetchFinale (fetchParams, response) {
}
}

// 4. Let processResponseEndOfBodyTask be the following steps:
// 3. Let processResponseEndOfBodyTask be the following steps:
const processResponseEndOfBodyTask = () => {
// 1. Set fetchParams’s request’s done flag.
fetchParams.request.done = true
Expand All @@ -1109,11 +1109,11 @@ function fetchFinale (fetchParams, response) {
}
}

// 5. Queue a fetch task to run processResponseEndOfBodyTask with fetchParams’s task destination
// 4. Queue a fetch task to run processResponseEndOfBodyTask with fetchParams’s task destination
queueMicrotask(() => processResponseEndOfBodyTask())
}

// 4. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s
// 5. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s
// process response given response, with fetchParams’s task destination.
if (fetchParams.processResponse != null) {
queueMicrotask(() => {
Expand All @@ -1122,11 +1122,14 @@ function fetchFinale (fetchParams, response) {
})
}

// 5. Let internalResponse be response, if response is a network error; otherwise response’s internal response.
// 6. Let internalResponse be response, if response is a network error; otherwise response’s internal response.
const internalResponse = response.type === 'error' ? response : (response.internalResponse ?? response)

// 6. If internalResponse’s body is null, then run processResponseEndOfBody.
// 7. Otherwise:
// 7. If response is a network error, then run the WebDriver BiDi fetch error steps with request.
// Otherwise, run the WebDriver BiDi response completed steps with request and response.

// 8. If internalResponse’s body is null, then run processResponseEndOfBody.
// 9. Otherwise:
if (internalResponse.body == null) {
processResponseEndOfBody()
} else {
Expand All @@ -1144,6 +1147,27 @@ function fetchFinale (fetchParams, response) {
processResponseEndOfBody()
})
}

// 10. If fetchParams’s process response consume body is non-null, then:
if (fetchParams.processResponseConsumeBody != null) {
// 1. Let processBody given nullOrBytes be this step: run fetchParams’s
// process response consume body given response and nullOrBytes.
const processBody = (nullOrBytes) => fetchParams.processResponseConsumeBody(response, nullOrBytes)

// 2. Let processBodyError be this step: run fetchParams’s process
// response consume body given response and failure.
const processBodyError = () => fetchParams.processResponseConsumeBody(response, 'failure')

// 3. If internalResponse’s body is null, then queue a fetch task to run
// processBody given null, with fetchParams’s task destination.
if (internalResponse.body == null) {
queueMicrotask(() => processBody(null))
} else {
// 4. Otherwise, fully read internalResponse’s body given processBody,
// processBodyError, and fetchParams’s task destination.
fullyReadBody(internalResponse.body, processBody, processBodyError)
}
}
}

// https://fetch.spec.whatwg.org/#http-fetch
Expand Down
32 changes: 32 additions & 0 deletions test/cache/issue-5615.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict'

const { test } = require('node:test')
const { once } = require('node:events')
const { createServer } = require('node:http')
const { caches } = require('../../')
const { closeServerAsPromise } = require('../utils/node-http')

// https://github.com/nodejs/undici/issues/5615
test('cache.add and cache.addAll settle for responses with a body', async (t) => {
const server = createServer((req, res) => {
if (req.url === '/empty') { res.writeHead(204); return res.end() }
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('hello')
}).listen(0, '127.0.0.1')

t.after(closeServerAsPromise(server))
await once(server, 'listening')

const base = `http://127.0.0.1:${server.address().port}`
const cache = await caches.open('issue-5615')

t.after(async () => {
await caches.delete('issue-5615')
})

await cache.add(`${base}/body`)
await cache.addAll([`${base}/body`])
await cache.add(`${base}/empty`)

t.assert.deepStrictEqual((await cache.keys()).map(r => r.url), [`${base}/body`, `${base}/empty`])
})
Loading