undici 7.0.0-alpha.1 → 7.0.0-alpha.10

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.
Files changed (113) hide show
  1. package/README.md +24 -38
  2. package/docs/docs/api/Agent.md +14 -14
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +131 -0
  5. package/docs/docs/api/Client.md +12 -12
  6. package/docs/docs/api/Debug.md +1 -1
  7. package/docs/docs/api/Dispatcher.md +98 -193
  8. package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
  9. package/docs/docs/api/MockAgent.md +5 -3
  10. package/docs/docs/api/MockClient.md +5 -5
  11. package/docs/docs/api/MockPool.md +4 -3
  12. package/docs/docs/api/Pool.md +15 -15
  13. package/docs/docs/api/PoolStats.md +1 -1
  14. package/docs/docs/api/ProxyAgent.md +3 -3
  15. package/docs/docs/api/RedirectHandler.md +1 -1
  16. package/docs/docs/api/RetryAgent.md +1 -1
  17. package/docs/docs/api/RetryHandler.md +4 -4
  18. package/docs/docs/api/WebSocket.md +46 -4
  19. package/docs/docs/api/api-lifecycle.md +11 -11
  20. package/docs/docs/best-practices/mocking-request.md +2 -2
  21. package/docs/docs/best-practices/proxy.md +1 -1
  22. package/index.d.ts +1 -1
  23. package/index.js +23 -3
  24. package/lib/api/abort-signal.js +2 -0
  25. package/lib/api/api-pipeline.js +4 -2
  26. package/lib/api/api-request.js +6 -4
  27. package/lib/api/api-stream.js +3 -1
  28. package/lib/api/api-upgrade.js +2 -2
  29. package/lib/api/readable.js +200 -47
  30. package/lib/api/util.js +2 -0
  31. package/lib/cache/memory-cache-store.js +177 -0
  32. package/lib/cache/sqlite-cache-store.js +446 -0
  33. package/lib/core/connect.js +54 -22
  34. package/lib/core/constants.js +35 -10
  35. package/lib/core/diagnostics.js +122 -128
  36. package/lib/core/errors.js +2 -2
  37. package/lib/core/request.js +6 -6
  38. package/lib/core/symbols.js +2 -0
  39. package/lib/core/tree.js +4 -2
  40. package/lib/core/util.js +238 -40
  41. package/lib/dispatcher/client-h1.js +405 -142
  42. package/lib/dispatcher/client-h2.js +212 -109
  43. package/lib/dispatcher/client.js +24 -7
  44. package/lib/dispatcher/dispatcher-base.js +4 -1
  45. package/lib/dispatcher/dispatcher.js +4 -0
  46. package/lib/dispatcher/fixed-queue.js +91 -49
  47. package/lib/dispatcher/pool-base.js +3 -3
  48. package/lib/dispatcher/pool-stats.js +2 -0
  49. package/lib/dispatcher/proxy-agent.js +3 -1
  50. package/lib/handler/cache-handler.js +393 -0
  51. package/lib/handler/cache-revalidation-handler.js +124 -0
  52. package/lib/handler/decorator-handler.js +3 -0
  53. package/lib/handler/redirect-handler.js +45 -59
  54. package/lib/handler/retry-handler.js +68 -109
  55. package/lib/handler/unwrap-handler.js +96 -0
  56. package/lib/handler/wrap-handler.js +98 -0
  57. package/lib/interceptor/cache.js +350 -0
  58. package/lib/interceptor/dns.js +375 -0
  59. package/lib/interceptor/response-error.js +15 -7
  60. package/lib/mock/mock-agent.js +5 -8
  61. package/lib/mock/mock-client.js +7 -2
  62. package/lib/mock/mock-errors.js +3 -1
  63. package/lib/mock/mock-interceptor.js +8 -6
  64. package/lib/mock/mock-pool.js +7 -2
  65. package/lib/mock/mock-symbols.js +2 -1
  66. package/lib/mock/mock-utils.js +33 -5
  67. package/lib/util/cache.js +360 -0
  68. package/lib/util/timers.js +50 -6
  69. package/lib/web/cache/cache.js +25 -21
  70. package/lib/web/cache/cachestorage.js +3 -1
  71. package/lib/web/cookies/index.js +18 -5
  72. package/lib/web/cookies/parse.js +6 -1
  73. package/lib/web/eventsource/eventsource.js +2 -0
  74. package/lib/web/fetch/body.js +43 -39
  75. package/lib/web/fetch/constants.js +45 -29
  76. package/lib/web/fetch/data-url.js +2 -2
  77. package/lib/web/fetch/formdata-parser.js +84 -46
  78. package/lib/web/fetch/formdata.js +42 -20
  79. package/lib/web/fetch/headers.js +119 -85
  80. package/lib/web/fetch/index.js +69 -65
  81. package/lib/web/fetch/request.js +132 -55
  82. package/lib/web/fetch/response.js +81 -36
  83. package/lib/web/fetch/util.js +274 -103
  84. package/lib/web/fetch/webidl.js +54 -18
  85. package/lib/web/websocket/connection.js +92 -15
  86. package/lib/web/websocket/constants.js +69 -9
  87. package/lib/web/websocket/events.js +8 -2
  88. package/lib/web/websocket/receiver.js +20 -26
  89. package/lib/web/websocket/stream/websocketerror.js +83 -0
  90. package/lib/web/websocket/stream/websocketstream.js +485 -0
  91. package/lib/web/websocket/util.js +115 -10
  92. package/lib/web/websocket/websocket.js +47 -170
  93. package/package.json +15 -11
  94. package/types/agent.d.ts +1 -1
  95. package/types/cache-interceptor.d.ts +172 -0
  96. package/types/cookies.d.ts +2 -0
  97. package/types/dispatcher.d.ts +29 -4
  98. package/types/env-http-proxy-agent.d.ts +1 -1
  99. package/types/fetch.d.ts +9 -8
  100. package/types/handlers.d.ts +4 -4
  101. package/types/index.d.ts +3 -1
  102. package/types/interceptors.d.ts +18 -1
  103. package/types/mock-agent.d.ts +4 -1
  104. package/types/mock-client.d.ts +1 -1
  105. package/types/mock-pool.d.ts +1 -1
  106. package/types/proxy-agent.d.ts +1 -1
  107. package/types/readable.d.ts +10 -7
  108. package/types/retry-handler.d.ts +3 -3
  109. package/types/webidl.d.ts +30 -4
  110. package/types/websocket.d.ts +33 -0
  111. package/lib/mock/pluralizer.js +0 -29
  112. package/lib/web/cache/symbols.js +0 -5
  113. package/lib/web/fetch/symbols.js +0 -8
@@ -4,13 +4,14 @@ const { isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util')
4
4
  const { utf8DecodeBytes } = require('./util')
5
5
  const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
6
6
  const { makeEntry } = require('./formdata')
7
+ const { webidl } = require('./webidl')
7
8
  const assert = require('node:assert')
8
9
  const { File: NodeFile } = require('node:buffer')
9
10
 
10
11
  const File = globalThis.File ?? NodeFile
11
12
 
12
13
  const formDataNameBuffer = Buffer.from('form-data; name="')
13
- const filenameBuffer = Buffer.from('; filename')
14
+ const filenameBuffer = Buffer.from('filename')
14
15
  const dd = Buffer.from('--')
15
16
  const ddcrlf = Buffer.from('--\r\n')
16
17
 
@@ -74,7 +75,7 @@ function multipartFormDataParser (input, mimeType) {
74
75
  // Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
75
76
  // parameters["boundary"].
76
77
  if (boundaryString === undefined) {
77
- return 'failure'
78
+ throw parsingError('missing boundary in content-type header')
78
79
  }
79
80
 
80
81
  const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
@@ -86,11 +87,21 @@ function multipartFormDataParser (input, mimeType) {
86
87
  // the first byte.
87
88
  const position = { position: 0 }
88
89
 
89
- // Note: undici addition, allow \r\n before the body.
90
- if (input[0] === 0x0d && input[1] === 0x0a) {
90
+ // Note: undici addition, allows leading and trailing CRLFs.
91
+ while (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
91
92
  position.position += 2
92
93
  }
93
94
 
95
+ let trailing = input.length
96
+
97
+ while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) {
98
+ trailing -= 2
99
+ }
100
+
101
+ if (trailing !== input.length) {
102
+ input = input.subarray(0, trailing)
103
+ }
104
+
94
105
  // 5. While true:
95
106
  while (true) {
96
107
  // 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
@@ -100,7 +111,7 @@ function multipartFormDataParser (input, mimeType) {
100
111
  if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
101
112
  position.position += boundary.length
102
113
  } else {
103
- return 'failure'
114
+ throw parsingError('expected a value starting with -- and the boundary')
104
115
  }
105
116
 
106
117
  // 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
@@ -116,7 +127,7 @@ function multipartFormDataParser (input, mimeType) {
116
127
  // 5.3. If position does not point to a sequence of bytes starting with 0x0D
117
128
  // 0x0A (CR LF), return failure.
118
129
  if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
119
- return 'failure'
130
+ throw parsingError('expected CRLF')
120
131
  }
121
132
 
122
133
  // 5.4. Advance position by 2. (This skips past the newline.)
@@ -127,10 +138,6 @@ function multipartFormDataParser (input, mimeType) {
127
138
  // is not failure. Otherwise, return failure.
128
139
  const result = parseMultipartFormDataHeaders(input, position)
129
140
 
130
- if (result === 'failure') {
131
- return 'failure'
132
- }
133
-
134
141
  let { name, filename, contentType, encoding } = result
135
142
 
136
143
  // 5.6. Advance position by 2. (This skips past the empty line that marks
@@ -146,7 +153,7 @@ function multipartFormDataParser (input, mimeType) {
146
153
  const boundaryIndex = input.indexOf(boundary.subarray(2), position.position)
147
154
 
148
155
  if (boundaryIndex === -1) {
149
- return 'failure'
156
+ throw parsingError('expected boundary after body')
150
157
  }
151
158
 
152
159
  body = input.subarray(position.position, boundaryIndex - 4)
@@ -163,7 +170,7 @@ function multipartFormDataParser (input, mimeType) {
163
170
  // 5.9. If position does not point to a sequence of bytes starting with
164
171
  // 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
165
172
  if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
166
- return 'failure'
173
+ throw parsingError('expected CRLF')
167
174
  } else {
168
175
  position.position += 2
169
176
  }
@@ -194,7 +201,7 @@ function multipartFormDataParser (input, mimeType) {
194
201
 
195
202
  // 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
196
203
  assert(isUSVString(name))
197
- assert((typeof value === 'string' && isUSVString(value)) || value instanceof File)
204
+ assert((typeof value === 'string' && isUSVString(value)) || webidl.is.File(value))
198
205
 
199
206
  // 5.13. Create an entry with name and value, and append it to entry list.
200
207
  entryList.push(makeEntry(name, value, filename))
@@ -219,7 +226,7 @@ function parseMultipartFormDataHeaders (input, position) {
219
226
  if (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
220
227
  // 2.1.1. If name is null, return failure.
221
228
  if (name === null) {
222
- return 'failure'
229
+ throw parsingError('header name is null')
223
230
  }
224
231
 
225
232
  // 2.1.2. Return name, filename and contentType.
@@ -239,12 +246,12 @@ function parseMultipartFormDataHeaders (input, position) {
239
246
 
240
247
  // 2.4. If header name does not match the field-name token production, return failure.
241
248
  if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) {
242
- return 'failure'
249
+ throw parsingError('header name does not match the field-name token production')
243
250
  }
244
251
 
245
252
  // 2.5. If the byte at position is not 0x3A (:), return failure.
246
253
  if (input[position.position] !== 0x3a) {
247
- return 'failure'
254
+ throw parsingError('expected :')
248
255
  }
249
256
 
250
257
  // 2.6. Advance position by 1.
@@ -267,7 +274,7 @@ function parseMultipartFormDataHeaders (input, position) {
267
274
  // 2. If position does not point to a sequence of bytes starting with
268
275
  // `form-data; name="`, return failure.
269
276
  if (!bufferStartsWith(input, formDataNameBuffer, position)) {
270
- return 'failure'
277
+ throw parsingError('expected form-data; name=" for content-disposition header')
271
278
  }
272
279
 
273
280
  // 3. Advance position so it points at the byte after the next 0x22 (")
@@ -279,34 +286,61 @@ function parseMultipartFormDataHeaders (input, position) {
279
286
  // failure.
280
287
  name = parseMultipartFormDataName(input, position)
281
288
 
282
- if (name === null) {
283
- return 'failure'
284
- }
285
-
286
289
  // 5. If position points to a sequence of bytes starting with `; filename="`:
287
- if (bufferStartsWith(input, filenameBuffer, position)) {
288
- // Note: undici also handles filename*
289
- let check = position.position + filenameBuffer.length
290
-
291
- if (input[check] === 0x2a) {
292
- position.position += 1
293
- check += 1
294
- }
295
-
296
- if (input[check] !== 0x3d || input[check + 1] !== 0x22) { // ="
297
- return 'failure'
298
- }
299
-
300
- // 1. Advance position so it points at the byte after the next 0x22 (") byte
301
- // (the one in the sequence of bytes matched above).
302
- position.position += 12
303
-
304
- // 2. Set filename to the result of parsing a multipart/form-data name given
305
- // input and position, if the result is not failure. Otherwise, return failure.
306
- filename = parseMultipartFormDataName(input, position)
307
-
308
- if (filename === null) {
309
- return 'failure'
290
+ if (input[position.position] === 0x3b /* ; */ && input[position.position + 1] === 0x20 /* ' ' */) {
291
+ const at = { position: position.position + 2 }
292
+
293
+ if (bufferStartsWith(input, filenameBuffer, at)) {
294
+ if (input[at.position + 8] === 0x2a /* '*' */) {
295
+ at.position += 10 // skip past filename*=
296
+
297
+ // Remove leading http tab and spaces. See RFC for examples.
298
+ // https://datatracker.ietf.org/doc/html/rfc6266#section-5
299
+ collectASequenceOfBytes(
300
+ (char) => char === 0x20 || char === 0x09,
301
+ input,
302
+ at
303
+ )
304
+
305
+ const headerValue = collectASequenceOfBytes(
306
+ (char) => char !== 0x20 && char !== 0x0d && char !== 0x0a, // ' ' or CRLF
307
+ input,
308
+ at
309
+ )
310
+
311
+ if (
312
+ (headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
313
+ (headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
314
+ (headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
315
+ headerValue[3] !== 0x2d || // -
316
+ headerValue[4] !== 0x38 // 8
317
+ ) {
318
+ throw parsingError('unknown encoding, expected utf-8\'\'')
319
+ }
320
+
321
+ // skip utf-8''
322
+ filename = decodeURIComponent(new TextDecoder().decode(headerValue.subarray(7)))
323
+
324
+ position.position = at.position
325
+ } else {
326
+ // 1. Advance position so it points at the byte after the next 0x22 (") byte
327
+ // (the one in the sequence of bytes matched above).
328
+ position.position += 11
329
+
330
+ // Remove leading http tab and spaces. See RFC for examples.
331
+ // https://datatracker.ietf.org/doc/html/rfc6266#section-5
332
+ collectASequenceOfBytes(
333
+ (char) => char === 0x20 || char === 0x09,
334
+ input,
335
+ position
336
+ )
337
+
338
+ position.position++ // skip past " after removing whitespace
339
+
340
+ // 2. Set filename to the result of parsing a multipart/form-data name given
341
+ // input and position, if the result is not failure. Otherwise, return failure.
342
+ filename = parseMultipartFormDataName(input, position)
343
+ }
310
344
  }
311
345
  }
312
346
 
@@ -356,7 +390,7 @@ function parseMultipartFormDataHeaders (input, position) {
356
390
  // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
357
391
  // (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
358
392
  if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
359
- return 'failure'
393
+ throw parsingError('expected CRLF')
360
394
  } else {
361
395
  position.position += 2
362
396
  }
@@ -382,7 +416,7 @@ function parseMultipartFormDataName (input, position) {
382
416
 
383
417
  // 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
384
418
  if (input[position.position] !== 0x22) {
385
- return null // name could be 'failure'
419
+ throw parsingError('expected "')
386
420
  } else {
387
421
  position.position++
388
422
  }
@@ -457,6 +491,10 @@ function bufferStartsWith (buffer, start, position) {
457
491
  return true
458
492
  }
459
493
 
494
+ function parsingError (cause) {
495
+ return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
496
+ }
497
+
460
498
  module.exports = {
461
499
  multipartFormDataParser,
462
500
  validateBoundary
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { iteratorMixin } = require('./util')
4
- const { kState } = require('./symbols')
5
4
  const { kEnumerableProperty } = require('../../core/util')
6
5
  const { webidl } = require('./webidl')
7
6
  const { File: NativeFile } = require('node:buffer')
@@ -12,7 +11,11 @@ const File = globalThis.File ?? NativeFile
12
11
 
13
12
  // https://xhr.spec.whatwg.org/#formdata
14
13
  class FormData {
14
+ #state = []
15
+
15
16
  constructor (form) {
17
+ webidl.util.markAsUncloneable(this)
18
+
16
19
  if (form !== undefined) {
17
20
  throw webidl.errors.conversionFailed({
18
21
  prefix: 'FormData constructor',
@@ -20,8 +23,6 @@ class FormData {
20
23
  types: ['undefined']
21
24
  })
22
25
  }
23
-
24
- this[kState] = []
25
26
  }
26
27
 
27
28
  append (name, value, filename = undefined) {
@@ -32,7 +33,7 @@ class FormData {
32
33
 
33
34
  name = webidl.converters.USVString(name)
34
35
 
35
- if (arguments.length === 3 || value instanceof Blob) {
36
+ if (arguments.length === 3 || webidl.is.Blob(value)) {
36
37
  value = webidl.converters.Blob(value, prefix, 'value')
37
38
 
38
39
  if (filename !== undefined) {
@@ -49,7 +50,7 @@ class FormData {
49
50
  const entry = makeEntry(name, value, filename)
50
51
 
51
52
  // 3. Append entry to this’s entry list.
52
- this[kState].push(entry)
53
+ this.#state.push(entry)
53
54
  }
54
55
 
55
56
  delete (name) {
@@ -62,7 +63,7 @@ class FormData {
62
63
 
63
64
  // The delete(name) method steps are to remove all entries whose name
64
65
  // is name from this’s entry list.
65
- this[kState] = this[kState].filter(entry => entry.name !== name)
66
+ this.#state = this.#state.filter(entry => entry.name !== name)
66
67
  }
67
68
 
68
69
  get (name) {
@@ -75,14 +76,14 @@ class FormData {
75
76
 
76
77
  // 1. If there is no entry whose name is name in this’s entry list,
77
78
  // then return null.
78
- const idx = this[kState].findIndex((entry) => entry.name === name)
79
+ const idx = this.#state.findIndex((entry) => entry.name === name)
79
80
  if (idx === -1) {
80
81
  return null
81
82
  }
82
83
 
83
84
  // 2. Return the value of the first entry whose name is name from
84
85
  // this’s entry list.
85
- return this[kState][idx].value
86
+ return this.#state[idx].value
86
87
  }
87
88
 
88
89
  getAll (name) {
@@ -97,7 +98,7 @@ class FormData {
97
98
  // then return the empty list.
98
99
  // 2. Return the values of all entries whose name is name, in order,
99
100
  // from this’s entry list.
100
- return this[kState]
101
+ return this.#state
101
102
  .filter((entry) => entry.name === name)
102
103
  .map((entry) => entry.value)
103
104
  }
@@ -112,7 +113,7 @@ class FormData {
112
113
 
113
114
  // The has(name) method steps are to return true if there is an entry
114
115
  // whose name is name in this’s entry list; otherwise false.
115
- return this[kState].findIndex((entry) => entry.name === name) !== -1
116
+ return this.#state.findIndex((entry) => entry.name === name) !== -1
116
117
  }
117
118
 
118
119
  set (name, value, filename = undefined) {
@@ -123,7 +124,7 @@ class FormData {
123
124
 
124
125
  name = webidl.converters.USVString(name)
125
126
 
126
- if (arguments.length === 3 || value instanceof Blob) {
127
+ if (arguments.length === 3 || webidl.is.Blob(value)) {
127
128
  value = webidl.converters.Blob(value, prefix, 'value')
128
129
 
129
130
  if (filename !== undefined) {
@@ -144,21 +145,21 @@ class FormData {
144
145
 
145
146
  // 3. If there are entries in this’s entry list whose name is name, then
146
147
  // replace the first such entry with entry and remove the others.
147
- const idx = this[kState].findIndex((entry) => entry.name === name)
148
+ const idx = this.#state.findIndex((entry) => entry.name === name)
148
149
  if (idx !== -1) {
149
- this[kState] = [
150
- ...this[kState].slice(0, idx),
150
+ this.#state = [
151
+ ...this.#state.slice(0, idx),
151
152
  entry,
152
- ...this[kState].slice(idx + 1).filter((entry) => entry.name !== name)
153
+ ...this.#state.slice(idx + 1).filter((entry) => entry.name !== name)
153
154
  ]
154
155
  } else {
155
156
  // 4. Otherwise, append entry to this’s entry list.
156
- this[kState].push(entry)
157
+ this.#state.push(entry)
157
158
  }
158
159
  }
159
160
 
160
161
  [nodeUtil.inspect.custom] (depth, options) {
161
- const state = this[kState].reduce((a, b) => {
162
+ const state = this.#state.reduce((a, b) => {
162
163
  if (a[b.name]) {
163
164
  if (Array.isArray(a[b.name])) {
164
165
  a[b.name].push(b.value)
@@ -180,9 +181,28 @@ class FormData {
180
181
  // remove [Object null prototype]
181
182
  return `FormData ${output.slice(output.indexOf(']') + 2)}`
182
183
  }
184
+
185
+ /**
186
+ * @param {FormData} formData
187
+ */
188
+ static getFormDataState (formData) {
189
+ return formData.#state
190
+ }
191
+
192
+ /**
193
+ * @param {FormData} formData
194
+ * @param {any[]} newState
195
+ */
196
+ static setFormDataState (formData, newState) {
197
+ formData.#state = newState
198
+ }
183
199
  }
184
200
 
185
- iteratorMixin('FormData', FormData, kState, 'name', 'value')
201
+ const { getFormDataState, setFormDataState } = FormData
202
+ Reflect.deleteProperty(FormData, 'getFormDataState')
203
+ Reflect.deleteProperty(FormData, 'setFormDataState')
204
+
205
+ iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value')
186
206
 
187
207
  Object.defineProperties(FormData.prototype, {
188
208
  append: kEnumerableProperty,
@@ -217,7 +237,7 @@ function makeEntry (name, value, filename) {
217
237
 
218
238
  // 1. If value is not a File object, then set value to a new File object,
219
239
  // representing the same bytes, whose name attribute value is "blob"
220
- if (!(value instanceof File)) {
240
+ if (!webidl.is.File(value)) {
221
241
  value = new File([value], 'blob', { type: value.type })
222
242
  }
223
243
 
@@ -238,4 +258,6 @@ function makeEntry (name, value, filename) {
238
258
  return { name, value }
239
259
  }
240
260
 
241
- module.exports = { FormData, makeEntry }
261
+ webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData)
262
+
263
+ module.exports = { FormData, makeEntry, setFormDataState }