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.13.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.14.0
Choose a head ref
  • 16 commits
  • 25 files changed
  • 7 contributors

Commits on Nov 25, 2022

  1. fetch: implement whatwg/fetch#1544 (#1780)

    * fetch: implement whatwg/fetch#1544
    
    * add wpt
    
    * chore: update url
    KhafraDev authored Nov 25, 2022
    Copy the full SHA
    4deb8af View commit details

Commits on Nov 26, 2022

  1. fix(fetch): set content-length header for FormData body (#1785)

    * fix(fetch): set content-length header for FormData body
    
    * fix: skip test on v16
    
    * fix: remove test
    
    * test: add test back
    KhafraDev authored Nov 26, 2022
    Copy the full SHA
    f38fbdc View commit details

Commits on Nov 28, 2022

  1. fix(fetch): parse multipart form data non-ascii filed names (#1786)

    Co-authored-by: surefire <surefire@cryptomile.net>
    repsac-by and surefire authored Nov 28, 2022
    Copy the full SHA
    bb84264 View commit details

Commits on Nov 29, 2022

  1. Copy the full SHA
    e49b68d View commit details
  2. build(deps-dev): bump sinon from 14.0.2 to 15.0.0 (#1788)

    Bumps [sinon](https://github.com/sinonjs/sinon) from 14.0.2 to 15.0.0.
    - [Release notes](https://github.com/sinonjs/sinon/releases)
    - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
    - [Commits](sinonjs/sinon@v14.0.2...v15.0.0)
    
    ---
    updated-dependencies:
    - dependency-name: sinon
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 29, 2022
    Copy the full SHA
    345bf70 View commit details
  3. Copy the full SHA
    ac54432 View commit details
  4. Copy the full SHA
    41d60d6 View commit details
  5. fix(fetch): send headers in the case that they were sent (#1784)

    * fix(fetch): treat headers as case sensitive
    
    * fix: mark test as flaky
    KhafraDev authored Nov 29, 2022
    Copy the full SHA
    232905f View commit details

Commits on Nov 30, 2022

  1. Copy the full SHA
    671e05f View commit details
  2. Copy the full SHA
    286ffea View commit details
  3. Copy the full SHA
    9fac791 View commit details

Commits on Dec 3, 2022

  1. Copy the full SHA
    68c269c View commit details

Commits on Dec 5, 2022

  1. build(deps-dev): bump tsd from 0.24.1 to 0.25.0 (#1797)

    Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.24.1 to 0.25.0.
    - [Release notes](https://github.com/SamVerschueren/tsd/releases)
    - [Commits](tsdjs/tsd@v0.24.1...v0.25.0)
    
    ---
    updated-dependencies:
    - dependency-name: tsd
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Dec 5, 2022
    Copy the full SHA
    cd1387a View commit details

Commits on Dec 8, 2022

  1. Copy the full SHA
    78060b6 View commit details
  2. Copy the full SHA
    e866a45 View commit details
  3. 5.14.0

    ronag committed Dec 8, 2022
    Copy the full SHA
    9f6c592 View commit details
4 changes: 3 additions & 1 deletion docs/api/Connector.md
Original file line number Diff line number Diff line change
@@ -24,8 +24,10 @@ Once you call `buildConnector`, it will return a connector function, which takes
* **hostname** `string` (required)
* **host** `string` (optional)
* **protocol** `string` (required)
* **port** `number` (required)
* **port** `string` (required)
* **servername** `string` (optional)
* **localAddress** `string | null` (optional) Local address the socket should connect from.
* **httpSocket** `Socket` (optional) Establish secure connection on a given socket rather than creating a new socket. It can only be sent on TLS update.

### Basic example

81 changes: 62 additions & 19 deletions lib/core/connect.js
Original file line number Diff line number Diff line change
@@ -4,22 +4,81 @@ const net = require('net')
const assert = require('assert')
const util = require('./util')
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')

let tls // include tls conditionally since it is not always available

// TODO: session re-use does not wait for the first
// connection to resolve the session and might therefore
// resolve the same servername multiple times even when
// re-use is enabled.

let SessionCache
if (global.FinalizationRegistry) {
SessionCache = class WeakSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
this._sessionCache = new Map()
this._sessionRegistry = new global.FinalizationRegistry((key) => {
if (this._sessionCache.size < this._maxCachedSessions) {
return
}

const ref = this._sessionCache.get(key)
if (ref !== undefined && ref.deref() === undefined) {
this._sessionCache.delete(key)
}
})
}

get (sessionKey) {
const ref = this._sessionCache.get(sessionKey)
return ref ? ref.deref() : null
}

set (sessionKey, session) {
if (this._maxCachedSessions === 0) {
return
}

this._sessionCache.set(sessionKey, new WeakRef(session))
this._sessionRegistry.register(session, sessionKey)
}
}
} else {
SessionCache = class SimpleSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
this._sessionCache = new Map()
}

get (sessionKey) {
return this._sessionCache.get(sessionKey)
}

set (sessionKey, session) {
if (this._maxCachedSessions === 0) {
return
}

if (this._sessionCache.size >= this._maxCachedSessions) {
// remove the oldest session
const { value: oldestKey } = this._sessionCache.keys().next()
this._sessionCache.delete(oldestKey)
}

this._sessionCache.set(sessionKey, session)
}
}
}

function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
}

const options = { path: socketPath, ...opts }
const sessionCache = new Map()
const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
timeout = timeout == null ? 10e3 : timeout
maxCachedSessions = maxCachedSessions == null ? 100 : maxCachedSessions

return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
let socket
@@ -47,25 +106,9 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {

socket
.on('session', function (session) {
// cache is disabled
if (maxCachedSessions === 0) {
return
}

if (sessionCache.size >= maxCachedSessions) {
// remove the oldest session
const { value: oldestKey } = sessionCache.keys().next()
sessionCache.delete(oldestKey)
}

// TODO (fix): Can a session become invalid once established? Don't think so?
sessionCache.set(sessionKey, session)
})
.on('error', function (err) {
if (sessionKey && err.code !== 'UND_ERR_INFO') {
// TODO (fix): Only delete for session related errors.
sessionCache.delete(sessionKey)
}
})
} else {
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
socket = net.connect({
45 changes: 38 additions & 7 deletions lib/fetch/body.js
Original file line number Diff line number Diff line change
@@ -7,17 +7,19 @@ const { FormData } = require('./formdata')
const { kState } = require('./symbols')
const { webidl } = require('./webidl')
const { DOMException, structuredClone } = require('./constants')
const { Blob } = require('buffer')
const { Blob, File: NativeFile } = require('buffer')
const { kBodyUsed } = require('../core/symbols')
const assert = require('assert')
const { isErrored } = require('../core/util')
const { isUint8Array, isArrayBuffer } = require('util/types')
const { File } = require('./file')
const { File: UndiciFile } = require('./file')
const { StringDecoder } = require('string_decoder')
const { parseMIMEType, serializeAMimeType } = require('./dataURL')

/** @type {globalThis['ReadableStream']} */
let ReadableStream
let ReadableStream = globalThis.ReadableStream

/** @type {globalThis['File']} */
const File = NativeFile ?? UndiciFile

// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
function extractBody (object, keepalive = false) {
@@ -142,7 +144,33 @@ function extractBody (object, keepalive = false) {
source = object

// Set length to unclear, see html/6424 for improving this.
// TODO
length = (() => {
const prefixLength = prefix.length
const boundaryLength = boundary.length
let bodyLength = 0

for (const [name, value] of object) {
if (typeof value === 'string') {
bodyLength +=
prefixLength +
Buffer.byteLength(`; name="${escape(normalizeLinefeeds(name))}"\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
} else {
bodyLength +=
prefixLength +
Buffer.byteLength(`; name="${escape(normalizeLinefeeds(name))}"` + (value.name ? `; filename="${escape(value.name)}"` : '')) +
2 + // \r\n
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`.length

// value is a Blob or File, and \r\n
bodyLength += value.size + 2
}
}

bodyLength += boundaryLength + 4 // --boundary--
return bodyLength
})()

// Set type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated
@@ -348,7 +376,10 @@ function bodyMixinMethods (instance) {
let busboy

try {
busboy = Busboy({ headers })
busboy = Busboy({
headers,
defParamCharset: 'utf8'
})
} catch (err) {
// Error due to headers:
throw Object.assign(new TypeError(), { cause: err })
@@ -361,7 +392,7 @@ function bodyMixinMethods (instance) {
const { filename, encoding, mimeType } = info
const chunks = []

if (encoding.toLowerCase() === 'base64') {
if (encoding === 'base64' || encoding.toLowerCase() === 'base64') {
let base64chunk = ''

value.on('data', (chunk) => {
15 changes: 9 additions & 6 deletions lib/fetch/file.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const { Blob } = require('buffer')
const { Blob, File: NativeFile } = require('buffer')
const { types } = require('util')
const { kState } = require('./symbols')
const { isBlobLike } = require('./util')
@@ -329,11 +329,14 @@ function convertLineEndingsNative (s) {
// rollup) will warn about circular dependencies. See:
// https://github.com/nodejs/undici/issues/1629
function isFileLike (object) {
return object instanceof File || (
object &&
(typeof object.stream === 'function' ||
typeof object.arrayBuffer === 'function') &&
object[Symbol.toStringTag] === 'File'
return (
(NativeFile && object instanceof NativeFile) ||
object instanceof File || (
object &&
(typeof object.stream === 'function' ||
typeof object.arrayBuffer === 'function') &&
object[Symbol.toStringTag] === 'File'
)
)
}

7 changes: 5 additions & 2 deletions lib/fetch/formdata.js
Original file line number Diff line number Diff line change
@@ -2,9 +2,12 @@

const { isBlobLike, toUSVString, makeIterator } = require('./util')
const { kState } = require('./symbols')
const { File, FileLike, isFileLike } = require('./file')
const { File: UndiciFile, FileLike, isFileLike } = require('./file')
const { webidl } = require('./webidl')
const { Blob } = require('buffer')
const { Blob, File: NativeFile } = require('buffer')

/** @type {globalThis['File']} */
const File = NativeFile ?? UndiciFile

// https://xhr.spec.whatwg.org/#formdata
class FormData {
32 changes: 22 additions & 10 deletions lib/fetch/headers.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
'use strict'

const { kHeadersList } = require('../core/symbols')
const { kGuard } = require('./symbols')
const { kGuard, kHeadersCaseInsensitive } = require('./symbols')
const { kEnumerableProperty } = require('../core/util')
const {
makeIterator,
@@ -96,27 +96,27 @@ class HeadersList {

// 1. If list contains name, then set name to the first such
// header’s name.
name = name.toLowerCase()
const exists = this[kHeadersMap].get(name)
const lowercaseName = name.toLowerCase()
const exists = this[kHeadersMap].get(lowercaseName)

// 2. Append (name, value) to list.
if (exists) {
this[kHeadersMap].set(name, `${exists}, ${value}`)
this[kHeadersMap].set(lowercaseName, { name: exists.name, value: `${exists.value}, ${value}` })
} else {
this[kHeadersMap].set(name, `${value}`)
this[kHeadersMap].set(lowercaseName, { name, value })
}
}

// https://fetch.spec.whatwg.org/#concept-header-list-set
set (name, value) {
this[kHeadersSortedMap] = null
name = name.toLowerCase()
const lowercaseName = name.toLowerCase()

// 1. If list contains name, then set the value of
// the first such header to value and remove the
// others.
// 2. Otherwise, append header (name, value) to list.
return this[kHeadersMap].set(name, value)
return this[kHeadersMap].set(lowercaseName, { name, value })
}

// https://fetch.spec.whatwg.org/#concept-header-list-delete
@@ -137,14 +137,26 @@ class HeadersList {
// 2. Return the values of all headers in list whose name
// is a byte-case-insensitive match for name,
// separated from each other by 0x2C 0x20, in order.
return this[kHeadersMap].get(name.toLowerCase()) ?? null
return this[kHeadersMap].get(name.toLowerCase())?.value ?? null
}

* [Symbol.iterator] () {
for (const pair of this[kHeadersMap]) {
yield pair
// use the lowercased name
for (const [name, { value }] of this[kHeadersMap]) {
yield [name, value]
}
}

get [kHeadersCaseInsensitive] () {
/** @type {string[]} */
const flatList = []

for (const { name, value } of this[kHeadersMap].values()) {
flatList.push(name, value)
}

return flatList
}
}

// https://fetch.spec.whatwg.org/#headers-class
Loading