Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deps: update undici to 5.8.1 #44158

Merged
merged 1 commit into from
Aug 8, 2022
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
18 changes: 9 additions & 9 deletions deps/undici/src/docs/best-practices/mocking-request.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Mocking Request

Undici have its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP request and return mocked value instead. It can be useful for testing purposes.
Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.

Example:

```js
// bank.mjs
import { request } from 'undici'

export async function bankTransfer(recepient, amount) {
export async function bankTransfer(recipient, amount) {
const { body } = await request('http://localhost:3000/bank-transfer',
{
method: 'POST',
headers: {
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recepient,
recipient,
amount
})
}
Expand Down Expand Up @@ -48,7 +48,7 @@ mockPool.intercept({
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recepient: '1234567890',
recipient: '1234567890',
amount: '100'
})
}).reply(200, {
Expand Down Expand Up @@ -77,7 +77,7 @@ Explore other MockAgent functionality [here](../api/MockAgent.md)

## Debug Mock Value

When the interceptor we wrote are not the same undici will automatically call real HTTP request. To debug our mock value use `mockAgent.disableNetConnect()`
When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`:

```js
const mockAgent = new MockAgent();
Expand All @@ -89,7 +89,7 @@ mockAgent.disableNetConnect()
const mockPool = mockAgent.get('http://localhost:3000');

mockPool.intercept({
path: '/bank-tanfer',
path: '/bank-transfer',
method: 'POST',
}).reply(200, {
message: 'transaction processed'
Expand All @@ -103,7 +103,7 @@ const badRequest = await bankTransfer('1234567890', '100')

## Reply with data based on request

If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`
If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`:

```js
mockPool.intercept({
Expand All @@ -113,7 +113,7 @@ mockPool.intercept({
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recepient: '1234567890',
recipient: '1234567890',
amount: '100'
})
}).reply(200, (opts) => {
Expand All @@ -129,7 +129,7 @@ in this case opts will be
{
method: 'POST',
headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
body: '{"recepient":"1234567890","amount":"100"}',
body: '{"recipient":"1234567890","amount":"100"}',
origin: 'http://localhost:3000',
path: '/bank-transfer'
}
Expand Down
33 changes: 28 additions & 5 deletions deps/undici/src/lib/core/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,12 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
})
}

const timeoutId = timeout
? setTimeout(onConnectTimeout, timeout, socket)
: null
const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)

socket
.setNoDelay(true)
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
clearTimeout(timeoutId)
cancelTimeout()

if (callback) {
const cb = callback
Expand All @@ -91,7 +89,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
}
})
.on('error', function (err) {
clearTimeout(timeoutId)
cancelTimeout()

if (callback) {
const cb = callback
Expand All @@ -104,6 +102,31 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
}
}

function setupTimeout (onConnectTimeout, timeout) {
if (!timeout) {
return () => {}
}

let s1 = null
let s2 = null
const timeoutId = setTimeout(() => {
// setImmediate is added to make sure that we priotorise socket error events over timeouts
s1 = setImmediate(() => {
if (process.platform === 'win32') {
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
s2 = setImmediate(() => onConnectTimeout())
} else {
onConnectTimeout()
}
})
}, timeout)
return () => {
clearTimeout(timeoutId)
clearImmediate(s1)
clearImmediate(s2)
}
}

function onConnectTimeout (socket) {
util.destroy(socket, new ConnectTimeoutError())
}
Expand Down
13 changes: 12 additions & 1 deletion deps/undici/src/lib/fetch/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,18 @@ function bodyMixinMethods (instance) {
// 1. Let entries be the result of parsing bytes.
let entries
try {
entries = new URLSearchParams(await this.text())
let text = ''
// application/x-www-form-urlencoded parser will keep the BOM.
// https://url.spec.whatwg.org/#concept-urlencoded-parser
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
for await (const chunk of consumeBody(this[kState].body)) {
if (!isUint8Array(chunk)) {
throw new TypeError('Expected Uint8Array chunk')
}
text += textDecoder.decode(chunk, { stream: true })
}
text += textDecoder.decode()
entries = new URLSearchParams(text)
} catch (err) {
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
// 2. If entries is failure, then throw a TypeError.
Expand Down
2 changes: 1 addition & 1 deletion deps/undici/src/lib/fetch/dataURL.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ function percentDecode (input) {
}

// 3. Return output.
return Uint8Array.of(...output)
return Uint8Array.from(output)
}

// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
Expand Down
18 changes: 11 additions & 7 deletions deps/undici/src/lib/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const {
isBlobLike,
sameOrigin,
isCancelled,
isAborted
isAborted,
isErrorLike
} = require('./util')
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
const assert = require('assert')
Expand Down Expand Up @@ -1854,7 +1855,7 @@ async function httpNetworkFetch (
timingInfo.decodedBodySize += bytes?.byteLength ?? 0

// 6. If bytes is failure, then terminate fetchParams’s controller.
if (bytes instanceof Error) {
if (isErrorLike(bytes)) {
fetchParams.controller.terminate(bytes)
return
}
Expand Down Expand Up @@ -1894,7 +1895,7 @@ async function httpNetworkFetch (
// 3. Otherwise, if stream is readable, error stream with a TypeError.
if (isReadable(stream)) {
fetchParams.controller.controller.error(new TypeError('terminated', {
cause: reason instanceof Error ? reason : undefined
cause: isErrorLike(reason) ? reason : undefined
}))
}
}
Expand Down Expand Up @@ -1942,14 +1943,17 @@ async function httpNetworkFetch (
}

let codings = []
let location = ''

const headers = new Headers()
for (let n = 0; n < headersList.length; n += 2) {
const key = headersList[n + 0].toString()
const val = headersList[n + 1].toString()
const key = headersList[n + 0].toString('latin1')
const val = headersList[n + 1].toString('latin1')

if (key.toLowerCase() === 'content-encoding') {
codings = val.split(',').map((x) => x.trim())
} else if (key.toLowerCase() === 'location') {
location = val
}

headers.append(key, val)
Expand All @@ -1960,7 +1964,7 @@ async function httpNetworkFetch (
const decoders = []

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status)) {
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !(request.redirect === 'follow' && location)) {
for (const coding of codings) {
if (/(x-)?gzip/.test(coding)) {
decoders.push(zlib.createGunzip())
Expand All @@ -1980,7 +1984,7 @@ async function httpNetworkFetch (
statusText,
headersList: headers[kHeadersList],
body: decoders.length
? pipeline(this.body, ...decoders, () => {})
? pipeline(this.body, ...decoders, () => { })
: this.body.on('error', () => {})
})

Expand Down
10 changes: 5 additions & 5 deletions deps/undici/src/lib/fetch/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,9 @@ class Request {
}

if (signal.aborted) {
ac.abort()
ac.abort(signal.reason)
} else {
const abort = () => ac.abort()
const abort = () => ac.abort(signal.reason)
signal.addEventListener('abort', abort, { once: true })
requestFinalizer.register(this, { signal, abort })
}
Expand Down Expand Up @@ -726,12 +726,12 @@ class Request {
// 4. Make clonedRequestObject’s signal follow this’s signal.
const ac = new AbortController()
if (this.signal.aborted) {
ac.abort()
ac.abort(this.signal.reason)
} else {
this.signal.addEventListener(
'abort',
function () {
ac.abort()
() => {
ac.abort(this.signal.reason)
},
{ once: true }
)
Expand Down
15 changes: 8 additions & 7 deletions deps/undici/src/lib/fetch/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const {
isCancelled,
isAborted,
isBlobLike,
serializeJavascriptValueToJSONString
serializeJavascriptValueToJSONString,
isErrorLike
} = require('./util')
const {
redirectStatus,
Expand Down Expand Up @@ -347,15 +348,15 @@ function makeResponse (init) {
}

function makeNetworkError (reason) {
const isError = isErrorLike(reason)
return makeResponse({
type: 'error',
status: 0,
error:
reason instanceof Error
? reason
: new Error(reason ? String(reason) : reason, {
cause: reason instanceof Error ? reason : undefined
}),
error: isError
? reason
: new Error(reason ? String(reason) : reason, {
cause: isError ? reason : undefined
}),
aborted: reason && reason.name === 'AbortError'
})
}
Expand Down
10 changes: 9 additions & 1 deletion deps/undici/src/lib/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ function isFileLike (object) {
)
}

function isErrorLike (object) {
return object instanceof Error || (
object?.constructor?.name === 'Error' ||
object?.constructor?.name === 'DOMException'
)
}

// Check whether |statusText| is a ByteString and
// matches the Reason-Phrase token production.
// RFC 2616: https://tools.ietf.org/html/rfc2616
Expand Down Expand Up @@ -469,5 +476,6 @@ module.exports = {
makeIterator,
isValidHeaderName,
isValidHeaderValue,
hasOwn
hasOwn,
isErrorLike
}
5 changes: 3 additions & 2 deletions deps/undici/src/lib/fetch/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,9 @@ webidl.converters.DOMString = function (V, opts = {}) {
return String(V)
}

// Check for 0 or more characters outside of the latin1 range.
// eslint-disable-next-line no-control-regex
const isNotLatin1 = /[^\u0000-\u00ff]/
const isLatin1 = /^[\u0000-\u00ff]{0,}$/

// https://webidl.spec.whatwg.org/#es-ByteString
webidl.converters.ByteString = function (V) {
Expand All @@ -399,7 +400,7 @@ webidl.converters.ByteString = function (V) {

// 2. If the value of any element of x is greater than
// 255, then throw a TypeError.
if (isNotLatin1.test(x)) {
if (!isLatin1.test(x)) {
throw new TypeError('Argument is not a ByteString')
}

Expand Down
29 changes: 20 additions & 9 deletions deps/undici/src/lib/mock/mock-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function lowerCaseEntries (headers) {
function getHeaderByName (headers, key) {
if (Array.isArray(headers)) {
for (let i = 0; i < headers.length; i += 2) {
if (headers[i] === key) {
if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) {
return headers[i + 1]
}
}
Expand All @@ -47,19 +47,24 @@ function getHeaderByName (headers, key) {
} else if (typeof headers.get === 'function') {
return headers.get(key)
} else {
return headers[key]
return lowerCaseEntries(headers)[key.toLocaleLowerCase()]
}
}

/** @param {string[]} headers */
function buildHeadersFromArray (headers) { // fetch HeadersList
const clone = headers.slice()
const entries = []
for (let index = 0; index < clone.length; index += 2) {
entries.push([clone[index], clone[index + 1]])
}
return Object.fromEntries(entries)
}

function matchHeaders (mockDispatch, headers) {
if (typeof mockDispatch.headers === 'function') {
if (Array.isArray(headers)) { // fetch HeadersList
const clone = headers.slice()
const entries = []
for (let index = 0; index < clone.length; index += 2) {
entries.push([clone[index], clone[index + 1]])
}
headers = Object.fromEntries(entries)
headers = buildHeadersFromArray(headers)
}
return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {})
}
Expand Down Expand Up @@ -284,7 +289,13 @@ function mockDispatch (opts, handler) {
}

function handleReply (mockDispatches) {
const responseData = getResponseData(typeof data === 'function' ? data(opts) : data)
// fetch's HeadersList is a 1D string array
const optsHeaders = Array.isArray(opts.headers)
? buildHeadersFromArray(opts.headers)
: opts.headers
const responseData = getResponseData(
typeof data === 'function' ? data({ ...opts, headers: optsHeaders }) : data
)
const responseHeaders = generateKeyValues(headers)
const responseTrailers = generateKeyValues(trailers)

Expand Down
Loading