Skip to content
Open
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
3 changes: 2 additions & 1 deletion deps/undici/src/docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
#### Parameter: `ResponseData`

* **statusCode** `number`
* **statusText** `string` - The status message from the response (e.g., "OK", "Not Found").
* **headers** `Record<string, string | string[]>` - Note that all header keys are lower-cased, e.g. `content-type`.
* **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
* **trailers** `Record<string, string>` - This object starts out
Expand Down Expand Up @@ -517,7 +518,7 @@ await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)

try {
const { body, headers, statusCode, trailers } = await client.request({
const { body, headers, statusCode, statusText, trailers } = await client.request({
path: '/',
method: 'GET'
})
Expand Down
3 changes: 2 additions & 1 deletion deps/undici/src/docs/docs/api/MockPool.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,8 @@ try {
method: 'GET'
})
} catch (error) {
console.error(error) // Error: kaboom
console.error(error) // TypeError: fetch failed
console.error(error.cause) // Error: kaboom
}
```

Expand Down
28 changes: 25 additions & 3 deletions deps/undici/src/index-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@ const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent')
const fetchImpl = require('./lib/web/fetch').fetch

function appendFetchStackTrace (err, filename) {
if (!err || typeof err !== 'object') {
return
}

const stack = typeof err.stack === 'string' ? err.stack : ''
const normalizedFilename = filename.replace(/\\/g, '/')

if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) {
return
}

const capture = {}
Error.captureStackTrace(capture, appendFetchStackTrace)

if (!capture.stack) {
return
}

const captureLines = capture.stack.split('\n').slice(1).join('\n')

err.stack = stack ? `${stack}\n${captureLines}` : capture.stack
}

module.exports.fetch = function fetch (init, options = undefined) {
return fetchImpl(init, options).catch(err => {
if (err && typeof err === 'object') {
Error.captureStackTrace(err)
}
appendFetchStackTrace(err, __filename)
throw err
})
}
Expand Down
28 changes: 25 additions & 3 deletions deps/undici/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,33 @@ module.exports.getGlobalDispatcher = getGlobalDispatcher

const fetchImpl = require('./lib/web/fetch').fetch

function appendFetchStackTrace (err, filename) {
if (!err || typeof err !== 'object') {
return
}

const stack = typeof err.stack === 'string' ? err.stack : ''
const normalizedFilename = filename.replace(/\\/g, '/')

if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) {
return
}

const capture = {}
Error.captureStackTrace(capture, appendFetchStackTrace)

if (!capture.stack) {
return
}

const captureLines = capture.stack.split('\n').slice(1).join('\n')

err.stack = stack ? `${stack}\n${captureLines}` : capture.stack
}

module.exports.fetch = function fetch (init, options = undefined) {
return fetchImpl(init, options).catch(err => {
if (err && typeof err === 'object') {
Error.captureStackTrace(err)
}
appendFetchStackTrace(err, __filename)
throw err
})
}
Expand Down
1 change: 1 addition & 0 deletions deps/undici/src/lib/api/api-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class RequestHandler extends AsyncResource {
try {
this.runInAsyncScope(callback, null, null, {
statusCode,
statusText: statusMessage,
headers,
trailers: this.trailers,
opaque,
Expand Down
9 changes: 7 additions & 2 deletions deps/undici/src/lib/dispatcher/client-h1.js
Original file line number Diff line number Diff line change
Expand Up @@ -735,8 +735,13 @@ class Parser {
}
}

function onParserTimeout (parser) {
const { socket, timeoutType, client, paused } = parser.deref()
function onParserTimeout (parserWeakRef) {
const parser = parserWeakRef.deref()
if (!parser) {
return
}

const { socket, timeoutType, client, paused } = parser

if (timeoutType === TIMEOUT_HEADERS) {
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
Expand Down
2 changes: 1 addition & 1 deletion deps/undici/src/lib/llhttp/wasm_build_env.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

> undici@7.19.2 build:wasm
> undici@7.20.0 build:wasm
> node build/wasm.js --docker

> docker run --rm --platform=linux/x86_64 --user 1001:1001 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/build/lib/llhttp --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/build,target=/home/node/build/build --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/deps,target=/home/node/build/deps -t ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970 node build/wasm.js
Expand Down
37 changes: 35 additions & 2 deletions deps/undici/src/lib/mock/mock-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,16 +312,45 @@ function mockDispatch (opts, handler) {
return true
}

// Track whether the request has been aborted
let aborted = false
let timer = null

function abort (err) {
if (aborted) {
return
}
aborted = true

// Clear the pending delayed response if any
if (timer !== null) {
clearTimeout(timer)
timer = null
}

// Notify the handler of the abort
handler.onError(err)
}

// Call onConnect to allow the handler to register the abort callback
handler.onConnect?.(abort, null)

// Handle the request with a delay if necessary
if (typeof delay === 'number' && delay > 0) {
setTimeout(() => {
timer = setTimeout(() => {
timer = null
handleReply(this[kDispatches])
}, delay)
} else {
handleReply(this[kDispatches])
}

function handleReply (mockDispatches, _data = data) {
// Don't send response if the request was aborted
if (aborted) {
return
}

// fetch's HeadersList is a 1D string array
const optsHeaders = Array.isArray(opts.headers)
? buildHeadersFromArray(opts.headers)
Expand All @@ -340,11 +369,15 @@ function mockDispatch (opts, handler) {
return body.then((newData) => handleReply(mockDispatches, newData))
}

// Check again if aborted after async body resolution
if (aborted) {
return
}

const responseData = getResponseData(body)
const responseHeaders = generateKeyValues(headers)
const responseTrailers = generateKeyValues(trailers)

handler.onConnect?.(err => handler.onError(err), null)
handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode))
handler.onData?.(Buffer.from(responseData))
handler.onComplete?.(responseTrailers)
Expand Down
82 changes: 34 additions & 48 deletions deps/undici/src/lib/web/fetch/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const { FormData, setFormDataState } = require('./formdata')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const { isErrored, isDisturbed } = require('node:stream')
const { isArrayBuffer } = require('node:util/types')
const { isUint8Array } = require('node:util/types')
const { serializeAMimeType } = require('./data-url')
const { multipartFormDataParser } = require('./formdata-parser')
const { createDeferredPromise } = require('../../util/promise')
Expand Down Expand Up @@ -45,6 +45,7 @@ const streamRegistry = new FinalizationRegistry((weakRef) => {
function extractBody (object, keepalive = false) {
// 1. Let stream be null.
let stream = null
let controller = null

// 2. If object is a ReadableStream object, then set stream to object.
if (webidl.is.ReadableStream(object)) {
Expand All @@ -57,16 +58,11 @@ function extractBody (object, keepalive = false) {
// 4. Otherwise, set stream to a new ReadableStream object, and set
// up stream with byte reading support.
stream = new ReadableStream({
pull (controller) {
const buffer = typeof source === 'string' ? textEncoder.encode(source) : source

if (buffer.byteLength) {
controller.enqueue(buffer)
}

queueMicrotask(() => readableStreamClose(controller))
pull () {},
start (c) {
controller = c
},
start () {},
cancel () {},
type: 'bytes'
})
}
Expand Down Expand Up @@ -108,9 +104,8 @@ function extractBody (object, keepalive = false) {
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
type = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (webidl.is.BufferSource(object)) {
source = isArrayBuffer(object)
? new Uint8Array(object.slice())
: new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
// Set source to a copy of the bytes held by object.
source = webidl.util.getCopyOfBytesHeldByBufferSource(object)
} else if (webidl.is.FormData(object)) {
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
Expand Down Expand Up @@ -213,45 +208,36 @@ function extractBody (object, keepalive = false) {

// 11. If source is a byte sequence, then set action to a
// step that returns source and length to source’s length.
if (typeof source === 'string' || util.isBuffer(source)) {
length = Buffer.byteLength(source)
if (typeof source === 'string' || isUint8Array(source)) {
action = () => {
length = typeof source === 'string' ? Buffer.byteLength(source) : source.length
return source
}
}

// 12. If action is non-null, then run these steps in in parallel:
// 12. If action is non-null, then run these steps in parallel:
if (action != null) {
// Run action.
let iterator
stream = new ReadableStream({
start () {
iterator = action(object)[Symbol.asyncIterator]()
},
pull (controller) {
return iterator.next().then(({ value, done }) => {
if (done) {
// When running action is done, close stream.
queueMicrotask(() => {
controller.close()
controller.byobRequest?.respond(0)
})
} else {
// Whenever one or more bytes are available and stream is not errored,
// enqueue a Uint8Array wrapping an ArrayBuffer containing the available
// bytes into stream.
if (!isErrored(stream)) {
const buffer = new Uint8Array(value)
if (buffer.byteLength) {
controller.enqueue(buffer)
}
}
;(async () => {
// 1. Run action.
const result = action()

// 2. Whenever one or more bytes are available and stream is not errored,
// enqueue the result of creating a Uint8Array from the available bytes into stream.
const iterator = result?.[Symbol.asyncIterator]?.()
if (iterator) {
for await (const bytes of iterator) {
if (isErrored(stream)) break
if (bytes.length) {
controller.enqueue(new Uint8Array(bytes))
}
return controller.desiredSize > 0
})
},
cancel (reason) {
return iterator.return()
},
type: 'bytes'
})
}
} else if (result?.length && !isErrored(stream)) {
controller.enqueue(typeof result === 'string' ? textEncoder.encode(result) : new Uint8Array(result))
}

// 3. When running action is done, close stream.
queueMicrotask(() => readableStreamClose(controller))
})()
}

// 13. Let body be a body whose stream is stream, source is source,
Expand Down
4 changes: 2 additions & 2 deletions deps/undici/src/lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1321,8 +1321,8 @@ function httpRedirectFetch (fetchParams, response) {
request.headersList.delete('host', true)
}

// 14. If requests body is non-null, then set requests body to the first return
// value of safely extracting requests bodys source.
// 14. If request's body is non-null, then set request's body to the first return
// value of safely extracting request's body's source.
if (request.body != null) {
assert(request.body.source != null)
request.body = safelyExtractBody(request.body.source)[0]
Expand Down
52 changes: 52 additions & 0 deletions deps/undici/src/lib/web/webidl/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const assert = require('node:assert')
const { types, inspect } = require('node:util')
const { runtimeFeatures } = require('../../util/runtime-features')

Expand Down Expand Up @@ -542,6 +543,57 @@ webidl.is.BufferSource = function (V) {
)
}

// https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy
webidl.util.getCopyOfBytesHeldByBufferSource = function (bufferSource) {
// 1. Let jsBufferSource be the result of converting bufferSource to a JavaScript value.
const jsBufferSource = bufferSource

// 2. Let jsArrayBuffer be jsBufferSource.
let jsArrayBuffer = jsBufferSource

// 3. Let offset be 0.
let offset = 0

// 4. Let length be 0.
let length = 0

// 5. If jsBufferSource has a [[ViewedArrayBuffer]] internal slot, then:
if (types.isTypedArray(jsBufferSource) || types.isDataView(jsBufferSource)) {
// 5.1. Set jsArrayBuffer to jsBufferSource.[[ViewedArrayBuffer]].
jsArrayBuffer = jsBufferSource.buffer

// 5.2. Set offset to jsBufferSource.[[ByteOffset]].
offset = jsBufferSource.byteOffset

// 5.3. Set length to jsBufferSource.[[ByteLength]].
length = jsBufferSource.byteLength
} else {
// 6. Otherwise:

// 6.1. Assert: jsBufferSource is an ArrayBuffer or SharedArrayBuffer object.
assert(types.isAnyArrayBuffer(jsBufferSource))

// 6.2. Set length to jsBufferSource.[[ArrayBufferByteLength]].
length = jsBufferSource.byteLength
}

// 7. If IsDetachedBuffer(jsArrayBuffer) is true, then return the empty byte sequence.
if (jsArrayBuffer.detached) {
return new Uint8Array(0)
}

// 8. Let bytes be a new byte sequence of length equal to length.
const bytes = new Uint8Array(length)

// 9. For i in the range offset to offset + length − 1, inclusive,
// set bytes[i − offset] to GetValueFromBuffer(jsArrayBuffer, i, Uint8, true, Unordered).
const view = new Uint8Array(jsArrayBuffer, offset, length)
bytes.set(view)

// 10. Return bytes.
return bytes
}

// https://webidl.spec.whatwg.org/#es-DOMString
webidl.converters.DOMString = function (V, prefix, argument, flags) {
// 1. If V is null and the conversion is to an IDL type
Expand Down
Loading
Loading