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: 98b63b25aab030d4aa08be818adebead9ed77788
Choose a base ref
...
head repository: nodejs/undici
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 2d9441733c231da8b70f31c39eb08a234a42e4bf
Choose a head ref

Commits on Mar 13, 2023

  1. Fix typo in kPipelining symbol (#2005)

    Co-authored-by: Andrew Fecenko <afecenko@atlassian.com>
    andrewfecenko and andrewfecenko2 authored Mar 13, 2023
    Copy the full SHA
    f04f9fa View commit details
  2. fix(fetch): remove undefined error cause (#2006)

    We have checked on the line before that `isError` is falsy, so the cause is always set to `undefined`, which is not very useful.
    aduh95 authored Mar 13, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    a64eb26 View commit details
  3. chore(deps-dev): bump tsd from 0.25.0 to 0.27.0 (#2007)

    Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.25.0 to 0.27.0.
    - [Release notes](https://github.com/SamVerschueren/tsd/releases)
    - [Commits](tsdjs/tsd@v0.25.0...v0.27.0)
    
    ---
    updated-dependencies:
    - dependency-name: tsd
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Mar 13, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ebf0b45 View commit details
  4. build(deps-dev): bump wait-on from 6.0.1 to 7.0.1 (#1820)

    Bumps [wait-on](https://github.com/jeffbski/wait-on) from 6.0.1 to 7.0.1.
    - [Release notes](https://github.com/jeffbski/wait-on/releases)
    - [Commits](jeffbski/wait-on@v6.0.1...v7.0.1)
    
    ---
    updated-dependencies:
    - dependency-name: wait-on
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Mar 13, 2023
    Copy the full SHA
    6ec8f4e View commit details

Commits on Mar 14, 2023

  1. Copy the full SHA
    4e1e0d0 View commit details

Commits on Mar 17, 2023

  1. fix: issue 2009 (#2013)

    KhafraDev authored Mar 17, 2023
    Copy the full SHA
    6fd7477 View commit details

Commits on Mar 20, 2023

  1. build(deps-dev): bump typescript from 4.9.5 to 5.0.2 (#2018)

    Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.0.2.
    - [Release notes](https://github.com/Microsoft/TypeScript/releases)
    - [Commits](microsoft/TypeScript@v4.9.5...v5.0.2)
    
    ---
    updated-dependencies:
    - dependency-name: typescript
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Mar 20, 2023
    Copy the full SHA
    7276126 View commit details

Commits on Mar 22, 2023

  1. added descriptive error messages for URL parser (#2016)

    * added descriptive error messages
    
    Signed-off-by: Rishabh Bhandari <rishabhbhandari6@gmail.com>
    
    * updating error msgs in tests
    
    Signed-off-by: Rishabh Bhandari <rishabhbhandari6@gmail.com>
    
    * updated error messages
    
    Signed-off-by: Rishabh Bhandari <rishabhbhandari6@gmail.com>
    
    ---------
    
    Signed-off-by: Rishabh Bhandari <rishabhbhandari6@gmail.com>
    RishabhKodes authored Mar 22, 2023
    Copy the full SHA
    e6fc80f View commit details

Commits on Mar 25, 2023

  1. Copy the full SHA
    f0271d4 View commit details

Commits on Mar 28, 2023

  1. Copy the full SHA
    a9ef509 View commit details

Commits on Mar 29, 2023

  1. Copy the full SHA
    dc8d111 View commit details

Commits on Mar 31, 2023

  1. Copy the full SHA
    9013a23 View commit details

Commits on Apr 1, 2023

  1. Copy the full SHA
    7ae1779 View commit details
  2. Copy the full SHA
    549e7ed View commit details
  3. build(deps): bump github/codeql-action from 2.2.4 to 2.2.9 (#2039)

    Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.2.4 to 2.2.9.
    - [Release notes](https://github.com/github/codeql-action/releases)
    - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
    - [Commits](github/codeql-action@17573ee...04df126)
    
    ---
    updated-dependencies:
    - dependency-name: github/codeql-action
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Apr 1, 2023
    Copy the full SHA
    a6d1474 View commit details

Commits on Apr 3, 2023

  1. build(deps-dev): bump concurrently from 7.6.0 to 8.0.1 (#2041)

    Bumps [concurrently](https://github.com/open-cli-tools/concurrently) from 7.6.0 to 8.0.1.
    - [Release notes](https://github.com/open-cli-tools/concurrently/releases)
    - [Commits](open-cli-tools/concurrently@v7.6.0...v8.0.1)
    
    ---
    updated-dependencies:
    - dependency-name: concurrently
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Apr 3, 2023
    Copy the full SHA
    3d21d22 View commit details

Commits on Apr 4, 2023

  1. Small performance improvements (#2044)

    * perf: improve null and undefined checks
    
    * perf: improve formdata delete operation
    anonrig authored Apr 4, 2023
    Copy the full SHA
    5f3b8e1 View commit details
  2. fix(types): Add missing Blob import (#2047)

    This stops TypeScript from complaining:
    error TS2304: Cannot find name 'Blob'.
    dpogue authored Apr 4, 2023
    Copy the full SHA
    eceaf9a View commit details

Commits on Apr 5, 2023

  1. fix: set window option properly (#2048)

    * fix: set window option properly
    
    * Update lib/fetch/request.js
    
    Co-authored-by: Robert Nagy <ronagy@icloud.com>
    
    * Update lib/fetch/request.js
    
    Co-authored-by: Robert Nagy <ronagy@icloud.com>
    
    * add test
    
    ---------
    
    Co-authored-by: Robert Nagy <ronagy@icloud.com>
    KhafraDev and ronag authored Apr 5, 2023
    Copy the full SHA
    816dcaa View commit details

Commits on Apr 8, 2023

  1. fetch: fix leak (#2049)

    ronag authored Apr 8, 2023
    Copy the full SHA
    a1846e5 View commit details
  2. 5.21.1

    ronag committed Apr 8, 2023
    Copy the full SHA
    2d94417 View commit details
2 changes: 1 addition & 1 deletion .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -51,6 +51,6 @@ jobs:

# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
uses: github/codeql-action/upload-sarif@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9
with:
sarif_file: results.sarif
2 changes: 1 addition & 1 deletion lib/core/symbols.js
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ module.exports = {
kClient: Symbol('client'),
kParser: Symbol('parser'),
kOnDestroyed: Symbol('destroy callbacks'),
kPipelining: Symbol('pipelinig'),
kPipelining: Symbol('pipelining'),
kSocket: Symbol('socket'),
kHostHeader: Symbol('host header'),
kConnector: Symbol('connector'),
33 changes: 24 additions & 9 deletions lib/core/util.js
Original file line number Diff line number Diff line change
@@ -48,38 +48,38 @@ function parseURL (url) {
url = new URL(url)

if (!/^https?:/.test(url.origin || url.protocol)) {
throw new InvalidArgumentError('invalid protocol')
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}

return url
}

if (!url || typeof url !== 'object') {
throw new InvalidArgumentError('invalid url')
throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
}

if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
throw new InvalidArgumentError('invalid port')
throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
}

if (url.path != null && typeof url.path !== 'string') {
throw new InvalidArgumentError('invalid path')
throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
}

if (url.pathname != null && typeof url.pathname !== 'string') {
throw new InvalidArgumentError('invalid pathname')
throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
}

if (url.hostname != null && typeof url.hostname !== 'string') {
throw new InvalidArgumentError('invalid hostname')
throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
}

if (url.origin != null && typeof url.origin !== 'string') {
throw new InvalidArgumentError('invalid origin')
throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
}

if (!/^https?:/.test(url.origin || url.protocol)) {
throw new InvalidArgumentError('invalid protocol')
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}

if (!(url instanceof URL)) {
@@ -409,6 +409,21 @@ function throwIfAborted (signal) {
}
}

const hasToWellFormed = !!String.prototype.toWellFormed

/**
* @param {string} val
*/
function toUSVString (val) {
if (hasToWellFormed) {
return `${val}`.toWellFormed()
} else if (nodeUtil.toUSVString) {
return nodeUtil.toUSVString(val)
}

return `${val}`
}

const kEnumerableProperty = Object.create(null)
kEnumerableProperty.enumerable = true

@@ -418,7 +433,7 @@ module.exports = {
isDisturbed,
isErrored,
isReadable,
toUSVString: nodeUtil.toUSVString || ((val) => `${val}`),
toUSVString,
isReadableAborted,
isBlobLike,
parseOrigin,
8 changes: 7 additions & 1 deletion lib/fetch/constants.js
Original file line number Diff line number Diff line change
@@ -48,11 +48,17 @@ const requestCache = [
'only-if-cached'
]

// https://fetch.spec.whatwg.org/#request-body-header-name
const requestBodyHeader = [
'content-encoding',
'content-language',
'content-location',
'content-type'
'content-type',
// See https://github.com/nodejs/undici/issues/2021
// 'Content-Length' is a forbidden header name, which is typically
// removed in the Headers implementation. However, undici doesn't
// filter out headers, so we add it here.
'content-length'
]

// https://fetch.spec.whatwg.org/#enumdef-requestduplex
9 changes: 1 addition & 8 deletions lib/fetch/formdata.js
Original file line number Diff line number Diff line change
@@ -61,14 +61,7 @@ class FormData {

// The delete(name) method steps are to remove all entries whose name
// is name from this’s entry list.
const next = []
for (const entry of this[kState]) {
if (entry.name !== name) {
next.push(entry)
}
}

this[kState] = next
this[kState] = this[kState].filter(entry => entry.name !== name)
}

get (name) {
36 changes: 22 additions & 14 deletions lib/fetch/index.js
Original file line number Diff line number Diff line change
@@ -37,7 +37,10 @@ const {
isErrorLike,
fullyReadBody,
readableStreamClose,
isomorphicEncode
isomorphicEncode,
urlIsLocal,
urlIsHttpHttpsScheme,
urlHasHttpsScheme
} = require('./util')
const { kState, kHeaders, kGuard, kRealm, kHeadersCaseInsensitive } = require('./symbols')
const assert = require('assert')
@@ -272,7 +275,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
let cacheState = response.cacheState

// 6. If originalURL’s scheme is not an HTTP(S) scheme, then return.
if (!/^https?:/.test(originalURL.protocol)) {
if (!urlIsHttpHttpsScheme(originalURL)) {
return
}

@@ -530,10 +533,7 @@ async function mainFetch (fetchParams, recursive = false) {

// 3. If request’s local-URLs-only flag is set and request’s current URL is
// not local, then set response to a network error.
if (
request.localURLsOnly &&
!/^(about|blob|data):/.test(requestCurrentURL(request).protocol)
) {
if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) {
response = makeNetworkError('local URLs only')
}

@@ -623,7 +623,7 @@ async function mainFetch (fetchParams, recursive = false) {
}

// request’s current URL’s scheme is not an HTTP(S) scheme
if (!/^https?:/.test(requestCurrentURL(request).protocol)) {
if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) {
// Return a network error.
return makeNetworkError('URL scheme must be a HTTP(S) scheme')
}
@@ -1130,7 +1130,7 @@ async function httpRedirectFetch (fetchParams, response) {

// 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network
// error.
if (!/^https?:/.test(locationURL.protocol)) {
if (!urlIsHttpHttpsScheme(locationURL)) {
return makeNetworkError('URL scheme must be a HTTP(S) scheme')
}

@@ -1205,7 +1205,7 @@ async function httpRedirectFetch (fetchParams, response) {
// 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)
assert(request.body.source != null)
request.body = safelyExtractBody(request.body.source)[0]
}

@@ -1399,7 +1399,7 @@ async function httpNetworkOrCacheFetch (
// header if httpRequest’s header list contains that header’s name.
// TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
if (!httpRequest.headersList.contains('accept-encoding')) {
if (/^https:/.test(requestCurrentURL(httpRequest).protocol)) {
if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate')
} else {
httpRequest.headersList.append('accept-encoding', 'gzip, deflate')
@@ -1845,6 +1845,7 @@ async function httpNetworkFetch (
// 4. Set bytes to the result of handling content codings given
// codings and bytes.
let bytes
let isFailure
try {
const { done, value } = await fetchParams.controller.next()

@@ -1859,6 +1860,10 @@ async function httpNetworkFetch (
bytes = undefined
} else {
bytes = err

// err may be propagated from the result of calling readablestream.cancel,
// which might not be an error. https://github.com/nodejs/undici/issues/2009
isFailure = true
}
}

@@ -1878,7 +1883,7 @@ async function httpNetworkFetch (
timingInfo.decodedBodySize += bytes?.byteLength ?? 0

// 6. If bytes is failure, then terminate fetchParams’s controller.
if (isErrorLike(bytes)) {
if (isFailure) {
fetchParams.controller.terminate(bytes)
return
}
@@ -1979,7 +1984,9 @@ async function httpNetworkFetch (
const val = headersList[n + 1].toString('latin1')

if (key.toLowerCase() === 'content-encoding') {
codings = val.split(',').map((x) => x.trim())
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
// "All content-coding values are case-insensitive..."
codings = val.toLowerCase().split(',').map((x) => x.trim())
} else if (key.toLowerCase() === 'location') {
location = val
}
@@ -1998,9 +2005,10 @@ async function httpNetworkFetch (
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
for (const coding of codings) {
if (/(x-)?gzip/.test(coding)) {
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
if (coding === 'x-gzip' || coding === 'gzip') {
decoders.push(zlib.createGunzip())
} else if (/(x-)?deflate/.test(coding)) {
} else if (coding === 'deflate') {
decoders.push(zlib.createInflate())
} else if (coding === 'br') {
decoders.push(zlib.createBrotliDecompress())
25 changes: 18 additions & 7 deletions lib/fetch/request.js
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ const { setMaxListeners, getEventListeners, defaultMaxListeners } = require('eve
let TransformStream = globalThis.TransformStream

const kInit = Symbol('init')
const kAbortController = Symbol('abortController')

const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
signal.removeEventListener('abort', abort)
@@ -128,12 +129,12 @@ class Request {
}

// 10. If init["window"] exists and is non-null, then throw a TypeError.
if (init.window !== undefined && init.window != null) {
if (init.window != null) {
throw new TypeError(`'window' option '${window}' must be null`)
}

// 11. If init["window"] exists, then set window to "no-window".
if (init.window !== undefined) {
if ('window' in init) {
window = 'no-window'
}

@@ -354,20 +355,30 @@ class Request {
if (signal.aborted) {
ac.abort(signal.reason)
} else {
// Keep a strong ref to ac while request object
// is alive. This is needed to prevent AbortController
// from being prematurely garbage collected.
// See, https://github.com/nodejs/undici/issues/1926.
this[kAbortController] = ac

const acRef = new WeakRef(ac)
const abort = function () {
ac.abort(this.reason)
const ac = acRef.deref()
if (ac !== undefined) {
ac.abort(this.reason)
}
}

// Third-party AbortControllers may not work with these.
// See https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619
// See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619.
try {
if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
setMaxListeners(100, signal)
}
} catch {}

signal.addEventListener('abort', abort, { once: true })
requestFinalizer.register(this, { signal, abort })
requestFinalizer.register(ac, { signal, abort })
}
}

@@ -427,7 +438,7 @@ class Request {
// non-null, and request’s method is `GET` or `HEAD`, then throw a
// TypeError.
if (
((init.body !== undefined && init.body != null) || inputBody != null) &&
(init.body != null || inputBody != null) &&
(request.method === 'GET' || request.method === 'HEAD')
) {
throw new TypeError('Request with GET/HEAD method cannot have body.')
@@ -437,7 +448,7 @@ class Request {
let initBody = null

// 36. If init["body"] exists and is non-null, then:
if (init.body !== undefined && init.body != null) {
if (init.body != null) {
// 1. Let Content-Type be null.
// 2. Set initBody and Content-Type to the result of extracting
// init["body"], with keepalive set to request’s keepalive.
4 changes: 1 addition & 3 deletions lib/fetch/response.js
Original file line number Diff line number Diff line change
@@ -348,9 +348,7 @@ function makeNetworkError (reason) {
status: 0,
error: isError
? reason
: new Error(reason ? String(reason) : reason, {
cause: isError ? reason : undefined
}),
: new Error(reason ? String(reason) : reason),
aborted: reason && reason.name === 'AbortError'
})
}
44 changes: 41 additions & 3 deletions lib/fetch/util.js
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ function requestBadPort (request) {

// 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
// then return blocked.
if (/^https?:/.test(url.protocol) && badPorts.includes(url.port)) {
if (urlIsHttpHttpsScheme(url) && badPorts.includes(url.port)) {
return 'blocked'
}

@@ -285,7 +285,7 @@ function appendRequestOriginHeader (request) {
case 'strict-origin':
case 'strict-origin-when-cross-origin':
// If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
if (/^https:/.test(request.origin) && !/^https:/.test(requestCurrentURL(request))) {
if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
serializedOrigin = null
}
break
@@ -944,6 +944,41 @@ async function readAllBytes (reader, successSteps, failureSteps) {
}
}

/**
* @see https://fetch.spec.whatwg.org/#is-local
* @param {URL} url
*/
function urlIsLocal (url) {
assert('protocol' in url) // ensure it's a url object

const protocol = url.protocol

return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
}

/**
* @param {string|URL} url
*/
function urlHasHttpsScheme (url) {
if (typeof url === 'string') {
return url.startsWith('https:')
}

return url.protocol === 'https:'
}

/**
* @see https://fetch.spec.whatwg.org/#http-scheme
* @param {URL} url
*/
function urlIsHttpHttpsScheme (url) {
assert('protocol' in url) // ensure it's a url object

const protocol = url.protocol

return protocol === 'http:' || protocol === 'https:'
}

/**
* Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0.
*/
@@ -988,5 +1023,8 @@ module.exports = {
isReadableStreamLike,
readableStreamClose,
isomorphicEncode,
isomorphicDecode
isomorphicDecode,
urlIsLocal,
urlHasHttpsScheme,
urlIsHttpHttpsScheme
}
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "undici",
"version": "5.21.0",
"version": "5.21.1",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org",
"bugs": {
@@ -49,7 +49,7 @@
"test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && tsd",
"test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js",
"test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch",
"test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js && tap test/webidl/*.js)",
"test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)",
"test:jest": "node scripts/verifyVersion.js 14 || jest",
"test:tap": "tap test/*.js test/diagnostics-channel/*.js",
"test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w",
@@ -75,7 +75,7 @@
"chai-as-promised": "^7.1.1",
"chai-iterator": "^3.0.2",
"chai-string": "^1.5.0",
"concurrently": "^7.1.0",
"concurrently": "^8.0.1",
"cronometro": "^1.0.5",
"delay": "^5.0.0",
"dns-packet": "^5.4.0",
@@ -98,9 +98,9 @@
"standard": "^17.0.0",
"table": "^6.8.0",
"tap": "^16.1.0",
"tsd": "^0.25.0",
"typescript": "^4.9.5",
"wait-on": "^6.0.0",
"tsd": "^0.27.0",
"typescript": "^5.0.2",
"wait-on": "^7.0.1",
"ws": "^8.11.0"
},
"engines": {
8 changes: 4 additions & 4 deletions test/client-errors.js
Original file line number Diff line number Diff line change
@@ -260,7 +260,7 @@ test('invalid options throws', (t) => {
t.fail()
} catch (err) {
t.type(err, errors.InvalidArgumentError)
t.equal(err.message, 'invalid port')
t.equal(err.message, 'Invalid URL: port must be a valid integer or a string representation of an integer.')
}

try {
@@ -364,7 +364,7 @@ test('invalid options throws', (t) => {
t.fail()
} catch (err) {
t.type(err, errors.InvalidArgumentError)
t.equal(err.message, 'invalid protocol')
t.equal(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.')
}

try {
@@ -374,7 +374,7 @@ test('invalid options throws', (t) => {
t.fail()
} catch (err) {
t.type(err, errors.InvalidArgumentError)
t.equal(err.message, 'invalid hostname')
t.equal(err.message, 'Invalid URL hostname: the hostname must be a string or null/undefined.')
}

try {
@@ -392,7 +392,7 @@ test('invalid options throws', (t) => {
t.fail()
} catch (err) {
t.type(err, errors.InvalidArgumentError)
t.equal(err.message, 'invalid url')
t.equal(err.message, 'Invalid URL: The URL argument must be a non-null object.')
}

try {
2 changes: 1 addition & 1 deletion test/client-keep-alive.js
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ test('keep-alive header', (t) => {
body.on('end', () => {
const timeout = setTimeout(() => {
t.fail()
}, 3e3)
}, 4e3)
client.on('disconnect', () => {
t.pass()
clearTimeout(timeout)
20 changes: 20 additions & 0 deletions test/fetch/407-statuscode-window-null.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict'

const { fetch } = require('../..')
const { createServer } = require('http')
const { once } = require('events')
const { test } = require('tap')

test('Receiving a 407 status code w/ a window option present should reject', async (t) => {
const server = createServer((req, res) => {
res.statusCode = 407
res.end()
}).listen(0)

t.teardown(server.close.bind(server))
await once(server, 'listening')

// if init.window exists, the spec tells us to set request.window to 'no-window',
// which later causes the request to be rejected if the status code is 407
await t.rejects(fetch(`http://localhost:${server.address().port}`, { window: null }))
})
5 changes: 4 additions & 1 deletion test/fetch/client-fetch.js
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ const { ReadableStream } = require('stream/web')
const { Blob } = require('buffer')
const { fetch, Response, Request, FormData, File } = require('../..')
const { Client, setGlobalDispatcher, Agent } = require('../..')
const { nodeMajor, nodeMinor } = require('../../lib/core/util')
const nodeFetch = require('../../index-fetch')
const { once } = require('events')
const { gzipSync } = require('zlib')
@@ -199,7 +200,9 @@ test('multipart formdata not base64', async (t) => {
t.equal(text, 'example\ntext file')
})

test('multipart formdata base64', (t) => {
// TODO(@KhafraDev): re-enable this test once the issue is fixed
// See https://github.com/nodejs/node/issues/47301
test('multipart formdata base64', { skip: nodeMajor >= 19 && nodeMinor >= 8 }, (t) => {
t.plan(1)

// Example form data with base64 encoding
33 changes: 33 additions & 0 deletions test/fetch/encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict'

const { test } = require('tap')
const { createServer } = require('http')
const { once } = require('events')
const { fetch } = require('../..')
const { createBrotliCompress, createGzip } = require('zlib')

test('content-encoding header is case-iNsENsITIve', async (t) => {
const contentCodings = 'GZiP, bR'
const text = 'Hello, World!'

const server = createServer((req, res) => {
const gzip = createGzip()
const brotli = createBrotliCompress()

res.setHeader('Content-Encoding', contentCodings)
res.setHeader('Content-Type', 'text/plain')

brotli.pipe(gzip).pipe(res)

brotli.write(text)
brotli.end()
}).listen(0)

t.teardown(server.close.bind(server))
await once(server, 'listening')

const response = await fetch(`http://localhost:${server.address().port}`)

t.equal(await response.text(), text)
t.equal(response.headers.get('content-encoding'), contentCodings)
})
43 changes: 43 additions & 0 deletions test/fetch/fetch-leak.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict'

const { test } = require('tap')
const { fetch } = require('../..')
const { createServer } = require('http')

test('do not leak', (t) => {
t.plan(1)

const server = createServer((req, res) => {
res.end()
})
t.teardown(server.close.bind(server))

let url
let done = false
server.listen(0, function attack () {
if (done) {
return
}
url ??= new URL(`http://127.0.0.1:${server.address().port}`)
const controller = new AbortController()
fetch(url, { signal: controller.signal })
.then(res => res.arrayBuffer())
.then(attack)
})

let prev = Infinity
let count = 0
const interval = setInterval(() => {
done = true
global.gc()
const next = process.memoryUsage().heapUsed
if (next <= prev) {
t.pass()
} else if (count++ > 10) {
t.fail()
} else {
prev = next
}
}, 1e3)
t.teardown(() => clearInterval(interval))
})
28 changes: 28 additions & 0 deletions test/fetch/issue-2009.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict'

const { test } = require('tap')
const { fetch } = require('../..')
const { createServer } = require('http')
const { once } = require('events')

test('issue 2009', async (t) => {
const server = createServer((req, res) => {
res.setHeader('a', 'b')
res.flushHeaders()

res.socket.end()
}).listen(0)

t.teardown(server.close.bind(server))
await once(server, 'listening')

for (let i = 0; i < 10; i++) {
await t.resolves(
fetch(`http://localhost:${server.address().port}`).then(
async (resp) => {
await resp.body.cancel('Some message')
}
)
)
}
})
32 changes: 32 additions & 0 deletions test/fetch/issue-2021.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict'

const { test } = require('tap')
const { once } = require('events')
const { createServer } = require('http')
const { fetch } = require('../..')

// https://github.com/nodejs/undici/issues/2021
test('content-length header is removed on redirect', async (t) => {
const server = createServer((req, res) => {
if (req.url === '/redirect') {
res.writeHead(302, { Location: '/redirect2' })
res.end()
return
}

res.end()
}).listen(0).unref()

t.teardown(server.close.bind(server))
await once(server, 'listening')

const body = 'a+b+c'

await t.resolves(fetch(`http://localhost:${server.address().port}/redirect`, {
method: 'POST',
body,
headers: {
'content-length': Buffer.byteLength(body)
}
}))
})
21 changes: 21 additions & 0 deletions test/fetch/redirect.js
Original file line number Diff line number Diff line change
@@ -27,3 +27,24 @@ test('Redirecting with a body does not cancel the current request - #1776', asyn
t.equal(await resp.text(), '/redirect/')
t.ok(resp.redirected)
})

test('Redirecting with an empty body does not throw an error - #2027', async (t) => {
const server = createServer((req, res) => {
if (req.url === '/redirect') {
res.statusCode = 307
res.setHeader('location', '/redirect/')
res.write('<a href="/redirect/">Moved Permanently</a>')
res.end()
return
}
res.write(req.url)
res.end()
}).listen(0)

t.teardown(server.close.bind(server))
await once(server, 'listening')

const resp = await fetch(`http://localhost:${server.address().port}/redirect`, { method: 'PUT', body: '' })
t.equal(await resp.text(), '/redirect/')
t.ok(resp.redirected)
})
4 changes: 4 additions & 0 deletions test/wpt/runner/worker.mjs
Original file line number Diff line number Diff line change
@@ -97,6 +97,10 @@ runInThisContext(`
globalThis.Window = Object.getPrototypeOf(globalThis).constructor
`)

if (meta.title) {
runInThisContext(`globalThis.META_TITLE = "${meta.title}"`)
}

const harness = readFileSync(join(basePath, '/resources/testharness.js'), 'utf-8')
runInThisContext(harness)

1 change: 1 addition & 0 deletions types/websocket.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="node" />

import type { Blob } from 'buffer'
import type { MessagePort } from 'worker_threads'
import {
EventTarget,