Skip to content

Commit

Permalink
deps: update undici to 5.8.1
Browse files Browse the repository at this point in the history
PR-URL: nodejs/node#44158
Reviewed-By: Mohammed Keyvanzadeh <[email protected]>
Reviewed-By: Feng Yu <[email protected]>
Reviewed-By: Tobias Nießen <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
  • Loading branch information
nodejs-github-bot authored and guangwong committed Jan 3, 2023
1 parent 40cbdbc commit b5aa0ad
Show file tree
Hide file tree
Showing 15 changed files with 195 additions and 71 deletions.
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

0 comments on commit b5aa0ad

Please sign in to comment.