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 +36 -7
- package/docs/api/Dispatcher.md +1 -1
- package/docs/api/Errors.md +0 -1
- package/docs/api/MockAgent.md +76 -0
- package/docs/best-practices/mocking-request.md +10 -7
- package/index-fetch.js +13 -0
- package/index.d.ts +1 -0
- package/index.js +2 -14
- package/lib/api/api-request.js +10 -8
- package/lib/client.js +1 -23
- package/lib/core/errors.js +0 -11
- package/lib/core/request.js +23 -2
- package/lib/core/util.js +6 -1
- package/lib/fetch/body.js +2 -2
- package/lib/fetch/headers.js +135 -112
- package/lib/fetch/index.js +26 -17
- package/lib/fetch/request.js +6 -5
- package/lib/fetch/response.js +14 -13
- package/lib/fetch/util.js +36 -1
- package/lib/global.js +32 -0
- package/lib/mock/mock-agent.js +27 -1
- package/lib/mock/mock-interceptor.js +4 -1
- package/lib/mock/mock-utils.js +107 -17
- package/lib/mock/pending-interceptors-formatter.js +40 -0
- package/lib/mock/pluralizer.js +29 -0
- package/lib/proxy-agent.js +22 -1
- package/package.json +14 -12
- package/types/dispatcher.d.ts +2 -1
- package/types/fetch.d.ts +24 -15
- package/types/mock-agent.d.ts +14 -1
- package/types/mock-interceptor.d.ts +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [](http://standardjs.com/) [](https://badge.fury.io/js/undici) [](https://codecov.io/gh/nodejs/undici)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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.
|
package/docs/api/Dispatcher.md
CHANGED
|
@@ -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'`.
|
package/docs/api/Errors.md
CHANGED
|
@@ -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
|
|
package/docs/api/MockAgent.md
CHANGED
|
@@ -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
|
-
//
|
|
8
|
+
// bank.mjs
|
|
9
9
|
import { request } from 'undici'
|
|
10
10
|
|
|
11
|
-
export async function bankTransfer(recepient,
|
|
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({
|
|
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 './
|
|
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
|
-
|
|
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
|
package/lib/api/api-request.js
CHANGED
|
@@ -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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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,
|
|
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())
|
package/lib/core/errors.js
CHANGED
|
@@ -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,
|
package/lib/core/request.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
}
|