From 961b76ad7cac17d23580d172702e11a080974f5d Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 23 Apr 2024 13:51:16 +0200 Subject: [PATCH 01/20] =?UTF-8?q?Expose=20EnvHttpProxyAgent=20to=20Node.js?= =?UTF-8?q?=20core=20bundle,=20so=20it=20can=20be=20turned=20=E2=80=A6=20(?= =?UTF-8?q?#3148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index-fetch.js | 7 +++++++ test/fetch/export-env-proxy-agent.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 test/fetch/export-env-proxy-agent.js diff --git a/index-fetch.js b/index-fetch.js index dc6c0d05e3e..fc8e557ce84 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -1,5 +1,7 @@ 'use strict' +const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') +const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent') const fetchImpl = require('./lib/web/fetch').fetch module.exports.fetch = function fetch (resource, init = undefined) { @@ -19,3 +21,8 @@ module.exports.WebSocket = require('./lib/web/websocket/websocket').WebSocket module.exports.MessageEvent = require('./lib/web/websocket/events').MessageEvent module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource + +// Expose the fetch implementation to be enabled in Node.js core via a flag +module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent +module.exports.getGlobalDispatcher = getGlobalDispatcher +module.exports.setGlobalDispatcher = setGlobalDispatcher diff --git a/test/fetch/export-env-proxy-agent.js b/test/fetch/export-env-proxy-agent.js new file mode 100644 index 00000000000..933a6bb530d --- /dev/null +++ b/test/fetch/export-env-proxy-agent.js @@ -0,0 +1,15 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') +const undiciFetch = require('../../undici-fetch') + +test('EnvHttpProxyAgent should be part of Node.js bundle', () => { + assert.strictEqual(typeof undiciFetch.EnvHttpProxyAgent, 'function') + assert.strictEqual(typeof undiciFetch.getGlobalDispatcher, 'function') + assert.strictEqual(typeof undiciFetch.setGlobalDispatcher, 'function') + + const agent = new undiciFetch.EnvHttpProxyAgent() + undiciFetch.setGlobalDispatcher(agent) + assert.strictEqual(undiciFetch.getGlobalDispatcher(), agent) +}) From 179237d1243da20a723b5a804be89154f39ca7fd Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:55:19 +0900 Subject: [PATCH 02/20] test: add headerslist copy check (#3156) * test: add headerslist copy check * Apply suggestions from code review * Update request.js * Update test/fetch/request.js --- test/fetch/request.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/fetch/request.js b/test/fetch/request.js index f02fd2405e5..4e007da2b19 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -449,7 +449,7 @@ test('set-cookie headers get cleared when passing a Request as first param', () assert.deepStrictEqual([...req1.headers], [['set-cookie', 'a=1']]) const req2 = new Request(req1, { headers: {} }) - + assert.deepStrictEqual([...req1.headers], [['set-cookie', 'a=1']]) assert.deepStrictEqual([...req2.headers], []) assert.deepStrictEqual(req2.headers.getSetCookie(), []) }) @@ -465,12 +465,13 @@ test('request.referrer', () => { // https://github.com/nodejs/undici/issues/2445 test('Clone the set-cookie header when Request is passed as the first parameter and no header is passed.', (t) => { - const { strictEqual } = tspl(t, { plan: 2 }) const request = new Request('http://localhost', { headers: { 'set-cookie': 'A' } }) const request2 = new Request(request) + assert.deepStrictEqual([...request.headers], [['set-cookie', 'A']]) request2.headers.append('set-cookie', 'B') - strictEqual(request.headers.getSetCookie().join(', '), request.headers.get('set-cookie')) - strictEqual(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie')) + assert.deepStrictEqual([...request.headers], [['set-cookie', 'A']]) + assert.strictEqual(request.headers.getSetCookie().join(', '), request.headers.get('set-cookie')) + assert.strictEqual(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie')) }) // Tests for optimization introduced in https://github.com/nodejs/undici/pull/2456 From 8dd32ef7717983f0bd8acc509d2bf6962736afdf Mon Sep 17 00:00:00 2001 From: Matt Weber Date: Wed, 24 Apr 2024 05:55:14 -0400 Subject: [PATCH 03/20] chore: ensure automated v6 release compared to v6 (#3149) --- .github/workflows/release.yml | 4 +++- scripts/release.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb21a930754..b645eb5a89f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,9 @@ jobs: repo }) - if (versionTag !== releases[0]?.tag_name) { + const previousRelease = releases.find((r) => r.tag_name.startsWith('v6')) + + if (versionTag !== previousRelease?.tag_name) { return versionTag } diff --git a/scripts/release.js b/scripts/release.js index b901f8854e7..ad8e8468669 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -8,12 +8,14 @@ const generateReleaseNotes = async ({ github, owner, repo, versionTag, defaultBr repo }) + const previousRelease = releases.find((r) => r.tag_name.startsWith('v6')) + const { data: { body } } = await github.rest.repos.generateReleaseNotes({ owner, repo, tag_name: versionTag, target_commitish: defaultBranch, - previous_tag_name: releases[0]?.tag_name + previous_tag_name: previousRelease?.tag_name }) const bodyWithoutReleasePr = body.split('\n') From c0a0bb5b8ba50d921a7903870ba86af5200f8be2 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 24 Apr 2024 13:51:57 +0200 Subject: [PATCH 04/20] fetch: do not leak signal listeners (#3158) * Do not leak signal listeners Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * Update lib/web/fetch/request.js Co-authored-by: Aras Abbasi --------- Signed-off-by: Matteo Collina Co-authored-by: Aras Abbasi --- lib/web/fetch/dispatcher-weakref.js | 5 ++- lib/web/fetch/request.js | 46 ++++++++++++---------- test/fetch/long-lived-abort-controller.js | 48 +++++++++++++++++++++++ 3 files changed, 77 insertions(+), 22 deletions(-) create mode 100644 test/fetch/long-lived-abort-controller.js diff --git a/lib/web/fetch/dispatcher-weakref.js b/lib/web/fetch/dispatcher-weakref.js index 05fde6f09f4..6ac5f374992 100644 --- a/lib/web/fetch/dispatcher-weakref.js +++ b/lib/web/fetch/dispatcher-weakref.js @@ -33,9 +33,10 @@ class CompatFinalizer { } module.exports = function () { - // FIXME: remove workaround when the Node bug is fixed + // FIXME: remove workaround when the Node bug is backported to v18 // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 - if (process.env.NODE_V8_COVERAGE) { + if (process.env.NODE_V8_COVERAGE && process.version.startsWith('v18')) { + process._rawDebug('Using compatibility WeakRef and FinalizationRegistry') return { WeakRef: CompatWeakRef, FinalizationRegistry: CompatFinalizer diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index 2cb8b66db90..ca12576b1f0 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -38,6 +38,29 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { signal.removeEventListener('abort', abort) }) +function buildAbort (acRef) { + return abort + + function abort () { + const ac = acRef.deref() + if (ac !== undefined) { + // Currently, there is a problem with FinalizationRegistry. + // https://github.com/nodejs/node/issues/49344 + // https://github.com/nodejs/node/issues/47748 + // In the case of abort, the first step is to unregister from it. + // If the controller can refer to it, it is still registered. + // It will be removed in the future. + requestFinalizer.unregister(abort) + + // Unsubscribe a listener. + // FinalizationRegistry will no longer be called, so this must be done. + this.removeEventListener('abort', abort) + + ac.abort(this.reason) + } + } +} + let patchMethodWarning = false // https://fetch.spec.whatwg.org/#request-class @@ -377,24 +400,7 @@ class Request { this[kAbortController] = ac const acRef = new WeakRef(ac) - const abort = function () { - const ac = acRef.deref() - if (ac !== undefined) { - // Currently, there is a problem with FinalizationRegistry. - // https://github.com/nodejs/node/issues/49344 - // https://github.com/nodejs/node/issues/47748 - // In the case of abort, the first step is to unregister from it. - // If the controller can refer to it, it is still registered. - // It will be removed in the future. - requestFinalizer.unregister(abort) - - // Unsubscribe a listener. - // FinalizationRegistry will no longer be called, so this must be done. - this.removeEventListener('abort', abort) - - ac.abort(this.reason) - } - } + const abort = buildAbort(acRef) // Third-party AbortControllers may not work with these. // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619. @@ -402,9 +408,9 @@ class Request { // If the max amount of listeners is equal to the default, increase it // This is only available in node >= v19.9.0 if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) { - setMaxListeners(100, signal) + setMaxListeners(1500, signal) } else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { - setMaxListeners(100, signal) + setMaxListeners(1500, signal) } } catch {} diff --git a/test/fetch/long-lived-abort-controller.js b/test/fetch/long-lived-abort-controller.js new file mode 100644 index 00000000000..1518989e4fb --- /dev/null +++ b/test/fetch/long-lived-abort-controller.js @@ -0,0 +1,48 @@ +'use strict' + +const http = require('node:http') +const { fetch } = require('../../') +const { once } = require('events') +const { test } = require('node:test') +const { closeServerAsPromise } = require('../utils/node-http') +const { strictEqual } = require('node:assert') + +const isNode18 = process.version.startsWith('v18') + +test('long-lived-abort-controller', { skip: isNode18 }, async (t) => { + const server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.write('Hello World!') + res.end() + }).listen(0) + + await once(server, 'listening') + + t.after(closeServerAsPromise(server)) + + let warningEmitted = false + function onWarning () { + warningEmitted = true + } + process.on('warning', onWarning) + t.after(() => { + process.off('warning', onWarning) + }) + + const controller = new AbortController() + + // The maxListener is set to 1500 in request.js. + // we set it to 2000 to make sure that we are not leaking event listeners. + // Unfortunately we are relying on GC and implementation details here. + for (let i = 0; i < 2000; i++) { + // make request + const res = await fetch(`http://localhost:${server.address().port}`, { + signal: controller.signal + }) + + // drain body + await res.text() + } + + strictEqual(warningEmitted, false) +}) From ef1b53b83e1bd33f6629af4a4d0aa627ce7d7528 Mon Sep 17 00:00:00 2001 From: Thomas Sibley Date: Wed, 24 Apr 2024 06:10:56 -0700 Subject: [PATCH 05/20] fix: request cache mode is not the same as request mode (#3151) Noticed while reading thru the code. --- lib/web/fetch/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index f0a4cf70f08..a082797e575 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -1537,7 +1537,7 @@ async function httpNetworkOrCacheFetch ( // 24. If httpRequest’s cache mode is neither "no-store" nor "reload", // then: - if (httpRequest.mode !== 'no-store' && httpRequest.mode !== 'reload') { + if (httpRequest.cache !== 'no-store' && httpRequest.cache !== 'reload') { // TODO: cache } @@ -1548,7 +1548,7 @@ async function httpNetworkOrCacheFetch ( if (response == null) { // 1. If httpRequest’s cache mode is "only-if-cached", then return a // network error. - if (httpRequest.mode === 'only-if-cached') { + if (httpRequest.cache === 'only-if-cached') { return makeNetworkError('only if cached') } From e5b720041e9759c758732d0dbc0d877063d447fe Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 24 Apr 2024 22:57:11 +0900 Subject: [PATCH 06/20] fetch: don't re-lowercase HeadersList (#3159) --- lib/web/fetch/request.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index ca12576b1f0..60626f06a92 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -459,8 +459,9 @@ class Request { // 4. If headers is a Headers object, then for each header in its header // list, append header’s name/header’s value to this’s headers. if (headers instanceof HeadersList) { - for (const [key, val] of headers) { - headersList.append(key, val) + for (const { 0: key, 1: val } of headers) { + // Note: The header names are already in lowercase. + headersList.append(key, val, true) } // Note: Copy the `set-cookie` meta-data. headersList.cookies = headers.cookies From ad811224c63c99d6840475f9805ea6dbe8be7747 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 24 Apr 2024 16:12:31 +0200 Subject: [PATCH 07/20] Revert "fetch: don't re-lowercase HeadersList (#3159)" This reverts commit e5b720041e9759c758732d0dbc0d877063d447fe. --- lib/web/fetch/request.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index 60626f06a92..ca12576b1f0 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -459,9 +459,8 @@ class Request { // 4. If headers is a Headers object, then for each header in its header // list, append header’s name/header’s value to this’s headers. if (headers instanceof HeadersList) { - for (const { 0: key, 1: val } of headers) { - // Note: The header names are already in lowercase. - headersList.append(key, val, true) + for (const [key, val] of headers) { + headersList.append(key, val) } // Note: Copy the `set-cookie` meta-data. headersList.cookies = headers.cookies From 034e3f08009120cdc8e811f7e9d0154062147ccb Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 24 Apr 2024 15:24:53 -0400 Subject: [PATCH 08/20] fix casing issue when cloning Headers object (#3160) --- lib/web/fetch/headers.js | 6 ++++++ lib/web/fetch/request.js | 5 +++-- test/fetch/headers-case.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 test/fetch/headers-case.js diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index 2235f125c65..a1dab1050a8 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -602,6 +602,12 @@ webidl.converters.HeadersInit = function (V) { if (webidl.util.Type(V) === 'Object') { const iterator = Reflect.get(V, Symbol.iterator) + // A work-around to ensure we send the properly-cased Headers when V is a Headers object. + // Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please. + if (!util.types.isProxy(V) && kHeadersList in V && iterator === Headers.prototype.entries) { // Headers object + return V[kHeadersList].entries + } + if (typeof iterator === 'function') { return webidl.converters['sequence>'](V, iterator.bind(V)) } diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index ca12576b1f0..60626f06a92 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -459,8 +459,9 @@ class Request { // 4. If headers is a Headers object, then for each header in its header // list, append header’s name/header’s value to this’s headers. if (headers instanceof HeadersList) { - for (const [key, val] of headers) { - headersList.append(key, val) + for (const { 0: key, 1: val } of headers) { + // Note: The header names are already in lowercase. + headersList.append(key, val, true) } // Note: Copy the `set-cookie` meta-data. headersList.cookies = headers.cookies diff --git a/test/fetch/headers-case.js b/test/fetch/headers-case.js new file mode 100644 index 00000000000..bd9770b755f --- /dev/null +++ b/test/fetch/headers-case.js @@ -0,0 +1,30 @@ +'use strict' + +const { fetch, Headers } = require('../..') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { test } = require('node:test') +const { tspl } = require('@matteo.collina/tspl') + +test('Headers retain keys case-sensitive', async (t) => { + const assert = tspl(t, { plan: 3 }) + + const server = createServer((req, res) => { + assert.ok(req.rawHeaders.includes('Content-Type')) + + res.end() + }).listen(0) + + t.after(() => server.close()) + await once(server, 'listening') + + for (const headers of [ + new Headers([['Content-Type', 'text/plain']]), + { 'Content-Type': 'text/plain' }, + [['Content-Type', 'text/plain']] + ]) { + await fetch(`http://localhost:${server.address().port}`, { + headers + }) + } +}) From dfacde873f8fbdb32ac158e359d97cccf190bc62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 22:23:08 +0000 Subject: [PATCH 09/20] build(deps): bump node from `6d0f18a` to `db8772d` in /build (#3163) Bumps node from `6d0f18a` to `db8772d`. --- updated-dependencies: - dependency-name: node dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index ce011b4aecc..aa8a8fb6ca5 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM node:21-alpine3.19@sha256:6d0f18a1c67dc218c4af50c21256616286a53c09e500fadf025b6d342e1c90ae +FROM node:21-alpine3.19@sha256:db8772d9f5796ac4e8c47508038c413ea1478da010568a2e48672f19a8b80cd2 ARG UID=1000 ARG GID=1000 From f6f0787d1c14c657201968c60044b8ab44faab9b Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 25 Apr 2024 08:30:37 +0900 Subject: [PATCH 10/20] fix header cloning bug (#3162) * fix header cloning bug * Apply suggestions from code review * Apply suggestions from code review * Update headers.js --- lib/web/fetch/headers.js | 20 +++++++++++++++++++- test/fetch/headers.js | 6 ++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index a1dab1050a8..7f2f526a9cb 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -259,6 +259,24 @@ class HeadersList { return headers } + get entriesList () { + const headers = [] + + if (this[kHeadersMap].size !== 0) { + for (const { 0: lowerName, 1: { name, value } } of this[kHeadersMap]) { + if (lowerName === 'set-cookie') { + for (const cookie of this.cookies) { + headers.push([name, cookie]) + } + } else { + headers.push([name, value]) + } + } + } + + return headers + } + // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set toSortedArray () { const size = this[kHeadersMap].size @@ -605,7 +623,7 @@ webidl.converters.HeadersInit = function (V) { // A work-around to ensure we send the properly-cased Headers when V is a Headers object. // Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please. if (!util.types.isProxy(V) && kHeadersList in V && iterator === Headers.prototype.entries) { // Headers object - return V[kHeadersList].entries + return V[kHeadersList].entriesList } if (typeof iterator === 'function') { diff --git a/test/fetch/headers.js b/test/fetch/headers.js index 00798559e90..ac1183eeded 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -725,6 +725,12 @@ test('Headers.prototype.getSetCookie', async (t) => { assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo', 'test=onetwothree']) assert.ok('set-cookie' in entries) }) + + await t.test('When Headers are cloned, so are the cookies (Headers constructor)', () => { + const headers = new Headers([['set-cookie', 'a'], ['set-cookie', 'b']]) + + assert.deepStrictEqual([...headers], [...new Headers(headers)]) + }) }) test('When the value is updated, update the cache', (t) => { From d3de002adb8fda39478f8c8fd938b2c245d7557e Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Thu, 25 Apr 2024 11:54:12 +0200 Subject: [PATCH 11/20] chore: change bench naming for h2 (#3165) --- .github/workflows/bench.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 9e1e0186500..f9eac1c6f27 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -94,7 +94,7 @@ jobs: working-directory: ./benchmarks benchmark_current_h2: - name: benchmark current + name: benchmark current h2 runs-on: ubuntu-latest steps: - name: Checkout Code @@ -116,7 +116,7 @@ jobs: working-directory: ./benchmarks benchmark_branch_h2: - name: benchmark branch + name: benchmark branch h2 runs-on: ubuntu-latest steps: - name: Checkout Code From 77a794753915542f1e57793ee4803672402d3189 Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 26 Apr 2024 02:41:48 -0400 Subject: [PATCH 12/20] expose WebSocket related events in node bundle (#3167) Refs: https://github.com/nodejs/node/issues/50275 --- index-fetch.js | 5 ++++- test/fetch/bundle.js | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/index-fetch.js b/index-fetch.js index fc8e557ce84..7d1268a1203 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -17,8 +17,11 @@ module.exports.Headers = require('./lib/web/fetch/headers').Headers module.exports.Response = require('./lib/web/fetch/response').Response module.exports.Request = require('./lib/web/fetch/request').Request +const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/web/websocket/events') module.exports.WebSocket = require('./lib/web/websocket/websocket').WebSocket -module.exports.MessageEvent = require('./lib/web/websocket/events').MessageEvent +module.exports.CloseEvent = CloseEvent +module.exports.ErrorEvent = ErrorEvent +module.exports.MessageEvent = MessageEvent module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource diff --git a/test/fetch/bundle.js b/test/fetch/bundle.js index f38b81d583f..f073e9e18f9 100644 --- a/test/fetch/bundle.js +++ b/test/fetch/bundle.js @@ -3,7 +3,7 @@ const { test } = require('node:test') const assert = require('node:assert') -const { Response, Request, FormData, Headers } = require('../../undici-fetch') +const { Response, Request, FormData, Headers, MessageEvent, CloseEvent, ErrorEvent } = require('../../undici-fetch') test('bundle sets constructor.name and .name properly', () => { assert.strictEqual(new Response().constructor.name, 'Response') @@ -31,3 +31,9 @@ test('regression test for https://github.com/nodejs/node/issues/50263', () => { assert.strictEqual(request1.headers.get('test'), 'abc') }) + +test('WebSocket related events are exported', (t) => { + assert.deepStrictEqual(typeof CloseEvent, 'function') + assert.deepStrictEqual(typeof MessageEvent, 'function') + assert.deepStrictEqual(typeof ErrorEvent, 'function') +}) From 3f927b8ef17791109cbb4f427b3e98ec4db9df25 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 26 Apr 2024 11:49:01 +0200 Subject: [PATCH 13/20] feat: add support for if-match on retry handler (#3144) --- lib/handler/retry-handler.js | 35 ++-- test/retry-handler.js | 318 +++++++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+), 14 deletions(-) diff --git a/lib/handler/retry-handler.js b/lib/handler/retry-handler.js index 2258801ba58..56ea4be79be 100644 --- a/lib/handler/retry-handler.js +++ b/lib/handler/retry-handler.js @@ -7,9 +7,7 @@ const { isDisturbed, parseHeaders, parseRangeHeader } = require('../core/util') function calculateRetryAfterHeader (retryAfter) { const current = Date.now() - const diff = new Date(retryAfter).getTime() - current - - return diff + return new Date(retryAfter).getTime() - current } class RetryHandler { @@ -116,11 +114,7 @@ class RetryHandler { const { counter } = state // Any code that is not a Undici's originated and allowed to retry - if ( - code && - code !== 'UND_ERR_REQ_RETRY' && - !errorCodes.includes(code) - ) { + if (code && code !== 'UND_ERR_REQ_RETRY' && !errorCodes.includes(code)) { cb(err) return } @@ -246,10 +240,7 @@ class RetryHandler { start != null && Number.isFinite(start), 'content-range mismatch' ) - assert( - end != null && Number.isFinite(end), - 'invalid content-length' - ) + assert(end != null && Number.isFinite(end), 'invalid content-length') this.start = start this.end = end @@ -270,6 +261,13 @@ class RetryHandler { this.resume = resume this.etag = headers.etag != null ? headers.etag : null + // Weak etags are not useful for comparison nor cache + // for instance not safe to assume if the response is byte-per-byte + // equal + if (this.etag != null && this.etag.startsWith('W/')) { + this.etag = null + } + return this.handler.onHeaders( statusCode, rawHeaders, @@ -308,7 +306,9 @@ class RetryHandler { // and server error response if (this.retryCount - this.retryCountCheckpoint > 0) { // We count the difference between the last checkpoint and the current retry count - this.retryCount = this.retryCountCheckpoint + (this.retryCount - this.retryCountCheckpoint) + this.retryCount = + this.retryCountCheckpoint + + (this.retryCount - this.retryCountCheckpoint) } else { this.retryCount += 1 } @@ -328,11 +328,18 @@ class RetryHandler { } if (this.start !== 0) { + const headers = { range: `bytes=${this.start}-${this.end ?? ''}` } + + // Weak etag check - weak etags will make comparison algorithms never match + if (this.etag != null) { + headers['if-match'] = this.etag + } + this.opts = { ...this.opts, headers: { ...this.opts.headers, - range: `bytes=${this.start}-${this.end ?? ''}` + ...headers } } } diff --git a/test/retry-handler.js b/test/retry-handler.js index 2596a7d80cb..8894ea86668 100644 --- a/test/retry-handler.js +++ b/test/retry-handler.js @@ -979,3 +979,321 @@ test('Issue#2986 - Handle custom 206', async t => { await t.completed }) + +test('Issue#3128 - Support if-match', async t => { + t = tspl(t, { plan: 9 }) + + const chunks = [] + let counter = 0 + + // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47 + let x = 0 + const server = createServer((req, res) => { + if (x === 0) { + t.deepStrictEqual(req.headers.range, 'bytes=0-3') + res.setHeader('etag', 'asd') + res.write('abc') + setTimeout(() => { + res.destroy() + }, 1e2) + } else if (x === 1) { + t.deepStrictEqual(req.headers.range, 'bytes=3-') + t.deepStrictEqual(req.headers['if-match'], 'asd') + + res.setHeader('content-range', 'bytes 3-6/6') + res.setHeader('etag', 'asd') + res.statusCode = 206 + res.end('def') + } + x++ + }) + + const dispatchOptions = { + retryOptions: { + retry: function (err, _, done) { + counter++ + + if (err.code && err.code === 'UND_ERR_DESTROYED') { + return done(false) + } + + if (err.statusCode === 206) return done(err) + + setTimeout(done, 800) + } + }, + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: (...args) => { + return client.dispatch(...args) + }, + handler: { + onRequestSent () { + t.ok(true, 'pass') + }, + onConnect () { + t.ok(true, 'pass') + }, + onBodySent () { + t.ok(true, 'pass') + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.strictEqual(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef') + t.strictEqual(counter, 1) + }, + onError () { + t.fail() + } + } + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json', + Range: 'bytes=0-3' + } + }, + handler + ) + + after(async () => { + await client.close() + + server.close() + await once(server, 'close') + }) + }) + + await t.completed +}) + +test('Issue#3128 - Should ignore weak etags', async t => { + t = tspl(t, { plan: 9 }) + + const chunks = [] + let counter = 0 + + // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47 + let x = 0 + const server = createServer((req, res) => { + if (x === 0) { + t.deepStrictEqual(req.headers.range, 'bytes=0-3') + res.setHeader('etag', 'W/asd') + res.write('abc') + setTimeout(() => { + res.destroy() + }, 1e2) + } else if (x === 1) { + t.deepStrictEqual(req.headers.range, 'bytes=3-') + t.equal(req.headers['if-match'], undefined) + + res.setHeader('content-range', 'bytes 3-6/6') + res.setHeader('etag', 'W/asd') + res.statusCode = 206 + res.end('def') + } + x++ + }) + + const dispatchOptions = { + retryOptions: { + retry: function (err, _, done) { + counter++ + + if (err.code && err.code === 'UND_ERR_DESTROYED') { + return done(false) + } + + if (err.statusCode === 206) return done(err) + + setTimeout(done, 800) + } + }, + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: (...args) => { + return client.dispatch(...args) + }, + handler: { + onRequestSent () { + t.ok(true, 'pass') + }, + onConnect () { + t.ok(true, 'pass') + }, + onBodySent () { + t.ok(true, 'pass') + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.strictEqual(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef') + t.strictEqual(counter, 1) + }, + onError () { + t.fail() + } + } + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json', + Range: 'bytes=0-3' + } + }, + handler + ) + + after(async () => { + await client.close() + + server.close() + await once(server, 'close') + }) + }) + + await t.completed +}) + +test('Weak etags are ignored on range-requests', async t => { + t = tspl(t, { plan: 9 }) + + const chunks = [] + let counter = 0 + + // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47 + let x = 0 + const server = createServer((req, res) => { + if (x === 0) { + t.deepStrictEqual(req.headers.range, 'bytes=0-3') + res.setHeader('etag', 'W/asd') + res.write('abc') + setTimeout(() => { + res.destroy() + }, 1e2) + } else if (x === 1) { + t.deepStrictEqual(req.headers.range, 'bytes=3-') + t.equal(req.headers['if-match'], undefined) + + res.setHeader('content-range', 'bytes 3-6/6') + res.setHeader('etag', 'W/efg') + res.statusCode = 206 + res.end('def') + } + x++ + }) + + const dispatchOptions = { + retryOptions: { + retry: function (err, _, done) { + counter++ + + if (err.code && err.code === 'UND_ERR_DESTROYED') { + return done(false) + } + + if (err.statusCode === 206) return done(err) + + setTimeout(done, 800) + } + }, + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: (...args) => { + return client.dispatch(...args) + }, + handler: { + onRequestSent () { + t.ok(true, 'pass') + }, + onConnect () { + t.ok(true, 'pass') + }, + onBodySent () { + t.ok(true, 'pass') + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.strictEqual(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef') + t.strictEqual(counter, 1) + }, + onError () { + t.fail() + } + } + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json', + Range: 'bytes=0-3' + } + }, + handler + ) + + after(async () => { + await client.close() + + server.close() + await once(server, 'close') + }) + }) + + await t.completed +}) From ed686fc7c8f42bd4a06a607c7ddc9e4777018d00 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:30:52 +0900 Subject: [PATCH 14/20] fix: correct firing order of abort events (#3169) --- lib/web/fetch/request.js | 30 ++++++++++++++++++++++++++---- test/wpt/status/fetch.status.json | 4 +--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index 60626f06a92..d963986ce2d 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -38,6 +38,8 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { signal.removeEventListener('abort', abort) }) +const dependentControllerMap = new WeakMap() + function buildAbort (acRef) { return abort @@ -57,6 +59,21 @@ function buildAbort (acRef) { this.removeEventListener('abort', abort) ac.abort(this.reason) + + const controllerList = dependentControllerMap.get(ac.signal) + + if (controllerList !== undefined) { + if (controllerList.size !== 0) { + for (const ref of controllerList) { + const ctrl = ref.deref() + if (ctrl !== undefined) { + ctrl.abort(this.reason) + } + } + controllerList.clear() + } + dependentControllerMap.delete(ac.signal) + } } } } @@ -754,11 +771,16 @@ class Request { if (this.signal.aborted) { ac.abort(this.signal.reason) } else { + let list = dependentControllerMap.get(this.signal) + if (list === undefined) { + list = new Set() + dependentControllerMap.set(this.signal, list) + } + const acRef = new WeakRef(ac) + list.add(acRef) util.addAbortListener( - this.signal, - () => { - ac.abort(this.signal.reason) - } + ac.signal, + buildAbort(acRef) ) } diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index 62677d62025..b07564d9303 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -2,13 +2,11 @@ "api": { "abort": { "general.any.js": { - "note": "TODO(@KhafraDev): Clone aborts with original controller can probably be fixed", "fail": [ "Already aborted signal rejects immediately", "Underlying connection is closed when aborting after receiving response - no-cors", "Stream errors once aborted. Underlying connection closed.", - "Readable stream synchronously cancels with AbortError if aborted before reading", - "Clone aborts with original controller" + "Readable stream synchronously cancels with AbortError if aborted before reading" ] }, "cache.https.any.js": { From 381f32c50e2826760fd399be40e06352f1b8f36d Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 26 Apr 2024 14:23:55 -0400 Subject: [PATCH 15/20] create fast MessageEvent (#3170) * create fast MessageEvent * expose * use * fixup --- benchmarks/websocket/messageevent.mjs | 20 ++++++++++++++++++++ index-fetch.js | 3 ++- lib/web/eventsource/eventsource.js | 4 ++-- lib/web/websocket/connection.js | 2 +- lib/web/websocket/events.js | 23 ++++++++++++++++++++++- lib/web/websocket/util.js | 11 ++++++----- 6 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 benchmarks/websocket/messageevent.mjs diff --git a/benchmarks/websocket/messageevent.mjs b/benchmarks/websocket/messageevent.mjs new file mode 100644 index 00000000000..b146cbcf300 --- /dev/null +++ b/benchmarks/websocket/messageevent.mjs @@ -0,0 +1,20 @@ +import { bench, group, run } from 'mitata' +import { createFastMessageEvent, MessageEvent as UndiciMessageEvent } from '../../lib/web/websocket/events.js' + +const { port1, port2 } = new MessageChannel() + +group('MessageEvent instantiation', () => { + bench('undici - fast MessageEvent init', () => { + return createFastMessageEvent('event', { data: null, ports: [port1, port2] }) + }) + + bench('undici - MessageEvent init', () => { + return new UndiciMessageEvent('event', { data: null, ports: [port1, port2] }) + }) + + bench('global - MessageEvent init', () => { + return new MessageEvent('event', { data: null, ports: [port1, port2] }) + }) +}) + +await run() diff --git a/index-fetch.js b/index-fetch.js index 7d1268a1203..a5903f1a8cb 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -17,11 +17,12 @@ module.exports.Headers = require('./lib/web/fetch/headers').Headers module.exports.Response = require('./lib/web/fetch/response').Response module.exports.Request = require('./lib/web/fetch/request').Request -const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/web/websocket/events') +const { CloseEvent, ErrorEvent, MessageEvent, createFastMessageEvent } = require('./lib/web/websocket/events') module.exports.WebSocket = require('./lib/web/websocket/websocket').WebSocket module.exports.CloseEvent = CloseEvent module.exports.ErrorEvent = ErrorEvent module.exports.MessageEvent = MessageEvent +module.exports.createFastMessageEvent = createFastMessageEvent module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource diff --git a/lib/web/eventsource/eventsource.js b/lib/web/eventsource/eventsource.js index 7b0d9b3e5de..0b1e48dbd2d 100644 --- a/lib/web/eventsource/eventsource.js +++ b/lib/web/eventsource/eventsource.js @@ -6,7 +6,7 @@ const { makeRequest } = require('../fetch/request') const { webidl } = require('../fetch/webidl') const { EventSourceStream } = require('./eventsource-stream') const { parseMIMEType } = require('../fetch/data-url') -const { MessageEvent } = require('../websocket/events') +const { createFastMessageEvent } = require('../websocket/events') const { isNetworkError } = require('../fetch/response') const { delay } = require('./util') const { kEnumerableProperty } = require('../../core/util') @@ -290,7 +290,7 @@ class EventSource extends EventTarget { const eventSourceStream = new EventSourceStream({ eventSourceSettings: this.#state, push: (event) => { - this.dispatchEvent(new MessageEvent( + this.dispatchEvent(createFastMessageEvent( event.type, event.options )) diff --git a/lib/web/websocket/connection.js b/lib/web/websocket/connection.js index 74674ee5ab7..8a0ce1914c1 100644 --- a/lib/web/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -267,7 +267,7 @@ function onSocketClose () { // decode without BOM to the WebSocket connection close // reason. // TODO: process.nextTick - fireEvent('close', ws, CloseEvent, { + fireEvent('close', ws, (type, init) => new CloseEvent(type, init), { wasClean, code, reason }) diff --git a/lib/web/websocket/events.js b/lib/web/websocket/events.js index b1f91d0e190..7b3bb263c61 100644 --- a/lib/web/websocket/events.js +++ b/lib/web/websocket/events.js @@ -2,6 +2,7 @@ const { webidl } = require('../fetch/webidl') const { kEnumerableProperty } = require('../../core/util') +const { kConstruct } = require('../../core/symbols') const { MessagePort } = require('node:worker_threads') /** @@ -11,6 +12,11 @@ class MessageEvent extends Event { #eventInit constructor (type, eventInitDict = {}) { + if (type === kConstruct) { + super(arguments[1], arguments[2]) + return + } + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent constructor' }) type = webidl.converters.DOMString(type) @@ -73,8 +79,22 @@ class MessageEvent extends Event { bubbles, cancelable, data, origin, lastEventId, source, ports }) } + + static createFastMessageEvent (type, init) { + const messageEvent = new MessageEvent(kConstruct, type, init) + messageEvent.#eventInit = init + messageEvent.#eventInit.data ??= null + messageEvent.#eventInit.origin ??= '' + messageEvent.#eventInit.lastEventId ??= '' + messageEvent.#eventInit.source ??= null + messageEvent.#eventInit.ports ??= [] + return messageEvent + } } +const { createFastMessageEvent } = MessageEvent +delete MessageEvent.createFastMessageEvent + /** * @see https://websockets.spec.whatwg.org/#the-closeevent-interface */ @@ -299,5 +319,6 @@ webidl.converters.ErrorEventInit = webidl.dictionaryConverter([ module.exports = { MessageEvent, CloseEvent, - ErrorEvent + ErrorEvent, + createFastMessageEvent } diff --git a/lib/web/websocket/util.js b/lib/web/websocket/util.js index 20cdb995efe..79d9d208182 100644 --- a/lib/web/websocket/util.js +++ b/lib/web/websocket/util.js @@ -2,7 +2,7 @@ const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require('./symbols') const { states, opcodes } = require('./constants') -const { MessageEvent, ErrorEvent } = require('./events') +const { ErrorEvent, createFastMessageEvent } = require('./events') const { isUtf8 } = require('node:buffer') /* globals Blob */ @@ -51,15 +51,16 @@ function isClosed (ws) { * @see https://dom.spec.whatwg.org/#concept-event-fire * @param {string} e * @param {EventTarget} target + * @param {(...args: ConstructorParameters) => Event} eventFactory * @param {EventInit | undefined} eventInitDict */ -function fireEvent (e, target, eventConstructor = Event, eventInitDict = {}) { +function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) { // 1. If eventConstructor is not given, then let eventConstructor be Event. // 2. Let event be the result of creating an event given eventConstructor, // in the relevant realm of target. // 3. Initialize event’s type attribute to e. - const event = new eventConstructor(e, eventInitDict) // eslint-disable-line new-cap + const event = eventFactory(e, eventInitDict) // 4. Initialize any other IDL attributes of event as described in the // invocation of this algorithm. @@ -110,7 +111,7 @@ function websocketMessageReceived (ws, type, data) { // 3. Fire an event named message at the WebSocket object, using MessageEvent, // with the origin attribute initialized to the serialization of the WebSocket // object’s url's origin, and the data attribute initialized to dataForEvent. - fireEvent('message', ws, MessageEvent, { + fireEvent('message', ws, createFastMessageEvent, { origin: ws[kWebSocketURL].origin, data: dataForEvent }) @@ -195,7 +196,7 @@ function failWebsocketConnection (ws, reason) { if (reason) { // TODO: process.nextTick - fireEvent('error', ws, ErrorEvent, { + fireEvent('error', ws, (type, init) => new ErrorEvent(type, init), { error: new Error(reason) }) } From 51a97af1845fa3d092ce1a5246a060e80393e967 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sat, 27 Apr 2024 01:26:31 +0200 Subject: [PATCH 16/20] chore: add explicitly @fastify/busboy (#3172) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cf4c7662aeb..fcbd63b12ae 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "prepare": "husky install && node ./scripts/platform-shell.js" }, "devDependencies": { + "@fastify/busboy": "2.1.1", "@matteo.collina/tspl": "^0.1.1", "@sinonjs/fake-timers": "^11.1.0", "@types/node": "^18.0.3", From 24f7ee66f01a1398ad15895fe0609a7adc47153e Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sat, 27 Apr 2024 09:39:01 +0200 Subject: [PATCH 17/20] deps: remove sinon (#3171) --- package.json | 1 - test/env-http-proxy-agent.js | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index fcbd63b12ae..be03f23267f 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,6 @@ "node-forge": "^1.3.1", "pre-commit": "^1.2.2", "proxy": "^2.1.1", - "sinon": "^17.0.1", "snazzy": "^9.0.0", "standard": "^17.0.0", "tsd": "^0.31.0", diff --git a/test/env-http-proxy-agent.js b/test/env-http-proxy-agent.js index 4949df9f5f8..1a707fad538 100644 --- a/test/env-http-proxy-agent.js +++ b/test/env-http-proxy-agent.js @@ -2,7 +2,6 @@ const { tspl } = require('@matteo.collina/tspl') const { test, describe, after, beforeEach } = require('node:test') -const sinon = require('sinon') const { EnvHttpProxyAgent, ProxyAgent, Agent, fetch, MockAgent } = require('..') const { kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent, kClosed, kDestroyed, kProxy } = require('../lib/core/symbols') @@ -174,15 +173,23 @@ const createEnvHttpProxyAgentWithMocks = (plan = 1, opts = {}) => { process.env.https_proxy = 'http://localhost:8443' const dispatcher = new EnvHttpProxyAgent({ ...opts, factory }) const agentSymbols = [kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent] - agentSymbols.forEach((agent) => { - sinon.spy(dispatcher[agent], 'dispatch') + agentSymbols.forEach((agentSymbol) => { + const originalDispatch = dispatcher[agentSymbol].dispatch + dispatcher[agentSymbol].dispatch = function () { + dispatcher[agentSymbol].dispatch.called = true + return originalDispatch.apply(this, arguments) + } + dispatcher[agentSymbol].dispatch.called = false }) const usesProxyAgent = async (agent, url) => { await fetch(url, { dispatcher }) const result = agentSymbols.every((agentSymbol) => agent === agentSymbol - ? dispatcher[agentSymbol].dispatch.called - : dispatcher[agentSymbol].dispatch.notCalled) - agentSymbols.forEach((agent) => { dispatcher[agent].dispatch.resetHistory() }) + ? dispatcher[agentSymbol].dispatch.called === true + : dispatcher[agentSymbol].dispatch.called === false) + + agentSymbols.forEach((agentSymbol) => { + dispatcher[agentSymbol].dispatch.called = false + }) return result } const doesNotProxy = usesProxyAgent.bind(this, kNoProxyAgent) From 57e75d3e8335fc588bd89a28bfbcc4a99da5aa20 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 29 Apr 2024 01:00:35 -0400 Subject: [PATCH 18/20] webidl changes (#3175) --- lib/web/cache/cache.js | 65 ++++++++------ lib/web/cache/cachestorage.js | 20 +++-- lib/web/cookies/index.js | 31 +++---- lib/web/eventsource/eventsource.js | 9 +- lib/web/fetch/formdata.js | 42 +++++---- lib/web/fetch/headers.js | 45 +++++----- lib/web/fetch/index.js | 2 +- lib/web/fetch/request.js | 17 ++-- lib/web/fetch/response.js | 28 +++--- lib/web/fetch/util.js | 2 +- lib/web/fetch/webidl.js | 136 ++++++++++++++++------------- lib/web/fileapi/filereader.js | 10 +-- lib/web/fileapi/progressevent.js | 14 +-- lib/web/websocket/events.js | 51 +++++------ lib/web/websocket/websocket.js | 30 +++---- test/fetch/headers.js | 4 +- test/webidl/converters.js | 50 +++++------ test/webidl/errors.js | 23 +++++ test/webidl/helpers.js | 10 +-- test/websocket/messageevent.js | 10 +-- types/webidl.d.ts | 15 ++-- 21 files changed, 342 insertions(+), 272 deletions(-) create mode 100644 test/webidl/errors.js diff --git a/lib/web/cache/cache.js b/lib/web/cache/cache.js index 5a66d08cb6c..45c6fec3467 100644 --- a/lib/web/cache/cache.js +++ b/lib/web/cache/cache.js @@ -42,10 +42,12 @@ class Cache { async match (request, options = {}) { webidl.brandCheck(this, Cache) - webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' }) - request = webidl.converters.RequestInfo(request) - options = webidl.converters.CacheQueryOptions(options) + const prefix = 'Cache.match' + webidl.argumentLengthCheck(arguments, 1, prefix) + + request = webidl.converters.RequestInfo(request, prefix, 'request') + options = webidl.converters.CacheQueryOptions(options, prefix, 'options') const p = this.#internalMatchAll(request, options, 1) @@ -59,17 +61,20 @@ class Cache { async matchAll (request = undefined, options = {}) { webidl.brandCheck(this, Cache) - if (request !== undefined) request = webidl.converters.RequestInfo(request) - options = webidl.converters.CacheQueryOptions(options) + const prefix = 'Cache.matchAll' + if (request !== undefined) request = webidl.converters.RequestInfo(request, prefix, 'request') + options = webidl.converters.CacheQueryOptions(options, prefix, 'options') return this.#internalMatchAll(request, options) } async add (request) { webidl.brandCheck(this, Cache) - webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' }) - request = webidl.converters.RequestInfo(request) + const prefix = 'Cache.add' + webidl.argumentLengthCheck(arguments, 1, prefix) + + request = webidl.converters.RequestInfo(request, prefix, 'request') // 1. const requests = [request] @@ -83,7 +88,9 @@ class Cache { async addAll (requests) { webidl.brandCheck(this, Cache) - webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' }) + + const prefix = 'Cache.addAll' + webidl.argumentLengthCheck(arguments, 1, prefix) // 1. const responsePromises = [] @@ -95,7 +102,7 @@ class Cache { for (let request of requests) { if (request === undefined) { throw webidl.errors.conversionFailed({ - prefix: 'Cache.addAll', + prefix, argument: 'Argument 1', types: ['undefined is not allowed'] }) @@ -113,7 +120,7 @@ class Cache { // 3.2 if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') { throw webidl.errors.exception({ - header: 'Cache.addAll', + header: prefix, message: 'Expected http/s scheme when method is not GET.' }) } @@ -131,7 +138,7 @@ class Cache { // 5.2 if (!urlIsHttpHttpsScheme(r.url)) { throw webidl.errors.exception({ - header: 'Cache.addAll', + header: prefix, message: 'Expected http/s scheme.' }) } @@ -251,10 +258,12 @@ class Cache { async put (request, response) { webidl.brandCheck(this, Cache) - webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' }) - request = webidl.converters.RequestInfo(request) - response = webidl.converters.Response(response) + const prefix = 'Cache.put' + webidl.argumentLengthCheck(arguments, 2, prefix) + + request = webidl.converters.RequestInfo(request, prefix, 'request') + response = webidl.converters.Response(response, prefix, 'response') // 1. let innerRequest = null @@ -269,7 +278,7 @@ class Cache { // 4. if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') { throw webidl.errors.exception({ - header: 'Cache.put', + header: prefix, message: 'Expected an http/s scheme when method is not GET' }) } @@ -280,7 +289,7 @@ class Cache { // 6. if (innerResponse.status === 206) { throw webidl.errors.exception({ - header: 'Cache.put', + header: prefix, message: 'Got 206 status' }) } @@ -295,7 +304,7 @@ class Cache { // 7.2.1 if (fieldValue === '*') { throw webidl.errors.exception({ - header: 'Cache.put', + header: prefix, message: 'Got * vary field value' }) } @@ -305,7 +314,7 @@ class Cache { // 8. if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { throw webidl.errors.exception({ - header: 'Cache.put', + header: prefix, message: 'Response body is locked or disturbed' }) } @@ -380,10 +389,12 @@ class Cache { async delete (request, options = {}) { webidl.brandCheck(this, Cache) - webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' }) - request = webidl.converters.RequestInfo(request) - options = webidl.converters.CacheQueryOptions(options) + const prefix = 'Cache.delete' + webidl.argumentLengthCheck(arguments, 1, prefix) + + request = webidl.converters.RequestInfo(request, prefix, 'request') + options = webidl.converters.CacheQueryOptions(options, prefix, 'options') /** * @type {Request} @@ -445,8 +456,10 @@ class Cache { async keys (request = undefined, options = {}) { webidl.brandCheck(this, Cache) - if (request !== undefined) request = webidl.converters.RequestInfo(request) - options = webidl.converters.CacheQueryOptions(options) + const prefix = 'Cache.keys' + + if (request !== undefined) request = webidl.converters.RequestInfo(request, prefix, 'request') + options = webidl.converters.CacheQueryOptions(options, prefix, 'options') // 1. let r = null @@ -810,17 +823,17 @@ const cacheQueryOptionConverters = [ { key: 'ignoreSearch', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'ignoreMethod', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'ignoreVary', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false } ] diff --git a/lib/web/cache/cachestorage.js b/lib/web/cache/cachestorage.js index de3813cfecb..cc773b94b49 100644 --- a/lib/web/cache/cachestorage.js +++ b/lib/web/cache/cachestorage.js @@ -20,7 +20,7 @@ class CacheStorage { async match (request, options = {}) { webidl.brandCheck(this, CacheStorage) - webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.match' }) + webidl.argumentLengthCheck(arguments, 1, 'CacheStorage.match') request = webidl.converters.RequestInfo(request) options = webidl.converters.MultiCacheQueryOptions(options) @@ -57,9 +57,11 @@ class CacheStorage { */ async has (cacheName) { webidl.brandCheck(this, CacheStorage) - webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' }) - cacheName = webidl.converters.DOMString(cacheName) + const prefix = 'CacheStorage.has' + webidl.argumentLengthCheck(arguments, 1, prefix) + + cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName') // 2.1.1 // 2.2 @@ -73,9 +75,11 @@ class CacheStorage { */ async open (cacheName) { webidl.brandCheck(this, CacheStorage) - webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' }) - cacheName = webidl.converters.DOMString(cacheName) + const prefix = 'CacheStorage.open' + webidl.argumentLengthCheck(arguments, 1, prefix) + + cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName') // 2.1 if (this.#caches.has(cacheName)) { @@ -105,9 +109,11 @@ class CacheStorage { */ async delete (cacheName) { webidl.brandCheck(this, CacheStorage) - webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' }) - cacheName = webidl.converters.DOMString(cacheName) + const prefix = 'CacheStorage.delete' + webidl.argumentLengthCheck(arguments, 1, prefix) + + cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName') return this.#caches.delete(cacheName) } diff --git a/lib/web/cookies/index.js b/lib/web/cookies/index.js index 1fc2bd295f6..d40c71318e5 100644 --- a/lib/web/cookies/index.js +++ b/lib/web/cookies/index.js @@ -24,7 +24,7 @@ const { Headers } = require('../fetch/headers') * @returns {Record} */ function getCookies (headers) { - webidl.argumentLengthCheck(arguments, 1, { header: 'getCookies' }) + webidl.argumentLengthCheck(arguments, 1, 'getCookies') webidl.brandCheck(headers, Headers, { strict: false }) @@ -51,11 +51,12 @@ function getCookies (headers) { * @returns {void} */ function deleteCookie (headers, name, attributes) { - webidl.argumentLengthCheck(arguments, 2, { header: 'deleteCookie' }) - webidl.brandCheck(headers, Headers, { strict: false }) - name = webidl.converters.DOMString(name) + const prefix = 'deleteCookie' + webidl.argumentLengthCheck(arguments, 2, prefix) + + name = webidl.converters.DOMString(name, prefix, 'name') attributes = webidl.converters.DeleteCookieAttributes(attributes) // Matches behavior of @@ -73,7 +74,7 @@ function deleteCookie (headers, name, attributes) { * @returns {Cookie[]} */ function getSetCookies (headers) { - webidl.argumentLengthCheck(arguments, 1, { header: 'getSetCookies' }) + webidl.argumentLengthCheck(arguments, 1, 'getSetCookies') webidl.brandCheck(headers, Headers, { strict: false }) @@ -93,7 +94,7 @@ function getSetCookies (headers) { * @returns {void} */ function setCookie (headers, cookie) { - webidl.argumentLengthCheck(arguments, 2, { header: 'setCookie' }) + webidl.argumentLengthCheck(arguments, 2, 'setCookie') webidl.brandCheck(headers, Headers, { strict: false }) @@ -110,12 +111,12 @@ webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([ { converter: webidl.nullableConverter(webidl.converters.DOMString), key: 'path', - defaultValue: null + defaultValue: () => null }, { converter: webidl.nullableConverter(webidl.converters.DOMString), key: 'domain', - defaultValue: null + defaultValue: () => null } ]) @@ -137,32 +138,32 @@ webidl.converters.Cookie = webidl.dictionaryConverter([ return new Date(value) }), key: 'expires', - defaultValue: null + defaultValue: () => null }, { converter: webidl.nullableConverter(webidl.converters['long long']), key: 'maxAge', - defaultValue: null + defaultValue: () => null }, { converter: webidl.nullableConverter(webidl.converters.DOMString), key: 'domain', - defaultValue: null + defaultValue: () => null }, { converter: webidl.nullableConverter(webidl.converters.DOMString), key: 'path', - defaultValue: null + defaultValue: () => null }, { converter: webidl.nullableConverter(webidl.converters.boolean), key: 'secure', - defaultValue: null + defaultValue: () => null }, { converter: webidl.nullableConverter(webidl.converters.boolean), key: 'httpOnly', - defaultValue: null + defaultValue: () => null }, { converter: webidl.converters.USVString, @@ -172,7 +173,7 @@ webidl.converters.Cookie = webidl.dictionaryConverter([ { converter: webidl.sequenceConverter(webidl.converters.DOMString), key: 'unparsed', - defaultValue: [] + defaultValue: () => new Array(0) } ]) diff --git a/lib/web/eventsource/eventsource.js b/lib/web/eventsource/eventsource.js index 0b1e48dbd2d..51634c1779f 100644 --- a/lib/web/eventsource/eventsource.js +++ b/lib/web/eventsource/eventsource.js @@ -105,7 +105,8 @@ class EventSource extends EventTarget { // 1. Let ev be a new EventSource object. super() - webidl.argumentLengthCheck(arguments, 1, { header: 'EventSource constructor' }) + const prefix = 'EventSource constructor' + webidl.argumentLengthCheck(arguments, 1, prefix) if (!experimentalWarned) { experimentalWarned = true @@ -114,8 +115,8 @@ class EventSource extends EventTarget { }) } - url = webidl.converters.USVString(url) - eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict) + url = webidl.converters.USVString(url, prefix, 'url') + eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, 'eventSourceInitDict') this.#dispatcher = eventSourceInitDict.dispatcher this.#state = { @@ -463,7 +464,7 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([ { key: 'withCredentials', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'dispatcher', // undici only diff --git a/lib/web/fetch/formdata.js b/lib/web/fetch/formdata.js index 029419ffe94..94a84b03ab3 100644 --- a/lib/web/fetch/formdata.js +++ b/lib/web/fetch/formdata.js @@ -28,7 +28,8 @@ class FormData { append (name, value, filename = undefined) { webidl.brandCheck(this, FormData) - webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' }) + const prefix = 'FormData.append' + webidl.argumentLengthCheck(arguments, 2, prefix) if (arguments.length === 3 && !isBlobLike(value)) { throw new TypeError( @@ -38,12 +39,12 @@ class FormData { // 1. Let value be value if given; otherwise blobValue. - name = webidl.converters.USVString(name) + name = webidl.converters.USVString(name, prefix, 'name') value = isBlobLike(value) - ? webidl.converters.Blob(value, { strict: false }) - : webidl.converters.USVString(value) + ? webidl.converters.Blob(value, prefix, 'value', { strict: false }) + : webidl.converters.USVString(value, prefix, 'value') filename = arguments.length === 3 - ? webidl.converters.USVString(filename) + ? webidl.converters.USVString(filename, prefix, 'filename') : undefined // 2. Let entry be the result of creating an entry with @@ -57,9 +58,10 @@ class FormData { delete (name) { webidl.brandCheck(this, FormData) - webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' }) + const prefix = 'FormData.delete' + webidl.argumentLengthCheck(arguments, 1, prefix) - name = webidl.converters.USVString(name) + name = webidl.converters.USVString(name, prefix, 'name') // The delete(name) method steps are to remove all entries whose name // is name from this’s entry list. @@ -69,9 +71,10 @@ class FormData { get (name) { webidl.brandCheck(this, FormData) - webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' }) + const prefix = 'FormData.get' + webidl.argumentLengthCheck(arguments, 1, prefix) - name = webidl.converters.USVString(name) + name = webidl.converters.USVString(name, prefix, 'name') // 1. If there is no entry whose name is name in this’s entry list, // then return null. @@ -88,9 +91,10 @@ class FormData { getAll (name) { webidl.brandCheck(this, FormData) - webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' }) + const prefix = 'FormData.getAll' + webidl.argumentLengthCheck(arguments, 1, prefix) - name = webidl.converters.USVString(name) + name = webidl.converters.USVString(name, prefix, 'name') // 1. If there is no entry whose name is name in this’s entry list, // then return the empty list. @@ -104,9 +108,10 @@ class FormData { has (name) { webidl.brandCheck(this, FormData) - webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' }) + const prefix = 'FormData.has' + webidl.argumentLengthCheck(arguments, 1, prefix) - name = webidl.converters.USVString(name) + name = webidl.converters.USVString(name, prefix, 'name') // The has(name) method steps are to return true if there is an entry // whose name is name in this’s entry list; otherwise false. @@ -116,7 +121,8 @@ class FormData { set (name, value, filename = undefined) { webidl.brandCheck(this, FormData) - webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' }) + const prefix = 'FormData.set' + webidl.argumentLengthCheck(arguments, 2, prefix) if (arguments.length === 3 && !isBlobLike(value)) { throw new TypeError( @@ -129,12 +135,12 @@ class FormData { // 1. Let value be value if given; otherwise blobValue. - name = webidl.converters.USVString(name) + name = webidl.converters.USVString(name, prefix, 'name') value = isBlobLike(value) - ? webidl.converters.Blob(value, { strict: false }) - : webidl.converters.USVString(value) + ? webidl.converters.Blob(value, prefix, 'name', { strict: false }) + : webidl.converters.USVString(value, prefix, 'name') filename = arguments.length === 3 - ? webidl.converters.USVString(filename) + ? webidl.converters.USVString(filename, prefix, 'name') : undefined // 2. Let entry be the result of creating an entry with name, value, and diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index 7f2f526a9cb..3157561b470 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -366,7 +366,7 @@ class Headers { // 2. If init is given, then fill this with init. if (init !== undefined) { - init = webidl.converters.HeadersInit(init) + init = webidl.converters.HeadersInit(init, 'Headers contructor', 'init') fill(this, init) } } @@ -375,10 +375,11 @@ class Headers { append (name, value) { webidl.brandCheck(this, Headers) - webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' }) + webidl.argumentLengthCheck(arguments, 2, 'Headers.append') - name = webidl.converters.ByteString(name) - value = webidl.converters.ByteString(value) + const prefix = 'Headers.append' + name = webidl.converters.ByteString(name, prefix, 'name') + value = webidl.converters.ByteString(value, prefix, 'value') return appendHeader(this, name, value) } @@ -387,9 +388,10 @@ class Headers { delete (name) { webidl.brandCheck(this, Headers) - webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' }) + webidl.argumentLengthCheck(arguments, 1, 'Headers.delete') - name = webidl.converters.ByteString(name) + const prefix = 'Headers.delete' + name = webidl.converters.ByteString(name, prefix, 'name') // 1. If name is not a header name, then throw a TypeError. if (!isValidHeaderName(name)) { @@ -432,14 +434,15 @@ class Headers { get (name) { webidl.brandCheck(this, Headers) - webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' }) + webidl.argumentLengthCheck(arguments, 1, 'Headers.get') - name = webidl.converters.ByteString(name) + const prefix = 'Headers.get' + name = webidl.converters.ByteString(name, prefix, 'name') // 1. If name is not a header name, then throw a TypeError. if (!isValidHeaderName(name)) { throw webidl.errors.invalidArgument({ - prefix: 'Headers.get', + prefix, value: name, type: 'header name' }) @@ -454,14 +457,15 @@ class Headers { has (name) { webidl.brandCheck(this, Headers) - webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' }) + webidl.argumentLengthCheck(arguments, 1, 'Headers.has') - name = webidl.converters.ByteString(name) + const prefix = 'Headers.has' + name = webidl.converters.ByteString(name, prefix, 'name') // 1. If name is not a header name, then throw a TypeError. if (!isValidHeaderName(name)) { throw webidl.errors.invalidArgument({ - prefix: 'Headers.has', + prefix, value: name, type: 'header name' }) @@ -476,10 +480,11 @@ class Headers { set (name, value) { webidl.brandCheck(this, Headers) - webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' }) + webidl.argumentLengthCheck(arguments, 2, 'Headers.set') - name = webidl.converters.ByteString(name) - value = webidl.converters.ByteString(value) + const prefix = 'Headers.set' + name = webidl.converters.ByteString(name, prefix, 'name') + value = webidl.converters.ByteString(value, prefix, 'value') // 1. Normalize value. value = headerValueNormalize(value) @@ -488,13 +493,13 @@ class Headers { // header value, then throw a TypeError. if (!isValidHeaderName(name)) { throw webidl.errors.invalidArgument({ - prefix: 'Headers.set', + prefix, value: name, type: 'header name' }) } else if (!isValidHeaderValue(value)) { throw webidl.errors.invalidArgument({ - prefix: 'Headers.set', + prefix, value, type: 'header value' }) @@ -616,7 +621,7 @@ Object.defineProperties(Headers.prototype, { } }) -webidl.converters.HeadersInit = function (V) { +webidl.converters.HeadersInit = function (V, prefix, argument) { if (webidl.util.Type(V) === 'Object') { const iterator = Reflect.get(V, Symbol.iterator) @@ -627,10 +632,10 @@ webidl.converters.HeadersInit = function (V) { } if (typeof iterator === 'function') { - return webidl.converters['sequence>'](V, iterator.bind(V)) + return webidl.converters['sequence>'](V, prefix, argument, iterator.bind(V)) } - return webidl.converters['record'](V) + return webidl.converters['record'](V, prefix, argument) } throw webidl.errors.conversionFailed({ diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index a082797e575..82ccb26865c 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -122,7 +122,7 @@ class Fetch extends EE { // https://fetch.spec.whatwg.org/#fetch-method function fetch (input, init = undefined) { - webidl.argumentLengthCheck(arguments, 1, { header: 'globalThis.fetch' }) + webidl.argumentLengthCheck(arguments, 1, 'globalThis.fetch') // 1. Let p be a new promise. const p = createDeferredPromise() diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index d963986ce2d..1441a58b06e 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -88,10 +88,11 @@ class Request { return } - webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' }) + const prefix = 'Request constructor' + webidl.argumentLengthCheck(arguments, 1, prefix) - input = webidl.converters.RequestInfo(input) - init = webidl.converters.RequestInit(init) + input = webidl.converters.RequestInfo(input, prefix, 'input') + init = webidl.converters.RequestInit(init, prefix, 'init') // 1. Let request be null. let request = null @@ -932,16 +933,16 @@ webidl.converters.Request = webidl.interfaceConverter( ) // https://fetch.spec.whatwg.org/#requestinfo -webidl.converters.RequestInfo = function (V) { +webidl.converters.RequestInfo = function (V, prefix, argument) { if (typeof V === 'string') { - return webidl.converters.USVString(V) + return webidl.converters.USVString(V, prefix, argument) } if (V instanceof Request) { - return webidl.converters.Request(V) + return webidl.converters.Request(V, prefix, argument) } - return webidl.converters.USVString(V) + return webidl.converters.USVString(V, prefix, argument) } webidl.converters.AbortSignal = webidl.interfaceConverter( @@ -1011,6 +1012,8 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([ converter: webidl.nullableConverter( (signal) => webidl.converters.AbortSignal( signal, + 'RequestInit', + 'signal', { strict: false } ) ) diff --git a/lib/web/fetch/response.js b/lib/web/fetch/response.js index f609e3d4c5d..222a9a5b2f7 100644 --- a/lib/web/fetch/response.js +++ b/lib/web/fetch/response.js @@ -43,7 +43,7 @@ class Response { // https://fetch.spec.whatwg.org/#dom-response-json static json (data, init = {}) { - webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' }) + webidl.argumentLengthCheck(arguments, 1, 'Response.json') if (init !== null) { init = webidl.converters.ResponseInit(init) @@ -70,7 +70,7 @@ class Response { // Creates a redirect Response that redirects to url with status status. static redirect (url, status = 302) { - webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' }) + webidl.argumentLengthCheck(arguments, 1, 'Response.redirect') url = webidl.converters.USVString(url) status = webidl.converters['unsigned short'](status) @@ -526,34 +526,34 @@ webidl.converters.URLSearchParams = webidl.interfaceConverter( ) // https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit -webidl.converters.XMLHttpRequestBodyInit = function (V) { +webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) { if (typeof V === 'string') { - return webidl.converters.USVString(V) + return webidl.converters.USVString(V, prefix, name) } if (isBlobLike(V)) { - return webidl.converters.Blob(V, { strict: false }) + return webidl.converters.Blob(V, prefix, name, { strict: false }) } if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { - return webidl.converters.BufferSource(V) + return webidl.converters.BufferSource(V, prefix, name) } if (util.isFormDataLike(V)) { - return webidl.converters.FormData(V, { strict: false }) + return webidl.converters.FormData(V, prefix, name, { strict: false }) } if (V instanceof URLSearchParams) { - return webidl.converters.URLSearchParams(V) + return webidl.converters.URLSearchParams(V, prefix, name) } - return webidl.converters.DOMString(V) + return webidl.converters.DOMString(V, prefix, name) } // https://fetch.spec.whatwg.org/#bodyinit -webidl.converters.BodyInit = function (V) { +webidl.converters.BodyInit = function (V, prefix, argument) { if (V instanceof ReadableStream) { - return webidl.converters.ReadableStream(V) + return webidl.converters.ReadableStream(V, prefix, argument) } // Note: the spec doesn't include async iterables, @@ -562,19 +562,19 @@ webidl.converters.BodyInit = function (V) { return V } - return webidl.converters.XMLHttpRequestBodyInit(V) + return webidl.converters.XMLHttpRequestBodyInit(V, prefix, argument) } webidl.converters.ResponseInit = webidl.dictionaryConverter([ { key: 'status', converter: webidl.converters['unsigned short'], - defaultValue: 200 + defaultValue: () => 200 }, { key: 'statusText', converter: webidl.converters.ByteString, - defaultValue: '' + defaultValue: () => '' }, { key: 'headers', diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index a684c27d883..299f4e64c81 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -1016,7 +1016,7 @@ function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueInde configurable: true, value: function forEach (callbackfn, thisArg = globalThis) { webidl.brandCheck(this, object) - webidl.argumentLengthCheck(arguments, 1, { header: `${name}.forEach` }) + webidl.argumentLengthCheck(arguments, 1, `${name}.forEach`) if (typeof callbackfn !== 'function') { throw new TypeError( `Failed to execute 'forEach' on '${name}': parameter 1 is not of type 'Function'.` diff --git a/lib/web/fetch/webidl.js b/lib/web/fetch/webidl.js index ceb7f3e8b09..c90e411afca 100644 --- a/lib/web/fetch/webidl.js +++ b/lib/web/fetch/webidl.js @@ -33,14 +33,18 @@ webidl.errors.invalidArgument = function (context) { } // https://webidl.spec.whatwg.org/#implements -webidl.brandCheck = function (V, I, opts = undefined) { +webidl.brandCheck = function (V, I, opts) { if (opts?.strict !== false) { if (!(V instanceof I)) { - throw new TypeError('Illegal invocation') + const err = new TypeError('Illegal invocation') + err.code = 'ERR_INVALID_THIS' // node compat. + throw err } } else { if (V?.[Symbol.toStringTag] !== I.prototype[Symbol.toStringTag]) { - throw new TypeError('Illegal invocation') + const err = new TypeError('Illegal invocation') + err.code = 'ERR_INVALID_THIS' // node compat. + throw err } } } @@ -50,7 +54,7 @@ webidl.argumentLengthCheck = function ({ length }, min, ctx) { throw webidl.errors.exception({ message: `${min} argument${min !== 1 ? 's' : ''} required, ` + `but${length ? ' only' : ''} ${length} found.`, - ...ctx + header: ctx }) } } @@ -83,7 +87,7 @@ webidl.util.Type = function (V) { } // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint -webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { +webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) { let upperBound let lowerBound @@ -127,7 +131,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { // 6. If the conversion is to an IDL type associated // with the [EnforceRange] extended attribute, then: - if (opts.enforceRange === true) { + if (opts?.enforceRange === true) { // 1. If x is NaN, +∞, or −∞, then throw a TypeError. if ( Number.isNaN(x) || @@ -159,7 +163,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { // 7. If x is not NaN and the conversion is to an IDL // type associated with the [Clamp] extended // attribute, then: - if (!Number.isNaN(x) && opts.clamp === true) { + if (!Number.isNaN(x) && opts?.clamp === true) { // 1. Set x to min(max(x, lowerBound), upperBound). x = Math.min(Math.max(x, lowerBound), upperBound) @@ -233,12 +237,12 @@ webidl.util.Stringify = function (V) { // https://webidl.spec.whatwg.org/#es-sequence webidl.sequenceConverter = function (converter) { - return (V, Iterable) => { + return (V, prefix, argument, Iterable) => { // 1. If Type(V) is not Object, throw a TypeError. if (webidl.util.Type(V) !== 'Object') { throw webidl.errors.exception({ - header: 'Sequence', - message: `Value of type ${webidl.util.Type(V)} is not an Object.` + header: prefix, + message: `${argument} (${webidl.util.Stringify(V)}) is not an Object.` }) } @@ -253,8 +257,8 @@ webidl.sequenceConverter = function (converter) { typeof method.next !== 'function' ) { throw webidl.errors.exception({ - header: 'Sequence', - message: 'Object is not an iterator.' + header: prefix, + message: `${argument} is not iterable.` }) } @@ -266,7 +270,7 @@ webidl.sequenceConverter = function (converter) { break } - seq.push(converter(value)) + seq.push(converter(value, prefix, argument)) } return seq @@ -275,12 +279,12 @@ webidl.sequenceConverter = function (converter) { // https://webidl.spec.whatwg.org/#es-to-record webidl.recordConverter = function (keyConverter, valueConverter) { - return (O) => { + return (O, prefix, argument) => { // 1. If Type(O) is not Object, throw a TypeError. if (webidl.util.Type(O) !== 'Object') { throw webidl.errors.exception({ - header: 'Record', - message: `Value of type ${webidl.util.Type(O)} is not an Object.` + header: prefix, + message: `${argument} ("${webidl.util.Type(O)}") is not an Object.` }) } @@ -293,11 +297,11 @@ webidl.recordConverter = function (keyConverter, valueConverter) { for (const key of keys) { // 1. Let typedKey be key converted to an IDL value of type K. - const typedKey = keyConverter(key) + const typedKey = keyConverter(key, prefix, argument) // 2. Let value be ? Get(O, key). // 3. Let typedValue be value converted to an IDL value of type V. - const typedValue = valueConverter(O[key]) + const typedValue = valueConverter(O[key], prefix, argument) // 4. Set result[typedKey] to typedValue. result[typedKey] = typedValue @@ -318,11 +322,11 @@ webidl.recordConverter = function (keyConverter, valueConverter) { // 2. If desc is not undefined and desc.[[Enumerable]] is true: if (desc?.enumerable) { // 1. Let typedKey be key converted to an IDL value of type K. - const typedKey = keyConverter(key) + const typedKey = keyConverter(key, prefix, argument) // 2. Let value be ? Get(O, key). // 3. Let typedValue be value converted to an IDL value of type V. - const typedValue = valueConverter(O[key]) + const typedValue = valueConverter(O[key], prefix, argument) // 4. Set result[typedKey] to typedValue. result[typedKey] = typedValue @@ -335,11 +339,11 @@ webidl.recordConverter = function (keyConverter, valueConverter) { } webidl.interfaceConverter = function (i) { - return (V, opts = {}) => { - if (opts.strict !== false && !(V instanceof i)) { + return (V, prefix, argument, opts) => { + if (opts?.strict !== false && !(V instanceof i)) { throw webidl.errors.exception({ - header: i.name, - message: `Expected ${webidl.util.Stringify(V)} to be an instance of ${i.name}.` + header: prefix, + message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${i.name}.` }) } @@ -348,7 +352,7 @@ webidl.interfaceConverter = function (i) { } webidl.dictionaryConverter = function (converters) { - return (dictionary) => { + return (dictionary, prefix, argument) => { const type = webidl.util.Type(dictionary) const dict = {} @@ -356,7 +360,7 @@ webidl.dictionaryConverter = function (converters) { return dict } else if (type !== 'Object') { throw webidl.errors.exception({ - header: 'Dictionary', + header: prefix, message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` }) } @@ -367,7 +371,7 @@ webidl.dictionaryConverter = function (converters) { if (required === true) { if (!Object.hasOwn(dictionary, key)) { throw webidl.errors.exception({ - header: 'Dictionary', + header: prefix, message: `Missing required key "${key}".` }) } @@ -379,21 +383,21 @@ webidl.dictionaryConverter = function (converters) { // Only use defaultValue if value is undefined and // a defaultValue options was provided. if (hasDefault && value !== null) { - value = value ?? defaultValue + value ??= defaultValue() } // A key can be optional and have no default value. // When this happens, do not perform a conversion, // and do not assign the key a value. if (required || hasDefault || value !== undefined) { - value = converter(value) + value = converter(value, prefix, argument) if ( options.allowedValues && !options.allowedValues.includes(value) ) { throw webidl.errors.exception({ - header: 'Dictionary', + header: prefix, message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` }) } @@ -407,28 +411,31 @@ webidl.dictionaryConverter = function (converters) { } webidl.nullableConverter = function (converter) { - return (V) => { + return (V, prefix, argument) => { if (V === null) { return V } - return converter(V) + return converter(V, prefix, argument) } } // https://webidl.spec.whatwg.org/#es-DOMString -webidl.converters.DOMString = function (V, opts = {}) { +webidl.converters.DOMString = function (V, prefix, argument, opts) { // 1. If V is null and the conversion is to an IDL type // associated with the [LegacyNullToEmptyString] // extended attribute, then return the DOMString value // that represents the empty string. - if (V === null && opts.legacyNullToEmptyString) { + if (V === null && opts?.legacyNullToEmptyString) { return '' } // 2. Let x be ? ToString(V). if (typeof V === 'symbol') { - throw new TypeError('Could not convert argument of type symbol to string.') + throw webidl.errors.exception({ + header: prefix, + message: `${argument} is a symbol, which cannot be converted to a DOMString.` + }) } // 3. Return the IDL DOMString value that represents the @@ -438,10 +445,10 @@ webidl.converters.DOMString = function (V, opts = {}) { } // https://webidl.spec.whatwg.org/#es-ByteString -webidl.converters.ByteString = function (V) { +webidl.converters.ByteString = function (V, prefix, argument) { // 1. Let x be ? ToString(V). // Note: DOMString converter perform ? ToString(V) - const x = webidl.converters.DOMString(V) + const x = webidl.converters.DOMString(V, prefix, argument) // 2. If the value of any element of x is greater than // 255, then throw a TypeError. @@ -461,6 +468,7 @@ webidl.converters.ByteString = function (V) { } // https://webidl.spec.whatwg.org/#es-USVString +// TODO: rewrite this so we can control the errors thrown webidl.converters.USVString = toUSVString // https://webidl.spec.whatwg.org/#es-boolean @@ -479,9 +487,9 @@ webidl.converters.any = function (V) { } // https://webidl.spec.whatwg.org/#es-long-long -webidl.converters['long long'] = function (V) { +webidl.converters['long long'] = function (V, prefix, argument) { // 1. Let x be ? ConvertToInt(V, 64, "signed"). - const x = webidl.util.ConvertToInt(V, 64, 'signed') + const x = webidl.util.ConvertToInt(V, 64, 'signed', undefined, prefix, argument) // 2. Return the IDL long long value that represents // the same numeric value as x. @@ -489,9 +497,9 @@ webidl.converters['long long'] = function (V) { } // https://webidl.spec.whatwg.org/#es-unsigned-long-long -webidl.converters['unsigned long long'] = function (V) { +webidl.converters['unsigned long long'] = function (V, prefix, argument) { // 1. Let x be ? ConvertToInt(V, 64, "unsigned"). - const x = webidl.util.ConvertToInt(V, 64, 'unsigned') + const x = webidl.util.ConvertToInt(V, 64, 'unsigned', undefined, prefix, argument) // 2. Return the IDL unsigned long long value that // represents the same numeric value as x. @@ -499,9 +507,9 @@ webidl.converters['unsigned long long'] = function (V) { } // https://webidl.spec.whatwg.org/#es-unsigned-long -webidl.converters['unsigned long'] = function (V) { +webidl.converters['unsigned long'] = function (V, prefix, argument) { // 1. Let x be ? ConvertToInt(V, 32, "unsigned"). - const x = webidl.util.ConvertToInt(V, 32, 'unsigned') + const x = webidl.util.ConvertToInt(V, 32, 'unsigned', undefined, prefix, argument) // 2. Return the IDL unsigned long value that // represents the same numeric value as x. @@ -509,9 +517,9 @@ webidl.converters['unsigned long'] = function (V) { } // https://webidl.spec.whatwg.org/#es-unsigned-short -webidl.converters['unsigned short'] = function (V, opts) { +webidl.converters['unsigned short'] = function (V, prefix, argument, opts) { // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). - const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts) + const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts, prefix, argument) // 2. Return the IDL unsigned short value that represents // the same numeric value as x. @@ -519,7 +527,7 @@ webidl.converters['unsigned short'] = function (V, opts) { } // https://webidl.spec.whatwg.org/#idl-ArrayBuffer -webidl.converters.ArrayBuffer = function (V, opts = {}) { +webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) { // 1. If Type(V) is not Object, or V does not have an // [[ArrayBufferData]] internal slot, then throw a // TypeError. @@ -530,8 +538,8 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) { !types.isAnyArrayBuffer(V) ) { throw webidl.errors.conversionFailed({ - prefix: webidl.util.Stringify(V), - argument: webidl.util.Stringify(V), + prefix, + argument: `${argument} ("${webidl.util.Stringify(V)}")`, types: ['ArrayBuffer'] }) } @@ -540,7 +548,7 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) { // with the [AllowShared] extended attribute, and // IsSharedArrayBuffer(V) is true, then throw a // TypeError. - if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { + if (opts?.allowShared === false && types.isSharedArrayBuffer(V)) { throw webidl.errors.exception({ header: 'ArrayBuffer', message: 'SharedArrayBuffer is not allowed.' @@ -563,7 +571,7 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) { return V } -webidl.converters.TypedArray = function (V, T, opts = {}) { +webidl.converters.TypedArray = function (V, T, prefix, name, opts) { // 1. Let T be the IDL type V is being converted to. // 2. If Type(V) is not Object, or V does not have a @@ -575,8 +583,8 @@ webidl.converters.TypedArray = function (V, T, opts = {}) { V.constructor.name !== T.name ) { throw webidl.errors.conversionFailed({ - prefix: `${T.name}`, - argument: webidl.util.Stringify(V), + prefix, + argument: `${name} ("${webidl.util.Stringify(V)}")`, types: [T.name] }) } @@ -585,7 +593,7 @@ webidl.converters.TypedArray = function (V, T, opts = {}) { // with the [AllowShared] extended attribute, and // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is // true, then throw a TypeError. - if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { throw webidl.errors.exception({ header: 'ArrayBuffer', message: 'SharedArrayBuffer is not allowed.' @@ -608,13 +616,13 @@ webidl.converters.TypedArray = function (V, T, opts = {}) { return V } -webidl.converters.DataView = function (V, opts = {}) { +webidl.converters.DataView = function (V, prefix, name, opts) { // 1. If Type(V) is not Object, or V does not have a // [[DataView]] internal slot, then throw a TypeError. if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { throw webidl.errors.exception({ - header: 'DataView', - message: 'Object is not a DataView.' + header: prefix, + message: `${name} is not a DataView.` }) } @@ -622,7 +630,7 @@ webidl.converters.DataView = function (V, opts = {}) { // with the [AllowShared] extended attribute, and // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, // then throw a TypeError. - if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { throw webidl.errors.exception({ header: 'ArrayBuffer', message: 'SharedArrayBuffer is not allowed.' @@ -646,20 +654,24 @@ webidl.converters.DataView = function (V, opts = {}) { } // https://webidl.spec.whatwg.org/#BufferSource -webidl.converters.BufferSource = function (V, opts = {}) { +webidl.converters.BufferSource = function (V, prefix, name, opts) { if (types.isAnyArrayBuffer(V)) { - return webidl.converters.ArrayBuffer(V, { ...opts, allowShared: false }) + return webidl.converters.ArrayBuffer(V, prefix, name, { ...opts, allowShared: false }) } if (types.isTypedArray(V)) { - return webidl.converters.TypedArray(V, V.constructor, { ...opts, allowShared: false }) + return webidl.converters.TypedArray(V, V.constructor, prefix, name, { ...opts, allowShared: false }) } if (types.isDataView(V)) { - return webidl.converters.DataView(V, opts, { ...opts, allowShared: false }) + return webidl.converters.DataView(V, prefix, name, { ...opts, allowShared: false }) } - throw new TypeError(`Could not convert ${webidl.util.Stringify(V)} to a BufferSource.`) + throw webidl.errors.conversionFailed({ + prefix, + argument: `${name} ("${webidl.util.Stringify(V)}")`, + types: ['BufferSource'] + }) } webidl.converters['sequence'] = webidl.sequenceConverter( diff --git a/lib/web/fileapi/filereader.js b/lib/web/fileapi/filereader.js index 0cca813994e..ccebe692a6f 100644 --- a/lib/web/fileapi/filereader.js +++ b/lib/web/fileapi/filereader.js @@ -39,7 +39,7 @@ class FileReader extends EventTarget { readAsArrayBuffer (blob) { webidl.brandCheck(this, FileReader) - webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsArrayBuffer' }) + webidl.argumentLengthCheck(arguments, 1, 'FileReader.readAsArrayBuffer') blob = webidl.converters.Blob(blob, { strict: false }) @@ -55,7 +55,7 @@ class FileReader extends EventTarget { readAsBinaryString (blob) { webidl.brandCheck(this, FileReader) - webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsBinaryString' }) + webidl.argumentLengthCheck(arguments, 1, 'FileReader.readAsBinaryString') blob = webidl.converters.Blob(blob, { strict: false }) @@ -72,12 +72,12 @@ class FileReader extends EventTarget { readAsText (blob, encoding = undefined) { webidl.brandCheck(this, FileReader) - webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsText' }) + webidl.argumentLengthCheck(arguments, 1, 'FileReader.readAsText') blob = webidl.converters.Blob(blob, { strict: false }) if (encoding !== undefined) { - encoding = webidl.converters.DOMString(encoding) + encoding = webidl.converters.DOMString(encoding, 'FileReader.readAsText', 'encoding') } // The readAsText(blob, encoding) method, when invoked, @@ -92,7 +92,7 @@ class FileReader extends EventTarget { readAsDataURL (blob) { webidl.brandCheck(this, FileReader) - webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsDataURL' }) + webidl.argumentLengthCheck(arguments, 1, 'FileReader.readAsDataURL') blob = webidl.converters.Blob(blob, { strict: false }) diff --git a/lib/web/fileapi/progressevent.js b/lib/web/fileapi/progressevent.js index 778cf224c6a..2d09d18107d 100644 --- a/lib/web/fileapi/progressevent.js +++ b/lib/web/fileapi/progressevent.js @@ -9,7 +9,7 @@ const kState = Symbol('ProgressEvent state') */ class ProgressEvent extends Event { constructor (type, eventInitDict = {}) { - type = webidl.converters.DOMString(type) + type = webidl.converters.DOMString(type, 'ProgressEvent constructor', 'type') eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {}) super(type, eventInitDict) @@ -44,32 +44,32 @@ webidl.converters.ProgressEventInit = webidl.dictionaryConverter([ { key: 'lengthComputable', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'loaded', converter: webidl.converters['unsigned long long'], - defaultValue: 0 + defaultValue: () => 0 }, { key: 'total', converter: webidl.converters['unsigned long long'], - defaultValue: 0 + defaultValue: () => 0 }, { key: 'bubbles', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'cancelable', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'composed', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false } ]) diff --git a/lib/web/websocket/events.js b/lib/web/websocket/events.js index 7b3bb263c61..760b7297359 100644 --- a/lib/web/websocket/events.js +++ b/lib/web/websocket/events.js @@ -17,10 +17,11 @@ class MessageEvent extends Event { return } - webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent constructor' }) + const prefix = 'MessageEvent constructor' + webidl.argumentLengthCheck(arguments, 1, prefix) - type = webidl.converters.DOMString(type) - eventInitDict = webidl.converters.MessageEventInit(eventInitDict) + type = webidl.converters.DOMString(type, prefix, 'type') + eventInitDict = webidl.converters.MessageEventInit(eventInitDict, prefix, 'eventInitDict') super(type, eventInitDict) @@ -73,7 +74,7 @@ class MessageEvent extends Event { ) { webidl.brandCheck(this, MessageEvent) - webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent.initMessageEvent' }) + webidl.argumentLengthCheck(arguments, 1, 'MessageEvent.initMessageEvent') return new MessageEvent(type, { bubbles, cancelable, data, origin, lastEventId, source, ports @@ -102,9 +103,10 @@ class CloseEvent extends Event { #eventInit constructor (type, eventInitDict = {}) { - webidl.argumentLengthCheck(arguments, 1, { header: 'CloseEvent constructor' }) + const prefix = 'CloseEvent constructor' + webidl.argumentLengthCheck(arguments, 1, prefix) - type = webidl.converters.DOMString(type) + type = webidl.converters.DOMString(type, prefix, 'type') eventInitDict = webidl.converters.CloseEventInit(eventInitDict) super(type, eventInitDict) @@ -136,11 +138,12 @@ class ErrorEvent extends Event { #eventInit constructor (type, eventInitDict) { - webidl.argumentLengthCheck(arguments, 1, { header: 'ErrorEvent constructor' }) + const prefix = 'ErrorEvent constructor' + webidl.argumentLengthCheck(arguments, 1, prefix) super(type, eventInitDict) - type = webidl.converters.DOMString(type) + type = webidl.converters.DOMString(type, prefix, 'type') eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {}) this.#eventInit = eventInitDict @@ -222,17 +225,17 @@ const eventInit = [ { key: 'bubbles', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'cancelable', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'composed', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false } ] @@ -241,31 +244,29 @@ webidl.converters.MessageEventInit = webidl.dictionaryConverter([ { key: 'data', converter: webidl.converters.any, - defaultValue: null + defaultValue: () => null }, { key: 'origin', converter: webidl.converters.USVString, - defaultValue: '' + defaultValue: () => '' }, { key: 'lastEventId', converter: webidl.converters.DOMString, - defaultValue: '' + defaultValue: () => '' }, { key: 'source', // Node doesn't implement WindowProxy or ServiceWorker, so the only // valid value for source is a MessagePort. converter: webidl.nullableConverter(webidl.converters.MessagePort), - defaultValue: null + defaultValue: () => null }, { key: 'ports', converter: webidl.converters['sequence'], - get defaultValue () { - return [] - } + defaultValue: () => new Array(0) } ]) @@ -274,17 +275,17 @@ webidl.converters.CloseEventInit = webidl.dictionaryConverter([ { key: 'wasClean', converter: webidl.converters.boolean, - defaultValue: false + defaultValue: () => false }, { key: 'code', converter: webidl.converters['unsigned short'], - defaultValue: 0 + defaultValue: () => 0 }, { key: 'reason', converter: webidl.converters.USVString, - defaultValue: '' + defaultValue: () => '' } ]) @@ -293,22 +294,22 @@ webidl.converters.ErrorEventInit = webidl.dictionaryConverter([ { key: 'message', converter: webidl.converters.DOMString, - defaultValue: '' + defaultValue: () => '' }, { key: 'filename', converter: webidl.converters.USVString, - defaultValue: '' + defaultValue: () => '' }, { key: 'lineno', converter: webidl.converters['unsigned long'], - defaultValue: 0 + defaultValue: () => 0 }, { key: 'colno', converter: webidl.converters['unsigned long'], - defaultValue: 0 + defaultValue: () => 0 }, { key: 'error', diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index c4b40b43188..00e81cbafca 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -51,7 +51,8 @@ class WebSocket extends EventTarget { constructor (url, protocols = []) { super() - webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' }) + const prefix = 'WebSocket constructor' + webidl.argumentLengthCheck(arguments, 1, prefix) if (!experimentalWarned) { experimentalWarned = true @@ -60,9 +61,9 @@ class WebSocket extends EventTarget { }) } - const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols) + const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols, prefix, 'options') - url = webidl.converters.USVString(url) + url = webidl.converters.USVString(url, prefix, 'url') protocols = options.protocols // 1. Let baseURL be this's relevant settings object's API base URL. @@ -159,12 +160,14 @@ class WebSocket extends EventTarget { close (code = undefined, reason = undefined) { webidl.brandCheck(this, WebSocket) + const prefix = 'WebSocket.close' + if (code !== undefined) { - code = webidl.converters['unsigned short'](code, { clamp: true }) + code = webidl.converters['unsigned short'](code, prefix, 'code', { clamp: true }) } if (reason !== undefined) { - reason = webidl.converters.USVString(reason) + reason = webidl.converters.USVString(reason, prefix, 'reason') } // 1. If code is present, but is neither an integer equal to 1000 nor an @@ -264,9 +267,10 @@ class WebSocket extends EventTarget { send (data) { webidl.brandCheck(this, WebSocket) - webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' }) + const prefix = 'WebSocket.send' + webidl.argumentLengthCheck(arguments, 1, prefix) - data = webidl.converters.WebSocketSendData(data) + data = webidl.converters.WebSocketSendData(data, prefix, 'data') // 1. If this's ready state is CONNECTING, then throw an // "InvalidStateError" DOMException. @@ -595,12 +599,12 @@ webidl.converters['sequence'] = webidl.sequenceConverter( webidl.converters.DOMString ) -webidl.converters['DOMString or sequence'] = function (V) { +webidl.converters['DOMString or sequence'] = function (V, prefix, argument) { if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) { return webidl.converters['sequence'](V) } - return webidl.converters.DOMString(V) + return webidl.converters.DOMString(V, prefix, argument) } // This implements the propsal made in https://github.com/whatwg/websockets/issues/42 @@ -608,16 +612,12 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([ { key: 'protocols', converter: webidl.converters['DOMString or sequence'], - get defaultValue () { - return [] - } + defaultValue: () => new Array(0) }, { key: 'dispatcher', converter: (V) => V, - get defaultValue () { - return getGlobalDispatcher() - } + defaultValue: () => getGlobalDispatcher() }, { key: 'headers', diff --git a/test/fetch/headers.js b/test/fetch/headers.js index ac1183eeded..91ba61f6e23 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -27,7 +27,7 @@ test('Headers initialization', async (t) => { throws(() => new Headers(['undici', 'fetch', 'fetch']), TypeError) throws( () => new Headers([0, 1, 2]), - TypeError('Sequence: Value of type Number is not an Object.') + TypeError('Headers contructor: init (0) is not an Object.') ) }) @@ -42,7 +42,7 @@ test('Headers initialization', async (t) => { const init = ['undici', 'fetch', 'fetch', 'undici'] throws( () => new Headers(init), - TypeError('Sequence: Value of type String is not an Object.') + TypeError('Headers contructor: init ("undici") is not an Object.') ) }) }) diff --git a/test/webidl/converters.js b/test/webidl/converters.js index f39c05a8d7b..c5f4ce88d71 100644 --- a/test/webidl/converters.js +++ b/test/webidl/converters.js @@ -12,19 +12,19 @@ test('sequence', () => { assert.deepStrictEqual(converter([1, 2, 3]), ['1', '2', '3']) assert.throws(() => { - converter(3) + converter(3, 'converter', 'converter') }, TypeError, 'disallows non-objects') assert.throws(() => { - converter(null) + converter(null, 'converter', 'converter') }, TypeError) assert.throws(() => { - converter(undefined) + converter(undefined, 'converter', 'converter') }, TypeError) assert.throws(() => { - converter({}) + converter({}, 'converter', 'converter') }, TypeError, 'no Symbol.iterator') assert.throws(() => { @@ -40,7 +40,7 @@ test('sequence', () => { next: 'never!' } } - })) + }), 'converter', 'converter') }, TypeError, 'invalid generator') }) @@ -49,12 +49,12 @@ describe('webidl.dictionaryConverter', () => { const converter = webidl.dictionaryConverter([]) assert.throws(() => { - converter(true) + converter(true, 'converter', 'converter') }, TypeError) for (const value of [{}, undefined, null]) { assert.doesNotThrow(() => { - converter(value) + converter(value, 'converter', 'converter') }) } }) @@ -69,47 +69,47 @@ describe('webidl.dictionaryConverter', () => { ]) assert.throws(() => { - converter({ wrongKey: 'key' }) + converter({ wrongKey: 'key' }, 'converter', 'converter') }, TypeError) assert.doesNotThrow(() => { - converter({ Key: 'this key was required!' }) + converter({ Key: 'this key was required!' }, 'converter', 'converter') }) }) }) test('ArrayBuffer', () => { assert.throws(() => { - webidl.converters.ArrayBuffer(true) + webidl.converters.ArrayBuffer(true, 'converter', 'converter') }, TypeError) assert.throws(() => { - webidl.converters.ArrayBuffer({}) + webidl.converters.ArrayBuffer({}, 'converter', 'converter') }, TypeError) assert.throws(() => { const sab = new SharedArrayBuffer(1024) - webidl.converters.ArrayBuffer(sab, { allowShared: false }) + webidl.converters.ArrayBuffer(sab, 'converter', 'converter', { allowShared: false }) }, TypeError) assert.doesNotThrow(() => { const sab = new SharedArrayBuffer(1024) - webidl.converters.ArrayBuffer(sab) + webidl.converters.ArrayBuffer(sab, 'converter', 'converter') }) assert.doesNotThrow(() => { const ab = new ArrayBuffer(8) - webidl.converters.ArrayBuffer(ab) + webidl.converters.ArrayBuffer(ab, 'converter', 'converter') }) }) test('TypedArray', () => { assert.throws(() => { - webidl.converters.TypedArray(3) + webidl.converters.TypedArray(3, 'converter', 'converter') }, TypeError) assert.throws(() => { - webidl.converters.TypedArray({}) + webidl.converters.TypedArray({}, 'converter', 'converter') }, TypeError) assert.throws(() => { @@ -120,7 +120,7 @@ test('TypedArray', () => { } }) - webidl.converters.TypedArray(uint8, Uint8Array, { + webidl.converters.TypedArray(uint8, Uint8Array, 'converter', 'converter', { allowShared: false }) }, TypeError) @@ -128,11 +128,11 @@ test('TypedArray', () => { test('DataView', () => { assert.throws(() => { - webidl.converters.DataView(3) + webidl.converters.DataView(3, 'converter', 'converter') }, TypeError) assert.throws(() => { - webidl.converters.DataView({}) + webidl.converters.DataView({}, 'converter', 'converter') }, TypeError) assert.throws(() => { @@ -145,7 +145,7 @@ test('DataView', () => { } }) - webidl.converters.DataView(view, { + webidl.converters.DataView(view, 'converter', 'converter', { allowShared: false }) }) @@ -153,7 +153,7 @@ test('DataView', () => { const buffer = new ArrayBuffer(16) const view = new DataView(buffer, 0) - assert.equal(webidl.converters.DataView(view), view) + assert.equal(webidl.converters.DataView(view, 'converter', 'converter'), view) }) test('BufferSource', () => { @@ -161,23 +161,23 @@ test('BufferSource', () => { const buffer = new ArrayBuffer(16) const view = new DataView(buffer, 0) - webidl.converters.BufferSource(view) + webidl.converters.BufferSource(view, 'converter', 'converter') }) assert.throws(() => { - webidl.converters.BufferSource(3) + webidl.converters.BufferSource(3, 'converter', 'converter') }, TypeError) }) test('ByteString', () => { assert.doesNotThrow(() => { - webidl.converters.ByteString('') + webidl.converters.ByteString('', 'converter', 'converter') }) // https://github.com/nodejs/undici/issues/1590 assert.throws(() => { const char = String.fromCharCode(256) - webidl.converters.ByteString(`invalid${char}char`) + webidl.converters.ByteString(`invalid${char}char`, 'converter', 'converter') }, { message: 'Cannot convert argument to a ByteString because the character at ' + 'index 7 has a value of 256 which is greater than 255.' diff --git a/test/webidl/errors.js b/test/webidl/errors.js new file mode 100644 index 00000000000..11449fbfd60 --- /dev/null +++ b/test/webidl/errors.js @@ -0,0 +1,23 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') +const { Headers } = require('../..') + +test('ByteString', (t) => { + const name = Symbol('') + const value = Symbol('') + + for (const method of [ + 'get', + 'set', + 'delete', + 'append', + 'has' + ]) { + assert.throws( + () => new Headers()[method](name, value), + new TypeError(`Headers.${method}: name is a symbol, which cannot be converted to a DOMString.`) + ) + } +}) diff --git a/test/webidl/helpers.js b/test/webidl/helpers.js index b18baa5fb7b..fcd375f68b4 100644 --- a/test/webidl/helpers.js +++ b/test/webidl/helpers.js @@ -11,11 +11,11 @@ test('webidl.interfaceConverter', () => { const converter = webidl.interfaceConverter(A) assert.throws(() => { - converter(new B()) + converter(new B(), 'converter', 'converter') }, TypeError) assert.doesNotThrow(() => { - converter(new A()) + converter(new A(), 'converter', 'converter') }) }) @@ -38,7 +38,7 @@ describe('webidl.dictionaryConverter', () => { get value () { return 6 } - }), + }, 'converter', 'converter'), { key: 'string' } ) }) @@ -52,7 +52,7 @@ describe('webidl.dictionaryConverter', () => { } ]) - assert.deepStrictEqual(converter({ key: null }), { key: 0 }) + assert.deepStrictEqual(converter({ key: null }, 'converter', 'converter'), { key: 0 }) }) test('no defaultValue and optional', () => { @@ -63,6 +63,6 @@ describe('webidl.dictionaryConverter', () => { } ]) - assert.deepStrictEqual(converter({ a: 'b', c: 'd' }), {}) + assert.deepStrictEqual(converter({ a: 'b', c: 'd' }, 'converter', 'converter'), {}) }) }) diff --git a/test/websocket/messageevent.js b/test/websocket/messageevent.js index de3b6c5fc8e..b1381019c27 100644 --- a/test/websocket/messageevent.js +++ b/test/websocket/messageevent.js @@ -99,25 +99,25 @@ test('test/parallel/test-worker-message-port.js', () => { assert.throws(() => new MessageEvent('message', { source: 1 }), { constructor: TypeError, - message: 'MessagePort: Expected 1 to be an instance of MessagePort.' + message: 'MessageEvent constructor: Expected eventInitDict ("1") to be an instance of MessagePort.' }) assert.throws(() => new MessageEvent('message', { source: {} }), { constructor: TypeError, - message: 'MessagePort: Expected {} to be an instance of MessagePort.' + message: 'MessageEvent constructor: Expected eventInitDict ("{}") to be an instance of MessagePort.' }) assert.throws(() => new MessageEvent('message', { ports: 0 }), { constructor: TypeError, - message: 'Sequence: Value of type Number is not an Object.' + message: 'MessageEvent constructor: eventInitDict (0) is not an Object.' }) assert.throws(() => new MessageEvent('message', { ports: [null] }), { constructor: TypeError, - message: 'MessagePort: Expected null to be an instance of MessagePort.' + message: 'MessageEvent constructor: Expected eventInitDict ("null") to be an instance of MessagePort.' }) assert.throws(() => new MessageEvent('message', { ports: [{}] }) , { constructor: TypeError, - message: 'MessagePort: Expected {} to be an instance of MessagePort.' + message: 'MessageEvent constructor: Expected eventInitDict ("{}") to be an instance of MessagePort.' }) assert(new MessageEvent('message') instanceof Event) diff --git a/types/webidl.d.ts b/types/webidl.d.ts index 1e362d6f40d..fe802a20124 100644 --- a/types/webidl.d.ts +++ b/types/webidl.d.ts @@ -55,7 +55,9 @@ interface WebidlUtil { V: unknown, bitLength: number, signedness: 'signed' | 'unsigned', - opts?: ConvertToIntOpts + opts?: ConvertToIntOpts, + prefix: string, + argument: string ): number /** @@ -73,14 +75,14 @@ interface WebidlConverters { /** * @see https://webidl.spec.whatwg.org/#es-DOMString */ - DOMString (V: unknown, opts?: { + DOMString (V: unknown, prefix: string, argument: string, opts?: { legacyNullToEmptyString: boolean }): string /** * @see https://webidl.spec.whatwg.org/#es-ByteString */ - ByteString (V: unknown): string + ByteString (V: unknown, prefix: string, argument: string): string /** * @see https://webidl.spec.whatwg.org/#es-USVString @@ -204,7 +206,7 @@ export interface Webidl { */ dictionaryConverter (converters: { key: string, - defaultValue?: unknown, + defaultValue?: () => unknown, required?: boolean, converter: (...args: unknown[]) => unknown, allowedValues?: unknown[] @@ -218,8 +220,5 @@ export interface Webidl { converter: Converter ): (V: unknown) => ReturnType | null - argumentLengthCheck (args: { length: number }, min: number, context: { - header: string - message?: string - }): void + argumentLengthCheck (args: { length: number }, min: number, context: string): void } From 73822d87940364a8e771da32592accc86af1855f Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 29 Apr 2024 03:43:39 -0400 Subject: [PATCH 19/20] preserve dictionary key name in webidl errors (#3176) * preserve dictionary key name in webidl errors * a sequence is not *iterable* --- lib/web/fetch/webidl.js | 4 ++-- test/fetch/headers.js | 4 ++-- test/types/dispatcher.test-d.ts | 2 +- test/webidl/errors.js | 13 +++++++++++-- test/websocket/messageevent.js | 10 +++++----- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/web/fetch/webidl.js b/lib/web/fetch/webidl.js index c90e411afca..96ec7767a1a 100644 --- a/lib/web/fetch/webidl.js +++ b/lib/web/fetch/webidl.js @@ -242,7 +242,7 @@ webidl.sequenceConverter = function (converter) { if (webidl.util.Type(V) !== 'Object') { throw webidl.errors.exception({ header: prefix, - message: `${argument} (${webidl.util.Stringify(V)}) is not an Object.` + message: `${argument} (${webidl.util.Stringify(V)}) is not iterable.` }) } @@ -390,7 +390,7 @@ webidl.dictionaryConverter = function (converters) { // When this happens, do not perform a conversion, // and do not assign the key a value. if (required || hasDefault || value !== undefined) { - value = converter(value, prefix, argument) + value = converter(value, prefix, `${argument}.${key}`) if ( options.allowedValues && diff --git a/test/fetch/headers.js b/test/fetch/headers.js index 91ba61f6e23..b5574ac03df 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -27,7 +27,7 @@ test('Headers initialization', async (t) => { throws(() => new Headers(['undici', 'fetch', 'fetch']), TypeError) throws( () => new Headers([0, 1, 2]), - TypeError('Headers contructor: init (0) is not an Object.') + TypeError('Headers contructor: init (0) is not iterable.') ) }) @@ -42,7 +42,7 @@ test('Headers initialization', async (t) => { const init = ['undici', 'fetch', 'fetch', 'undici'] throws( () => new Headers(init), - TypeError('Headers contructor: init ("undici") is not an Object.') + TypeError('Headers contructor: init ("undici") is not iterable.') ) }) }) diff --git a/test/types/dispatcher.test-d.ts b/test/types/dispatcher.test-d.ts index 4e9aabc93bc..3185726bbb7 100644 --- a/test/types/dispatcher.test-d.ts +++ b/test/types/dispatcher.test-d.ts @@ -1,7 +1,7 @@ import { IncomingHttpHeaders } from 'http' import { Duplex, Readable, Writable } from 'stream' import { expectAssignable, expectType } from 'tsd' -import { Dispatcher } from '../..' +import { Dispatcher, Headers } from '../..' import { URL } from 'url' import { Blob } from 'buffer' diff --git a/test/webidl/errors.js b/test/webidl/errors.js index 11449fbfd60..20a73a765a3 100644 --- a/test/webidl/errors.js +++ b/test/webidl/errors.js @@ -1,8 +1,8 @@ 'use strict' -const { test } = require('node:test') +const { test, describe } = require('node:test') const assert = require('node:assert') -const { Headers } = require('../..') +const { Headers, MessageEvent } = require('../..') test('ByteString', (t) => { const name = Symbol('') @@ -21,3 +21,12 @@ test('ByteString', (t) => { ) } }) + +describe('dictionary converters', () => { + test('error message retains property name', () => { + assert.throws( + () => new MessageEvent('message', { source: 1 }), + new TypeError('MessageEvent constructor: Expected eventInitDict.source ("1") to be an instance of MessagePort.') + ) + }) +}) diff --git a/test/websocket/messageevent.js b/test/websocket/messageevent.js index b1381019c27..ae8c2227336 100644 --- a/test/websocket/messageevent.js +++ b/test/websocket/messageevent.js @@ -99,25 +99,25 @@ test('test/parallel/test-worker-message-port.js', () => { assert.throws(() => new MessageEvent('message', { source: 1 }), { constructor: TypeError, - message: 'MessageEvent constructor: Expected eventInitDict ("1") to be an instance of MessagePort.' + message: 'MessageEvent constructor: Expected eventInitDict.source ("1") to be an instance of MessagePort.' }) assert.throws(() => new MessageEvent('message', { source: {} }), { constructor: TypeError, - message: 'MessageEvent constructor: Expected eventInitDict ("{}") to be an instance of MessagePort.' + message: 'MessageEvent constructor: Expected eventInitDict.source ("{}") to be an instance of MessagePort.' }) assert.throws(() => new MessageEvent('message', { ports: 0 }), { constructor: TypeError, - message: 'MessageEvent constructor: eventInitDict (0) is not an Object.' + message: 'MessageEvent constructor: eventInitDict.ports (0) is not iterable.' }) assert.throws(() => new MessageEvent('message', { ports: [null] }), { constructor: TypeError, - message: 'MessageEvent constructor: Expected eventInitDict ("null") to be an instance of MessagePort.' + message: 'MessageEvent constructor: Expected eventInitDict.ports ("null") to be an instance of MessagePort.' }) assert.throws(() => new MessageEvent('message', { ports: [{}] }) , { constructor: TypeError, - message: 'MessageEvent constructor: Expected eventInitDict ("{}") to be an instance of MessagePort.' + message: 'MessageEvent constructor: Expected eventInitDict.ports ("{}") to be an instance of MessagePort.' }) assert(new MessageEvent('message') instanceof Event) From 41dc36c845ce1386d0700bba099879ef355648d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 14:00:36 +0200 Subject: [PATCH 20/20] Bumped v6.15.0 (#3177) Co-authored-by: github-actions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be03f23267f..2624ec452ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "6.14.1", + "version": "6.15.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": {