undici 5.12.0 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -178,10 +178,6 @@ Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
178
178
 
179
179
  Only supported on Node 16.8+.
180
180
 
181
- This is [experimental](https://nodejs.org/api/documentation.html#documentation_stability_index) and is not yet fully compliant with the Fetch Standard.
182
- We plan to ship breaking changes to this feature until it is out of experimental.
183
- Help us improve the test coverage by following instructions at [nodejs/undici/#951](https://github.com/nodejs/undici/issues/951).
184
-
185
181
  Basic usage example:
186
182
 
187
183
  ```js
@@ -234,9 +230,15 @@ const data = {
234
230
  },
235
231
  }
236
232
 
237
- await fetch('https://example.com', { body: data, method: 'POST' })
233
+ await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
238
234
  ```
239
235
 
236
+ #### `request.duplex`
237
+
238
+ - half
239
+
240
+ In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`. And fetch requests are currently always be full duplex. More detail refer to [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex)
241
+
240
242
  #### `response.body`
241
243
 
242
244
  Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
Binary file
package/index.d.ts CHANGED
@@ -1,19 +1,19 @@
1
- import Dispatcher = require('./types/dispatcher')
1
+ import Dispatcher from'./types/dispatcher'
2
2
  import { setGlobalDispatcher, getGlobalDispatcher } from './types/global-dispatcher'
3
3
  import { setGlobalOrigin, getGlobalOrigin } from './types/global-origin'
4
- import Pool = require('./types/pool')
4
+ import Pool from'./types/pool'
5
5
  import { RedirectHandler, DecoratorHandler } from './types/handlers'
6
6
 
7
- import BalancedPool = require('./types/balanced-pool')
8
- import Client = require('./types/client')
9
- import buildConnector = require('./types/connector')
10
- import errors = require('./types/errors')
11
- import Agent = require('./types/agent')
12
- import MockClient = require('./types/mock-client')
13
- import MockPool = require('./types/mock-pool')
14
- import MockAgent = require('./types/mock-agent')
15
- import mockErrors = require('./types/mock-errors')
16
- import ProxyAgent = require('./types/proxy-agent')
7
+ import BalancedPool from './types/balanced-pool'
8
+ import Client from'./types/client'
9
+ import buildConnector from'./types/connector'
10
+ import errors from'./types/errors'
11
+ import Agent from'./types/agent'
12
+ import MockClient from'./types/mock-client'
13
+ import MockPool from'./types/mock-pool'
14
+ import MockAgent from'./types/mock-agent'
15
+ import mockErrors from'./types/mock-errors'
16
+ import ProxyAgent from'./types/proxy-agent'
17
17
  import { request, pipeline, stream, connect, upgrade } from './types/api'
18
18
 
19
19
  export * from './types/fetch'
@@ -27,16 +27,16 @@ export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent,
27
27
  export default Undici
28
28
 
29
29
  declare namespace Undici {
30
- var Dispatcher: typeof import('./types/dispatcher')
31
- var Pool: typeof import('./types/pool');
30
+ var Dispatcher: typeof import('./types/dispatcher').default
31
+ var Pool: typeof import('./types/pool').default;
32
32
  var RedirectHandler: typeof import ('./types/handlers').RedirectHandler
33
33
  var DecoratorHandler: typeof import ('./types/handlers').DecoratorHandler
34
34
  var createRedirectInterceptor: typeof import ('./types/interceptors').createRedirectInterceptor
35
- var BalancedPool: typeof import('./types/balanced-pool');
36
- var Client: typeof import('./types/client');
37
- var buildConnector: typeof import('./types/connector');
38
- var errors: typeof import('./types/errors');
39
- var Agent: typeof import('./types/agent');
35
+ var BalancedPool: typeof import('./types/balanced-pool').default;
36
+ var Client: typeof import('./types/client').default;
37
+ var buildConnector: typeof import('./types/connector').default;
38
+ var errors: typeof import('./types/errors').default;
39
+ var Agent: typeof import('./types/agent').default;
40
40
  var setGlobalDispatcher: typeof import('./types/global-dispatcher').setGlobalDispatcher;
41
41
  var getGlobalDispatcher: typeof import('./types/global-dispatcher').getGlobalDispatcher;
42
42
  var request: typeof import('./types/api').request;
@@ -44,9 +44,9 @@ declare namespace Undici {
44
44
  var pipeline: typeof import('./types/api').pipeline;
45
45
  var connect: typeof import('./types/api').connect;
46
46
  var upgrade: typeof import('./types/api').upgrade;
47
- var MockClient: typeof import('./types/mock-client');
48
- var MockPool: typeof import('./types/mock-pool');
49
- var MockAgent: typeof import('./types/mock-agent');
50
- var mockErrors: typeof import('./types/mock-errors');
47
+ var MockClient: typeof import('./types/mock-client').default;
48
+ var MockPool: typeof import('./types/mock-pool').default;
49
+ var MockAgent: typeof import('./types/mock-agent').default;
50
+ var mockErrors: typeof import('./types/mock-errors').default;
51
51
  var fetch: typeof import('./types/fetch').fetch;
52
52
  }
package/lib/fetch/body.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Busboy = require('busboy')
4
4
  const util = require('../core/util')
5
- const { ReadableStreamFrom, toUSVString, isBlobLike, isReadableStreamLike, readableStreamClose } = require('./util')
5
+ const { ReadableStreamFrom, isBlobLike, isReadableStreamLike, readableStreamClose } = require('./util')
6
6
  const { FormData } = require('./formdata')
7
7
  const { kState } = require('./symbols')
8
8
  const { webidl } = require('./webidl')
@@ -66,9 +66,13 @@ function extractBody (object, keepalive = false) {
66
66
  let type = null
67
67
 
68
68
  // 10. Switch on object:
69
- if (object == null) {
70
- // Note: The IDL processor cannot handle this situation. See
71
- // https://crbug.com/335871.
69
+ if (typeof object === 'string') {
70
+ // Set source to the UTF-8 encoding of object.
71
+ // Note: setting source to a Uint8Array here breaks some mocking assumptions.
72
+ source = object
73
+
74
+ // Set type to `text/plain;charset=UTF-8`.
75
+ type = 'text/plain;charset=UTF-8'
72
76
  } else if (object instanceof URLSearchParams) {
73
77
  // URLSearchParams
74
78
 
@@ -126,7 +130,8 @@ function extractBody (object, keepalive = false) {
126
130
 
127
131
  yield * value.stream()
128
132
 
129
- yield enc.encode('\r\n')
133
+ // '\r\n' encoded
134
+ yield new Uint8Array([13, 10])
130
135
  }
131
136
  }
132
137
 
@@ -157,6 +162,11 @@ function extractBody (object, keepalive = false) {
157
162
  if (object.type) {
158
163
  type = object.type
159
164
  }
165
+ } else if (object instanceof Uint8Array) {
166
+ // byte sequence
167
+
168
+ // Set source to object.
169
+ source = object
160
170
  } else if (typeof object[Symbol.asyncIterator] === 'function') {
161
171
  // If keepalive is true, then throw a TypeError.
162
172
  if (keepalive) {
@@ -172,17 +182,10 @@ function extractBody (object, keepalive = false) {
172
182
 
173
183
  stream =
174
184
  object instanceof ReadableStream ? object : ReadableStreamFrom(object)
175
- } else {
176
- // TODO: byte sequence?
177
- // TODO: scalar value string?
178
- // TODO: else?
179
- source = toUSVString(object)
180
- type = 'text/plain;charset=UTF-8'
181
185
  }
182
186
 
183
187
  // 11. If source is a byte sequence, then set action to a
184
188
  // step that returns source and length to source’s length.
185
- // TODO: What is a "byte sequence?"
186
189
  if (typeof source === 'string' || util.isBuffer(source)) {
187
190
  length = Buffer.byteLength(source)
188
191
  }
@@ -329,9 +332,7 @@ function bodyMixinMethods (instance) {
329
332
  },
330
333
 
331
334
  async formData () {
332
- if (!(this instanceof instance)) {
333
- throw new TypeError('Illegal invocation')
334
- }
335
+ webidl.brandCheck(this, instance)
335
336
 
336
337
  throwIfAborted(this[kState])
337
338
 
@@ -433,7 +434,7 @@ function bodyMixinMethods (instance) {
433
434
  throwIfAborted(this[kState])
434
435
 
435
436
  // Otherwise, throw a TypeError.
436
- webidl.errors.exception({
437
+ throw webidl.errors.exception({
437
438
  header: `${instance.name}.formData`,
438
439
  message: 'Could not parse content as FormData.'
439
440
  })
@@ -450,11 +451,8 @@ function mixinBody (prototype) {
450
451
 
451
452
  // https://fetch.spec.whatwg.org/#concept-body-consume-body
452
453
  async function specConsumeBody (object, type, instance) {
453
- if (!(object instanceof instance)) {
454
- throw new TypeError('Illegal invocation')
455
- }
454
+ webidl.brandCheck(object, instance)
456
455
 
457
- // TODO: why is this needed?
458
456
  throwIfAborted(object[kState])
459
457
 
460
458
  // 1. If object is unusable, then return a promise rejected
@@ -8,6 +8,17 @@ const nullBodyStatus = [101, 204, 205, 304]
8
8
 
9
9
  const redirectStatus = [301, 302, 303, 307, 308]
10
10
 
11
+ // https://fetch.spec.whatwg.org/#block-bad-port
12
+ const badPorts = [
13
+ '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
14
+ '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
15
+ '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
16
+ '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
17
+ '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697',
18
+ '10080'
19
+ ]
20
+
21
+ // https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
11
22
  const referrerPolicy = [
12
23
  '',
13
24
  'no-referrer',
@@ -44,6 +55,11 @@ const requestBodyHeader = [
44
55
  'content-type'
45
56
  ]
46
57
 
58
+ // https://fetch.spec.whatwg.org/#enumdef-requestduplex
59
+ const requestDuplex = [
60
+ 'half'
61
+ ]
62
+
47
63
  // http://fetch.spec.whatwg.org/#forbidden-method
48
64
  const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']
49
65
 
@@ -108,5 +124,7 @@ module.exports = {
108
124
  redirectStatus,
109
125
  corsSafeListedMethods,
110
126
  nullBodyStatus,
111
- safeMethods
127
+ safeMethods,
128
+ badPorts,
129
+ requestDuplex
112
130
  }
@@ -1,6 +1,7 @@
1
1
  const assert = require('assert')
2
2
  const { atob } = require('buffer')
3
- const { isValidHTTPToken } = require('./util')
3
+ const { format } = require('url')
4
+ const { isValidHTTPToken, isomorphicDecode } = require('./util')
4
5
 
5
6
  const encoder = new TextEncoder()
6
7
 
@@ -54,7 +55,6 @@ function dataURLProcessor (dataURL) {
54
55
  const encodedBody = input.slice(mimeTypeLength + 1)
55
56
 
56
57
  // 10. Let body be the percent-decoding of encodedBody.
57
- /** @type {Uint8Array|string} */
58
58
  let body = stringPercentDecode(encodedBody)
59
59
 
60
60
  // 11. If mimeType ends with U+003B (;), followed by
@@ -62,7 +62,8 @@ function dataURLProcessor (dataURL) {
62
62
  // case-insensitive match for "base64", then:
63
63
  if (/;(\u0020){0,}base64$/i.test(mimeType)) {
64
64
  // 1. Let stringBody be the isomorphic decode of body.
65
- const stringBody = decodeURIComponent(new TextDecoder('utf-8').decode(body))
65
+ const stringBody = isomorphicDecode(body)
66
+
66
67
  // 2. Set body to the forgiving-base64 decode of
67
68
  // stringBody.
68
69
  body = forgivingBase64(stringBody)
@@ -111,73 +112,7 @@ function dataURLProcessor (dataURL) {
111
112
  * @param {boolean} excludeFragment
112
113
  */
113
114
  function URLSerializer (url, excludeFragment = false) {
114
- // 1. Let output be url’s scheme and U+003A (:) concatenated.
115
- let output = url.protocol
116
-
117
- // 2. If url’s host is non-null:
118
- if (url.host.length > 0) {
119
- // 1. Append "//" to output.
120
- output += '//'
121
-
122
- // 2. If url includes credentials, then:
123
- if (url.username.length > 0 || url.password.length > 0) {
124
- // 1. Append url’s username to output.
125
- output += url.username
126
-
127
- // 2. If url’s password is not the empty string, then append U+003A (:),
128
- // followed by url’s password, to output.
129
- if (url.password.length > 0) {
130
- output += ':' + url.password
131
- }
132
-
133
- // 3. Append U+0040 (@) to output.
134
- output += '@'
135
- }
136
-
137
- // 3. Append url’s host, serialized, to output.
138
- output += decodeURIComponent(url.hostname)
139
-
140
- // 4. If url’s port is non-null, append U+003A (:) followed by url’s port,
141
- // serialized, to output.
142
- if (url.port.length > 0) {
143
- output += ':' + url.port
144
- }
145
- }
146
-
147
- // 3. If url’s host is null, url does not have an opaque path,
148
- // url’s path’s size is greater than 1, and url’s path[0]
149
- // is the empty string, then append U+002F (/) followed by
150
- // U+002E (.) to output.
151
- // Note: This prevents web+demo:/.//not-a-host/ or web+demo:/path/..//not-a-host/,
152
- // when parsed and then serialized, from ending up as web+demo://not-a-host/
153
- // (they end up as web+demo:/.//not-a-host/).
154
- // Undici implementation note: url's path[0] can never be an
155
- // empty string, so we have to slightly alter what the spec says.
156
- if (
157
- url.host.length === 0 &&
158
- url.pathname.length > 1 &&
159
- url.href.slice(url.protocol.length + 1)[0] === '.'
160
- ) {
161
- output += '/.'
162
- }
163
-
164
- // 4. Append the result of URL path serializing url to output.
165
- output += url.pathname
166
-
167
- // 5. If url’s query is non-null, append U+003F (?),
168
- // followed by url’s query, to output.
169
- if (url.search.length > 0) {
170
- output += url.search
171
- }
172
-
173
- // 6. If exclude fragment is false and url’s fragment is non-null,
174
- // then append U+0023 (#), followed by url’s fragment, to output.
175
- if (excludeFragment === false && url.hash.length > 0) {
176
- output += url.hash
177
- }
178
-
179
- // 7. Return output.
180
- return output
115
+ return format(url, { fragment: !excludeFragment })
181
116
  }
182
117
 
183
118
  // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
package/lib/fetch/file.js CHANGED
@@ -13,9 +13,7 @@ class File extends Blob {
13
13
  // The File constructor is invoked with two or three parameters, depending
14
14
  // on whether the optional dictionary parameter is used. When the File()
15
15
  // constructor is invoked, user agents must run the following steps:
16
- if (arguments.length < 2) {
17
- throw new TypeError('2 arguments required')
18
- }
16
+ webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' })
19
17
 
20
18
  fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
21
19
  fileName = webidl.converters.USVString(fileName)
@@ -76,32 +74,22 @@ class File extends Blob {
76
74
  }
77
75
 
78
76
  get name () {
79
- if (!(this instanceof File)) {
80
- throw new TypeError('Illegal invocation')
81
- }
77
+ webidl.brandCheck(this, File)
82
78
 
83
79
  return this[kState].name
84
80
  }
85
81
 
86
82
  get lastModified () {
87
- if (!(this instanceof File)) {
88
- throw new TypeError('Illegal invocation')
89
- }
83
+ webidl.brandCheck(this, File)
90
84
 
91
85
  return this[kState].lastModified
92
86
  }
93
87
 
94
88
  get type () {
95
- if (!(this instanceof File)) {
96
- throw new TypeError('Illegal invocation')
97
- }
89
+ webidl.brandCheck(this, File)
98
90
 
99
91
  return this[kState].type
100
92
  }
101
-
102
- get [Symbol.toStringTag] () {
103
- return this.constructor.name
104
- }
105
93
  }
106
94
 
107
95
  class FileLike {
@@ -153,65 +141,49 @@ class FileLike {
153
141
  }
154
142
 
155
143
  stream (...args) {
156
- if (!(this instanceof FileLike)) {
157
- throw new TypeError('Illegal invocation')
158
- }
144
+ webidl.brandCheck(this, FileLike)
159
145
 
160
146
  return this[kState].blobLike.stream(...args)
161
147
  }
162
148
 
163
149
  arrayBuffer (...args) {
164
- if (!(this instanceof FileLike)) {
165
- throw new TypeError('Illegal invocation')
166
- }
150
+ webidl.brandCheck(this, FileLike)
167
151
 
168
152
  return this[kState].blobLike.arrayBuffer(...args)
169
153
  }
170
154
 
171
155
  slice (...args) {
172
- if (!(this instanceof FileLike)) {
173
- throw new TypeError('Illegal invocation')
174
- }
156
+ webidl.brandCheck(this, FileLike)
175
157
 
176
158
  return this[kState].blobLike.slice(...args)
177
159
  }
178
160
 
179
161
  text (...args) {
180
- if (!(this instanceof FileLike)) {
181
- throw new TypeError('Illegal invocation')
182
- }
162
+ webidl.brandCheck(this, FileLike)
183
163
 
184
164
  return this[kState].blobLike.text(...args)
185
165
  }
186
166
 
187
167
  get size () {
188
- if (!(this instanceof FileLike)) {
189
- throw new TypeError('Illegal invocation')
190
- }
168
+ webidl.brandCheck(this, FileLike)
191
169
 
192
170
  return this[kState].blobLike.size
193
171
  }
194
172
 
195
173
  get type () {
196
- if (!(this instanceof FileLike)) {
197
- throw new TypeError('Illegal invocation')
198
- }
174
+ webidl.brandCheck(this, FileLike)
199
175
 
200
176
  return this[kState].blobLike.type
201
177
  }
202
178
 
203
179
  get name () {
204
- if (!(this instanceof FileLike)) {
205
- throw new TypeError('Illegal invocation')
206
- }
180
+ webidl.brandCheck(this, FileLike)
207
181
 
208
182
  return this[kState].name
209
183
  }
210
184
 
211
185
  get lastModified () {
212
- if (!(this instanceof FileLike)) {
213
- throw new TypeError('Illegal invocation')
214
- }
186
+ webidl.brandCheck(this, FileLike)
215
187
 
216
188
  return this[kState].lastModified
217
189
  }
@@ -222,6 +194,10 @@ class FileLike {
222
194
  }
223
195
 
224
196
  Object.defineProperties(File.prototype, {
197
+ [Symbol.toStringTag]: {
198
+ value: 'File',
199
+ configurable: true
200
+ },
225
201
  name: kEnumerableProperty,
226
202
  lastModified: kEnumerableProperty
227
203
  })
@@ -8,11 +8,9 @@ const { Blob } = require('buffer')
8
8
 
9
9
  // https://xhr.spec.whatwg.org/#formdata
10
10
  class FormData {
11
- static name = 'FormData'
12
-
13
11
  constructor (form) {
14
12
  if (form !== undefined) {
15
- webidl.errors.conversionFailed({
13
+ throw webidl.errors.conversionFailed({
16
14
  prefix: 'FormData constructor',
17
15
  argument: 'Argument 1',
18
16
  types: ['undefined']
@@ -23,15 +21,9 @@ class FormData {
23
21
  }
24
22
 
25
23
  append (name, value, filename = undefined) {
26
- if (!(this instanceof FormData)) {
27
- throw new TypeError('Illegal invocation')
28
- }
24
+ webidl.brandCheck(this, FormData)
29
25
 
30
- if (arguments.length < 2) {
31
- throw new TypeError(
32
- `Failed to execute 'append' on 'FormData': 2 arguments required, but only ${arguments.length} present.`
33
- )
34
- }
26
+ webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' })
35
27
 
36
28
  if (arguments.length === 3 && !isBlobLike(value)) {
37
29
  throw new TypeError(
@@ -58,15 +50,9 @@ class FormData {
58
50
  }
59
51
 
60
52
  delete (name) {
61
- if (!(this instanceof FormData)) {
62
- throw new TypeError('Illegal invocation')
63
- }
53
+ webidl.brandCheck(this, FormData)
64
54
 
65
- if (arguments.length < 1) {
66
- throw new TypeError(
67
- `Failed to execute 'delete' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
68
- )
69
- }
55
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' })
70
56
 
71
57
  name = webidl.converters.USVString(name)
72
58
 
@@ -83,15 +69,9 @@ class FormData {
83
69
  }
84
70
 
85
71
  get (name) {
86
- if (!(this instanceof FormData)) {
87
- throw new TypeError('Illegal invocation')
88
- }
72
+ webidl.brandCheck(this, FormData)
89
73
 
90
- if (arguments.length < 1) {
91
- throw new TypeError(
92
- `Failed to execute 'get' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
93
- )
94
- }
74
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' })
95
75
 
96
76
  name = webidl.converters.USVString(name)
97
77
 
@@ -108,15 +88,9 @@ class FormData {
108
88
  }
109
89
 
110
90
  getAll (name) {
111
- if (!(this instanceof FormData)) {
112
- throw new TypeError('Illegal invocation')
113
- }
91
+ webidl.brandCheck(this, FormData)
114
92
 
115
- if (arguments.length < 1) {
116
- throw new TypeError(
117
- `Failed to execute 'getAll' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
118
- )
119
- }
93
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' })
120
94
 
121
95
  name = webidl.converters.USVString(name)
122
96
 
@@ -130,15 +104,9 @@ class FormData {
130
104
  }
131
105
 
132
106
  has (name) {
133
- if (!(this instanceof FormData)) {
134
- throw new TypeError('Illegal invocation')
135
- }
107
+ webidl.brandCheck(this, FormData)
136
108
 
137
- if (arguments.length < 1) {
138
- throw new TypeError(
139
- `Failed to execute 'has' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
140
- )
141
- }
109
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' })
142
110
 
143
111
  name = webidl.converters.USVString(name)
144
112
 
@@ -148,15 +116,9 @@ class FormData {
148
116
  }
149
117
 
150
118
  set (name, value, filename = undefined) {
151
- if (!(this instanceof FormData)) {
152
- throw new TypeError('Illegal invocation')
153
- }
119
+ webidl.brandCheck(this, FormData)
154
120
 
155
- if (arguments.length < 2) {
156
- throw new TypeError(
157
- `Failed to execute 'set' on 'FormData': 2 arguments required, but only ${arguments.length} present.`
158
- )
159
- }
121
+ webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' })
160
122
 
161
123
  if (arguments.length === 3 && !isBlobLike(value)) {
162
124
  throw new TypeError(
@@ -196,14 +158,8 @@ class FormData {
196
158
  }
197
159
  }
198
160
 
199
- get [Symbol.toStringTag] () {
200
- return this.constructor.name
201
- }
202
-
203
161
  entries () {
204
- if (!(this instanceof FormData)) {
205
- throw new TypeError('Illegal invocation')
206
- }
162
+ webidl.brandCheck(this, FormData)
207
163
 
208
164
  return makeIterator(
209
165
  () => this[kState].map(pair => [pair.name, pair.value]),
@@ -213,9 +169,7 @@ class FormData {
213
169
  }
214
170
 
215
171
  keys () {
216
- if (!(this instanceof FormData)) {
217
- throw new TypeError('Illegal invocation')
218
- }
172
+ webidl.brandCheck(this, FormData)
219
173
 
220
174
  return makeIterator(
221
175
  () => this[kState].map(pair => [pair.name, pair.value]),
@@ -225,9 +179,7 @@ class FormData {
225
179
  }
226
180
 
227
181
  values () {
228
- if (!(this instanceof FormData)) {
229
- throw new TypeError('Illegal invocation')
230
- }
182
+ webidl.brandCheck(this, FormData)
231
183
 
232
184
  return makeIterator(
233
185
  () => this[kState].map(pair => [pair.name, pair.value]),
@@ -241,15 +193,9 @@ class FormData {
241
193
  * @param {unknown} thisArg
242
194
  */
243
195
  forEach (callbackFn, thisArg = globalThis) {
244
- if (!(this instanceof FormData)) {
245
- throw new TypeError('Illegal invocation')
246
- }
196
+ webidl.brandCheck(this, FormData)
247
197
 
248
- if (arguments.length < 1) {
249
- throw new TypeError(
250
- `Failed to execute 'forEach' on 'FormData': 1 argument required, but only ${arguments.length} present.`
251
- )
252
- }
198
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' })
253
199
 
254
200
  if (typeof callbackFn !== 'function') {
255
201
  throw new TypeError(
@@ -265,6 +211,13 @@ class FormData {
265
211
 
266
212
  FormData.prototype[Symbol.iterator] = FormData.prototype.entries
267
213
 
214
+ Object.defineProperties(FormData.prototype, {
215
+ [Symbol.toStringTag]: {
216
+ value: 'FormData',
217
+ configurable: true
218
+ }
219
+ })
220
+
268
221
  /**
269
222
  * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
270
223
  * @param {string} name