Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: nodejs/undici
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v5.8.0
Choose a base ref
...
head repository: nodejs/undici
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v5.8.1
Choose a head ref
  • 14 commits
  • 27 files changed
  • 10 contributors

Commits on Jul 18, 2022

  1. Do not decode the body while we are following a redirect (#1554)

    Ref: nodejs/node#43868
    
    Signed-off-by: Matteo Collina <hello@matteocollina.com>
    mcollina authored Jul 18, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    784c6b4 View commit details
  2. Copy the full SHA
    23f80fb View commit details
  3. Copy the full SHA
    0ab421f View commit details

Commits on Jul 19, 2022

  1. Copy the full SHA
    76f6627 View commit details
  2. Copy the full SHA
    7b25efc View commit details

Commits on Jul 20, 2022

  1. Copy the full SHA
    bd69341 View commit details
  2. Copy the full SHA
    21d0604 View commit details

Commits on Jul 21, 2022

  1. fix: add isErrorLike (#1570)

    KhafraDev authored Jul 21, 2022
    Copy the full SHA
    0ef0e26 View commit details

Commits on Jul 22, 2022

  1. fix(types): add missing pool stats (#1573)

    * fix(types): add missing pool stats
    
    * fix(test): expect type fix
    SkeLLLa authored Jul 22, 2022
    Copy the full SHA
    2944587 View commit details

Commits on Jul 25, 2022

  1. Copy the full SHA
    40af2c0 View commit details

Commits on Jul 27, 2022

  1. Copy the full SHA
    dd613ef View commit details
  2. Copy the full SHA
    1822ee6 View commit details

Commits on Jul 31, 2022

  1. Copy the full SHA
    c1dd24a View commit details

Commits on Aug 3, 2022

  1. 5.8.1

    ronag committed Aug 3, 2022
    Copy the full SHA
    e1e1638 View commit details
18 changes: 9 additions & 9 deletions 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
})
}
@@ -48,7 +48,7 @@ mockPool.intercept({
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recepient: '1234567890',
recipient: '1234567890',
amount: '100'
})
}).reply(200, {
@@ -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();
@@ -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'
@@ -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({
@@ -113,7 +113,7 @@ mockPool.intercept({
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recepient: '1234567890',
recipient: '1234567890',
amount: '100'
})
}).reply(200, (opts) => {
@@ -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'
}
2 changes: 1 addition & 1 deletion docsify/sidebar.md
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
* [MockAgent](/docs/api/MockAgent.md "Undici API - MockAgent")
* [MockErrors](/docs/api/MockErrors.md "Undici API - MockErrors")
* [API Lifecycle](/docs/api/api-lifecycle.md "Undici API - Lifecycle")
* [Diagnosthic Channel Support](/docs/api/DiagnosticChannel.md "Diagnostic Channel Support")
* [Diagnostics Channel Support](/docs/api/DiagnosticsChannel.md "Diagnostics Channel Support")
* Best Practices
* [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy")
* [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate")
33 changes: 28 additions & 5 deletions lib/core/connect.js
Original file line number Diff line number Diff line change
@@ -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
@@ -91,7 +89,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
}
})
.on('error', function (err) {
clearTimeout(timeoutId)
cancelTimeout()

if (callback) {
const cb = callback
@@ -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())
}
13 changes: 12 additions & 1 deletion lib/fetch/body.js
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion lib/fetch/dataURL.js
Original file line number Diff line number Diff line change
@@ -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
18 changes: 11 additions & 7 deletions lib/fetch/index.js
Original file line number Diff line number Diff line change
@@ -33,7 +33,8 @@ const {
isBlobLike,
sameOrigin,
isCancelled,
isAborted
isAborted,
isErrorLike
} = require('./util')
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
const assert = require('assert')
@@ -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
}
@@ -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
}))
}
}
@@ -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)
@@ -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())
@@ -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', () => {})
})

10 changes: 5 additions & 5 deletions lib/fetch/request.js
Original file line number Diff line number Diff line change
@@ -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 })
}
@@ -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 }
)
15 changes: 8 additions & 7 deletions lib/fetch/response.js
Original file line number Diff line number Diff line change
@@ -10,7 +10,8 @@ const {
isCancelled,
isAborted,
isBlobLike,
serializeJavascriptValueToJSONString
serializeJavascriptValueToJSONString,
isErrorLike
} = require('./util')
const {
redirectStatus,
@@ -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'
})
}
10 changes: 9 additions & 1 deletion lib/fetch/util.js
Original file line number Diff line number Diff line change
@@ -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
@@ -469,5 +476,6 @@ module.exports = {
makeIterator,
isValidHeaderName,
isValidHeaderValue,
hasOwn
hasOwn,
isErrorLike
}
5 changes: 3 additions & 2 deletions lib/fetch/webidl.js
Original file line number Diff line number Diff line change
@@ -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) {
@@ -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')
}

29 changes: 20 additions & 9 deletions lib/mock/mock-utils.js
Original file line number Diff line number Diff line change
@@ -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]
}
}
@@ -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) : {})
}
@@ -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)

Loading