undici 5.0.0 → 5.2.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici)
4
4
 
5
- A HTTP/1.1 client, written from scratch for Node.js.
5
+ An HTTP/1.1 client, written from scratch for Node.js.
6
6
 
7
7
  > Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
8
8
  It is also a Stranger Things reference.
@@ -65,7 +65,15 @@ for await (const data of body) {
65
65
  console.log('trailers', trailers)
66
66
  ```
67
67
 
68
- Using [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
68
+ ## Body Mixins
69
+
70
+ The `body` mixins are the most common way to format the request/response body. Mixins include:
71
+
72
+ - [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
73
+ - [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
74
+ - [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
75
+
76
+ Example usage:
69
77
 
70
78
  ```js
71
79
  import { request } from 'undici'
@@ -83,6 +91,12 @@ console.log('data', await body.json())
83
91
  console.log('trailers', trailers)
84
92
  ```
85
93
 
94
+ _Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._
95
+
96
+ Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format.
97
+
98
+ For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
99
+
86
100
  ## Common API Methods
87
101
 
88
102
  This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
@@ -180,6 +194,21 @@ Basic usage example:
180
194
  }
181
195
  ```
182
196
 
197
+ You can pass an optional dispatcher to `fetch` as:
198
+
199
+ ```js
200
+ import { fetch, Agent } from 'undici'
201
+
202
+ const res = await fetch('https://example.com', {
203
+ // Mocks are also supported
204
+ dispatcher: new Agent({
205
+ keepAliveTimeout: 10,
206
+ keepAliveMaxTimeout: 10
207
+ })
208
+ })
209
+ const json = await res.json()
210
+ console.log(json)
211
+ ```
183
212
 
184
213
  #### `request.body`
185
214
 
@@ -213,7 +242,7 @@ const data = {
213
242
 
214
243
  #### `response.body`
215
244
 
216
- 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()`.
245
+ 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()`.
217
246
 
218
247
  ```js
219
248
  import {fetch} from 'undici';
@@ -228,7 +257,7 @@ Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v1
228
257
 
229
258
  #### Specification Compliance
230
259
 
231
- This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) which Undici does
260
+ This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
232
261
  not support or does not fully implement.
233
262
 
234
263
  ##### Garbage Collection
@@ -239,7 +268,7 @@ The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consumi
239
268
  [garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body.
240
269
 
241
270
  Garbage collection in Node is less aggressive and deterministic
242
- (due to the lack of clear idle periods that browser have through the rendering refresh rate)
271
+ (due to the lack of clear idle periods that browsers have through the rendering refresh rate)
243
272
  which means that leaving the release of connection resources to the garbage collector can lead
244
273
  to excessive connection usage, reduced performance (due to less connection re-use), and even
245
274
  stalls or deadlocks when running out of connections.
@@ -301,7 +330,7 @@ Returns: `Dispatcher`
301
330
 
302
331
  ## Specification Compliance
303
332
 
304
- This section documents parts of the HTTP/1.1 specification which Undici does
333
+ This section documents parts of the HTTP/1.1 specification that Undici does
305
334
  not support or does not fully implement.
306
335
 
307
336
  ### Expect
@@ -334,7 +363,7 @@ aborted.
334
363
 
335
364
  ### Manual Redirect
336
365
 
337
- Since it is not possible to manually follow an HTTP redirect on server-side,
366
+ Since it is not possible to manually follow an HTTP redirect on the server-side,
338
367
  Undici returns the actual response instead of an `opaqueredirect` filtered one
339
368
  when invoked with a `manual` redirect. This aligns `fetch()` with the other
340
369
  implementations in Deno and Cloudflare Workers.
@@ -193,7 +193,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
193
193
  * **path** `string`
194
194
  * **method** `string`
195
195
  * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
196
- * **headers** `UndiciHeaders` (optional) - Default: `null`
196
+ * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
197
197
  * **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed.
198
198
  * **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
199
199
  * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
@@ -19,7 +19,6 @@ import { errors } from 'undici'
19
19
  | `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header |
20
20
  | `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header |
21
21
  | `InformationalError` | `UND_ERR_INFO` | expected error with reason |
22
- | `TrailerMismatchError` | `UND_ERR_TRAILER_MISMATCH` | trailers did not match specification |
23
22
 
24
23
  ### `SocketError`
25
24
 
@@ -445,3 +445,79 @@ mockAgent.disableNetConnect()
445
445
  await request('http://example.com')
446
446
  // Will throw
447
447
  ```
448
+
449
+ ### `MockAgent.pendingInterceptors()`
450
+
451
+ This method returns any pending interceptors registered on a mock agent. A pending interceptor meets one of the following criteria:
452
+
453
+ - Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
454
+ - Is persistent (i.e., registered with `.persist()`) and has not been invoked;
455
+ - Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
456
+
457
+ Returns: `PendingInterceptor[]` (where `PendingInterceptor` is a `MockDispatch` with an additional `origin: string`)
458
+
459
+ #### Example - List all pending inteceptors
460
+
461
+ ```js
462
+ const agent = new MockAgent()
463
+ agent.disableNetConnect()
464
+
465
+ agent
466
+ .get('https://example.com')
467
+ .intercept({ method: 'GET', path: '/' })
468
+ .reply(200, '')
469
+
470
+ const pendingInterceptors = agent.pendingInterceptors()
471
+ // Returns [
472
+ // {
473
+ // timesInvoked: 0,
474
+ // times: 1,
475
+ // persist: false,
476
+ // consumed: false,
477
+ // pending: true,
478
+ // path: '/',
479
+ // method: 'GET',
480
+ // body: undefined,
481
+ // headers: undefined,
482
+ // data: {
483
+ // error: null,
484
+ // statusCode: 200,
485
+ // data: '',
486
+ // headers: {},
487
+ // trailers: {}
488
+ // },
489
+ // origin: 'https://example.com'
490
+ // }
491
+ // ]
492
+ ```
493
+
494
+ ### `MockAgent.assertNoPendingInterceptors([options])`
495
+
496
+ This method throws if the mock agent has any pending interceptors. A pending interceptor meets one of the following criteria:
497
+
498
+ - Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
499
+ - Is persistent (i.e., registered with `.persist()`) and has not been invoked;
500
+ - Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
501
+
502
+ #### Example - Check that there are no pending interceptors
503
+
504
+ ```js
505
+ const agent = new MockAgent()
506
+ agent.disableNetConnect()
507
+
508
+ agent
509
+ .get('https://example.com')
510
+ .intercept({ method: 'GET', path: '/' })
511
+ .reply(200, '')
512
+
513
+ agent.assertNoPendingInterceptors()
514
+ // Throws an UndiciError with the following message:
515
+ //
516
+ // 1 interceptor is pending:
517
+ //
518
+ // ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐
519
+ // │ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
520
+ // ├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
521
+ // │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │
522
+ // └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
523
+ ```
@@ -5,17 +5,20 @@ Undici have its own mocking [utility](../api/MockAgent.md). It allow us to inter
5
5
  Example:
6
6
 
7
7
  ```js
8
- // index.mjs
8
+ // bank.mjs
9
9
  import { request } from 'undici'
10
10
 
11
- export async function bankTransfer(recepient, ammount) {
12
- const { body } = await request('http://localhost:3000/bank-transfer',
11
+ export async function bankTransfer(recepient, amount) {
12
+ const { body } = await request('http://localhost:3000/bank-transfer',
13
13
  {
14
14
  method: 'POST',
15
15
  headers: {
16
16
  'X-TOKEN-SECRET': 'SuperSecretToken',
17
17
  },
18
- body: JSON.stringify({ recepient })
18
+ body: JSON.stringify({
19
+ recepient,
20
+ amount
21
+ })
19
22
  }
20
23
  )
21
24
  return await body.json()
@@ -28,7 +31,7 @@ And this is what the test file looks like:
28
31
  // index.test.mjs
29
32
  import { strict as assert } from 'assert'
30
33
  import { MockAgent, setGlobalDispatcher, } from 'undici'
31
- import { bankTransfer } from './undici.mjs'
34
+ import { bankTransfer } from './bank.mjs'
32
35
 
33
36
  const mockAgent = new MockAgent();
34
37
 
@@ -46,7 +49,7 @@ mockPool.intercept({
46
49
  },
47
50
  body: JSON.stringify({
48
51
  recepient: '1234567890',
49
- ammount: '100'
52
+ amount: '100'
50
53
  })
51
54
  }).reply(200, {
52
55
  message: 'transaction processed'
@@ -94,7 +97,7 @@ mockPool.intercept({
94
97
 
95
98
  const badRequest = await bankTransfer('1234567890', '100')
96
99
  // Will throw an error
97
- // MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer':
100
+ // MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer':
98
101
  // subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled)
99
102
  ```
100
103
 
package/index-fetch.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ const { getGlobalDispatcher } = require('./lib/global')
4
+ const fetchImpl = require('./lib/fetch')
5
+
6
+ module.exports.fetch = async function fetch (resource) {
7
+ const dispatcher = (arguments[1] && arguments[1].dispatcher) || getGlobalDispatcher()
8
+ return fetchImpl.apply(dispatcher, arguments)
9
+ }
10
+ module.exports.FormData = require('./lib/fetch/formdata').FormData
11
+ module.exports.Headers = require('./lib/fetch/headers').Headers
12
+ module.exports.Response = require('./lib/fetch/response').Response
13
+ module.exports.Request = require('./lib/fetch/request').Request
package/index.d.ts CHANGED
@@ -16,6 +16,7 @@ import { request, pipeline, stream, connect, upgrade } from './types/api'
16
16
  export * from './types/fetch'
17
17
  export * from './types/file'
18
18
  export * from './types/formdata'
19
+ export { Interceptable } from './types/mock-interceptor'
19
20
 
20
21
  export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
21
22
  export default Undici
package/index.js CHANGED
@@ -15,6 +15,7 @@ const MockAgent = require('./lib/mock/mock-agent')
15
15
  const MockPool = require('./lib/mock/mock-pool')
16
16
  const mockErrors = require('./lib/mock/mock-errors')
17
17
  const ProxyAgent = require('./lib/proxy-agent')
18
+ const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
18
19
 
19
20
  const nodeVersion = process.versions.node.split('.')
20
21
  const nodeMajor = Number(nodeVersion[0])
@@ -32,19 +33,6 @@ module.exports.ProxyAgent = ProxyAgent
32
33
  module.exports.buildConnector = buildConnector
33
34
  module.exports.errors = errors
34
35
 
35
- let globalDispatcher = new Agent()
36
-
37
- function setGlobalDispatcher (agent) {
38
- if (!agent || typeof agent.dispatch !== 'function') {
39
- throw new InvalidArgumentError('Argument agent must implement Agent')
40
- }
41
- globalDispatcher = agent
42
- }
43
-
44
- function getGlobalDispatcher () {
45
- return globalDispatcher
46
- }
47
-
48
36
  function makeDispatcher (fn) {
49
37
  return (url, opts, handler) => {
50
38
  if (typeof opts === 'function') {
@@ -98,7 +86,7 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 5)) {
98
86
  if (!fetchImpl) {
99
87
  fetchImpl = require('./lib/fetch')
100
88
  }
101
- const dispatcher = getGlobalDispatcher()
89
+ const dispatcher = (arguments[1] && arguments[1].dispatcher) || getGlobalDispatcher()
102
90
  return fetchImpl.apply(dispatcher, arguments)
103
91
  }
104
92
  module.exports.Headers = require('./lib/fetch/headers').Headers
@@ -88,14 +88,16 @@ class RequestHandler extends AsyncResource {
88
88
  this.res = body
89
89
  const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
90
90
 
91
- this.runInAsyncScope(callback, null, null, {
92
- statusCode,
93
- headers,
94
- trailers: this.trailers,
95
- opaque,
96
- body,
97
- context
98
- })
91
+ if (callback !== null) {
92
+ this.runInAsyncScope(callback, null, null, {
93
+ statusCode,
94
+ headers,
95
+ trailers: this.trailers,
96
+ opaque,
97
+ body,
98
+ context
99
+ })
100
+ }
99
101
  }
100
102
 
101
103
  onData (chunk) {
package/lib/client.js CHANGED
@@ -11,7 +11,6 @@ const RedirectHandler = require('./handler/redirect')
11
11
  const {
12
12
  RequestContentLengthMismatchError,
13
13
  ResponseContentLengthMismatchError,
14
- TrailerMismatchError,
15
14
  InvalidArgumentError,
16
15
  RequestAbortedError,
17
16
  HeadersTimeoutError,
@@ -425,7 +424,6 @@ class Parser {
425
424
 
426
425
  this.bytesRead = 0
427
426
 
428
- this.trailer = ''
429
427
  this.keepAlive = ''
430
428
  this.contentLength = ''
431
429
  }
@@ -615,8 +613,6 @@ class Parser {
615
613
  const key = this.headers[len - 2]
616
614
  if (key.length === 10 && key.toString().toLowerCase() === 'keep-alive') {
617
615
  this.keepAlive += buf.toString()
618
- } else if (key.length === 7 && key.toString().toLowerCase() === 'trailer') {
619
- this.trailer += buf.toString()
620
616
  } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') {
621
617
  this.contentLength += buf.toString()
622
618
  }
@@ -819,7 +815,7 @@ class Parser {
819
815
  }
820
816
 
821
817
  onMessageComplete () {
822
- const { client, socket, statusCode, upgrade, trailer, headers, contentLength, bytesRead, shouldKeepAlive } = this
818
+ const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this
823
819
 
824
820
  if (socket.destroyed && (!statusCode || shouldKeepAlive)) {
825
821
  return -1
@@ -838,7 +834,6 @@ class Parser {
838
834
  this.statusText = ''
839
835
  this.bytesRead = 0
840
836
  this.contentLength = ''
841
- this.trailer = ''
842
837
  this.keepAlive = ''
843
838
 
844
839
  assert(this.headers.length % 2 === 0)
@@ -849,23 +844,6 @@ class Parser {
849
844
  return
850
845
  }
851
846
 
852
- const trailers = trailer ? trailer.split(/,\s*/) : []
853
- for (let i = 0; i < trailers.length; i++) {
854
- const trailer = trailers[i]
855
- let found = false
856
- for (let n = 0; n < headers.length; n += 2) {
857
- const key = headers[n]
858
- if (key.length === trailer.length && key.toString().toLowerCase() === trailer.toLowerCase()) {
859
- found = true
860
- break
861
- }
862
- }
863
- if (!found) {
864
- util.destroy(socket, new TrailerMismatchError())
865
- return -1
866
- }
867
- }
868
-
869
847
  /* istanbul ignore next: should be handled by llhttp? */
870
848
  if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) {
871
849
  util.destroy(socket, new ResponseContentLengthMismatchError())
@@ -116,16 +116,6 @@ class ResponseContentLengthMismatchError extends UndiciError {
116
116
  }
117
117
  }
118
118
 
119
- class TrailerMismatchError extends UndiciError {
120
- constructor (message) {
121
- super(message)
122
- Error.captureStackTrace(this, TrailerMismatchError)
123
- this.name = 'TrailerMismatchError'
124
- this.message = message || 'Trailers does not match trailer header'
125
- this.code = 'UND_ERR_TRAILER_MISMATCH'
126
- }
127
- }
128
-
129
119
  class ClientDestroyedError extends UndiciError {
130
120
  constructor (message) {
131
121
  super(message)
@@ -196,7 +186,6 @@ module.exports = {
196
186
  BodyTimeoutError,
197
187
  RequestContentLengthMismatchError,
198
188
  ConnectTimeoutError,
199
- TrailerMismatchError,
200
189
  InvalidArgumentError,
201
190
  InvalidReturnValueError,
202
191
  RequestAbortedError,
@@ -11,6 +11,12 @@ const kHandler = Symbol('handler')
11
11
 
12
12
  const channels = {}
13
13
 
14
+ let extractBody
15
+
16
+ const nodeVersion = process.versions.node.split('.')
17
+ const nodeMajor = Number(nodeVersion[0])
18
+ const nodeMinor = Number(nodeVersion[1])
19
+
14
20
  try {
15
21
  const diagnosticsChannel = require('diagnostics_channel')
16
22
  channels.create = diagnosticsChannel.channel('undici:request:create')
@@ -79,7 +85,7 @@ class Request {
79
85
  this.body = body.byteLength ? body : null
80
86
  } else if (typeof body === 'string') {
81
87
  this.body = body.length ? Buffer.from(body) : null
82
- } else if (util.isIterable(body) || util.isBlobLike(body)) {
88
+ } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
83
89
  this.body = body
84
90
  } else {
85
91
  throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
@@ -126,7 +132,22 @@ class Request {
126
132
  throw new InvalidArgumentError('headers must be an object or an array')
127
133
  }
128
134
 
129
- if (util.isBlobLike(body) && this.contentType == null && body.type) {
135
+ if (util.isFormDataLike(this.body)) {
136
+ if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 5)) {
137
+ throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.5 and newer.')
138
+ }
139
+
140
+ if (!extractBody) {
141
+ extractBody = require('../fetch/body.js').extractBody
142
+ }
143
+
144
+ const [bodyStream, contentType] = extractBody(body)
145
+ if (this.contentType == null) {
146
+ this.contentType = contentType
147
+ this.headers += `content-type: ${contentType}\r\n`
148
+ }
149
+ this.body = bodyStream.stream
150
+ } else if (util.isBlobLike(body) && this.contentType == null && body.type) {
130
151
  this.contentType = body.type
131
152
  this.headers += `content-type: ${body.type}\r\n`
132
153
  }
package/lib/core/util.js CHANGED
@@ -324,6 +324,10 @@ function ReadableStreamFrom (iterable) {
324
324
  )
325
325
  }
326
326
 
327
+ function isFormDataLike (chunk) {
328
+ return chunk && chunk.constructor && chunk.constructor.name === 'FormData'
329
+ }
330
+
327
331
  const kEnumerableProperty = Object.create(null)
328
332
  kEnumerableProperty.enumerable = true
329
333
 
@@ -352,5 +356,6 @@ module.exports = {
352
356
  ReadableStreamFrom,
353
357
  isBuffer,
354
358
  validateHandler,
355
- getSocketInfo
359
+ getSocketInfo,
360
+ isFormDataLike
356
361
  }
package/lib/fetch/body.js CHANGED
@@ -71,7 +71,7 @@ function extractBody (object, keepalive = false) {
71
71
 
72
72
  // Set source to a copy of the bytes held by object.
73
73
  source = new Uint8Array(object)
74
- } else if (object instanceof FormData) {
74
+ } else if (util.isFormDataLike(object)) {
75
75
  const boundary = '----formdata-undici-' + Math.random()
76
76
  const prefix = `--${boundary}\r\nContent-Disposition: form-data`
77
77
 
@@ -348,7 +348,7 @@ const properties = {
348
348
  bodyUsed: {
349
349
  enumerable: true,
350
350
  get () {
351
- return this[kState].body && util.isDisturbed(this[kState].body.stream)
351
+ return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
352
352
  }
353
353
  }
354
354
  }