UNPKG

21.2 kBMarkdownView Raw
1# undici
2
3![Node CI](https://github.com/mcollina/undici/workflows/Node%20CI/badge.svg) [![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)
4
5A HTTP/1.1 client, written from scratch for Node.js.
6
7> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
8It is also a Stranger Things reference.
9
10<!--
11Picture of Eleven
12-->
13
14## Install
15
16```
17npm i undici
18```
19
20## Benchmarks
21
22Machine: AMD EPYC 7502P<br/>
23
24Node 15
25```
26http - keepalive x 12,028 ops/sec ±2.60% (265 runs sampled)
27undici - pipeline x 31,321 ops/sec ±0.77% (276 runs sampled)
28undici - request x 36,612 ops/sec ±0.71% (277 runs sampled)
29undici - stream x 41,291 ops/sec ±0.90% (268 runs sampled)
30undici - dispatch x 47,319 ops/sec ±1.17% (263 runs sampled)
31```
32
33The benchmark is a simple `hello world` [example](benchmarks/index.js) using a
34single unix socket with pipelining.
35
36## API
37
38<a name='client'></a>
39### `new undici.Client(url, opts)`
40
41A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled
42by default.
43
44`url` can be a string or a [`URL`](https://nodejs.org/api/url.html#url_class_url) object.
45It should only include the protocol, hostname, and the port.
46
47Options:
48
49- `socketTimeout: Number`, the timeout after which a socket with active requests
50 will time out. Monitors time between activity on a connected socket.
51 Use `0` to disable it entirely. Default: `30e3` milliseconds (30s).
52
53- `socketPath: String|Null`, an IPC endpoint, either Unix domain socket or Windows named pipe.
54 Default: `null`.
55
56- `idleTimeout: Number`, the timeout after which a socket without active requests
57 will time out. Monitors time between activity on a connected socket.
58 This value may be overriden by *keep-alive* hints from the server.
59 Default: `4e3` milliseconds (4s).
60
61- `keepAlive: Boolean`, enable or disable keep alive connections.
62 Default: `true`.
63
64- `keepAliveMaxTimeout: Number`, the maximum allowed `idleTimeout` when overriden by
65 *keep-alive* hints from the server.
66 Default: `600e3` milliseconds (10min).
67
68- `keepAliveTimeoutThreshold: Number`, a number subtracted from server *keep-alive* hints
69 when overriding `idleTimeout` to account for timing inaccuries caused by e.g.
70 transport latency.
71 Default: `1e3` milliseconds (1s).
72
73- `pipelining: Number`, the amount of concurrent requests to be sent over the
74 single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2).
75 Carefully consider your workload and environment before enabling concurrent requests
76 as pipelining may reduce performance if used incorrectly. Pipelining is sensitive
77 to network stack settings as well as head of line blocking caused by e.g. long running requests.
78 Default: `1`.
79
80- `tls: Object|Null`, an options object which in the case of `https` will be passed to
81 [`tls.connect`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
82 Default: `null`.
83
84- `maxHeaderSize: Number`, the maximum length of request headers in bytes.
85 Default: `16384` (16KiB).
86
87- `headersTimeout: Number`, the amount of time the parser will wait to receive the complete
88 HTTP headers (Node 14 and above only).
89 Default: `30e3` milliseconds (30s).
90
91<a name='request'></a>
92#### `client.request(opts[, callback(err, data)]): Promise|Void`
93
94Performs a HTTP request.
95
96Options:
97
98* `path: String`
99* `method: String`
100* `opaque: Any`
101* `body: String|Buffer|Uint8Array|stream.Readable|Null`
102 Default: `null`.
103* `headers: Object|Array|Null`, an object with header-value pairs or an array with header-value pairs bi-indexed (`['header1', 'value1', 'header2', 'value2']`).
104 Default: `null`.
105* `signal: AbortSignal|EventEmitter|Null`
106 Default: `null`.
107* `requestTimeout: Number`, the timeout after which a request will time out, in
108 milliseconds. Monitors time between request being enqueued and receiving
109 a response. Use `0` to disable it entirely.
110 Default: `30e3` milliseconds (30s).
111* `idempotent: Boolean`, whether the requests can be safely retried or not.
112 If `false` the request won't be sent until all preceeding
113 requests in the pipeline has completed.
114 Default: `true` if `method` is `HEAD` or `GET`.
115
116Headers are represented by an object like this:
117
118```js
119{
120 'content-length': '123',
121 'content-type': 'text/plain',
122 connection: 'keep-alive',
123 host: 'mysite.com',
124 accept: '*/*'
125}
126```
127
128Or an array like this:
129
130```js
131[
132 'content-length', '123',
133 'content-type', 'text/plain',
134 'connection', 'keep-alive',
135 'host', 'mysite.com',
136 'accept', '*/*'
137]
138```
139
140Keys are lowercased. Values are not modified.
141If you don't specify a `host` header, it will be derived from the `url` of the client instance.
142
143The `data` parameter in `callback` is defined as follow:
144
145* `statusCode: Number`
146* `opaque: Any`
147* `headers: Object`, an object where all keys have been lowercased.
148* `body: stream.Readable` response payload. A user **must**
149 either fully consume or destroy the body unless there is an error, or no further requests
150 will be processed.
151
152Returns a promise if no callback is provided.
153
154Example:
155
156```js
157const { Client } = require('undici')
158const client = new Client(`http://localhost:3000`)
159
160client.request({
161 path: '/',
162 method: 'GET'
163}, function (err, data) {
164 if (err) {
165 // handle this in some way!
166 return
167 }
168
169 const {
170 statusCode,
171 headers,
172 body
173 } = data
174
175 console.log('response received', statusCode)
176 console.log('headers', headers)
177
178 body.setEncoding('utf8')
179 body.on('data', console.log)
180
181 client.close()
182})
183```
184
185Non-idempotent requests will not be pipelined in order
186to avoid indirect failures.
187
188Idempotent requests will be automatically retried if
189they fail due to indirect failure from the request
190at the head of the pipeline. This does not apply to
191idempotent requests with a stream request body.
192
193##### Aborting a request
194
195A request can may be aborted using either an `AbortController` or an `EventEmitter`.
196To use `AbortController` in Node.js versions earlier than 15, you will need to
197install a shim - `npm i abort-controller`.
198
199```js
200const { Client } = require('undici')
201
202const client = new Client('http://localhost:3000')
203const abortController = new AbortController()
204
205client.request({
206 path: '/',
207 method: 'GET',
208 signal: abortController.signal
209}, function (err, data) {
210 console.log(err) // RequestAbortedError
211 client.close()
212})
213
214abortController.abort()
215```
216
217Alternatively, any `EventEmitter` that emits an `'abort'` event may be used as an abort controller:
218
219```js
220const EventEmitter = require('events')
221const { Client } = require('undici')
222
223const client = new Client('http://localhost:3000')
224const ee = new EventEmitter()
225
226client.request({
227 path: '/',
228 method: 'GET',
229 signal: ee
230}, function (err, data) {
231 console.log(err) // RequestAbortedError
232 client.close()
233})
234
235ee.emit('abort')
236```
237
238Destroying the request or response body will have the same effect.
239
240<a name='stream'></a>
241#### `client.stream(opts, factory(data)[, callback(err)]): Promise|Void`
242
243A faster version of [`request`][request].
244
245Unlike [`request`][request] this method expects `factory`
246to return a [`Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) which the response will be
247written to. This improves performance by avoiding
248creating an intermediate [`Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) when the user
249expects to directly pipe the response body to a
250[`Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable).
251
252Options:
253
254* ... same as [`client.request(opts[, callback])`][request].
255
256The `data` parameter in `factory` is defined as follow:
257
258* `statusCode: Number`
259* `headers: Object`, an object where all keys have been lowercased.
260* `opaque: Any`
261
262The `data` parameter in `callback` is defined as follow:
263
264* `opaque: Any`
265* `trailers: Object`, an object where all keys have been lowercased.
266
267Returns a promise if no callback is provided.
268
269```js
270const { Client } = require('undici')
271const client = new Client(`http://localhost:3000`)
272const fs = require('fs')
273
274client.stream({
275 path: '/',
276 method: 'GET',
277 opaque: filename
278}, ({ statusCode, headers, opaque: filename }) => {
279 console.log('response received', statusCode)
280 console.log('headers', headers)
281 return fs.createWriteStream(filename)
282}, (err) => {
283 if (err) {
284 console.error('failure', err)
285 } else {
286 console.log('success')
287 }
288})
289```
290
291`opaque` makes it possible to avoid creating a closure
292for the `factory` method:
293
294```js
295function (req, res) {
296 return client.stream({ ...opts, opaque: res }, proxy)
297}
298```
299
300Instead of:
301
302```js
303function (req, res) {
304 return client.stream(opts, (data) => {
305 // Creates closure to capture `res`.
306 proxy({ ...data, opaque: res })
307 }
308}
309```
310
311<a name='pipeline'></a>
312#### `client.pipeline(opts, handler(data)): Duplex`
313
314For easy use with [`stream.pipeline`](https://nodejs.org/api/stream.html#stream_stream_pipeline_source_transforms_destination_callback).
315
316Options:
317
318* ... same as [`client.request(opts, callback)`][request].
319* `objectMode: Boolean`, `true` if the `handler` will return an object stream.
320 Default: `false`
321
322The `data` parameter in `handler` is defined as follow:
323
324* `statusCode: Number`
325* `headers: Object`, an object where all keys have been lowercased.
326* `opaque: Any`
327* `body: stream.Readable` response payload. A user **must**
328 either fully consume or destroy the body unless there is an error, or no further requests
329 will be processed.
330
331`handler` should return a [`Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable) from which the result will be
332read. Usually it should just return the `body` argument unless
333some kind of transformation needs to be performed based on e.g.
334`headers` or `statusCode`.
335
336The `handler` should validate the response and save any
337required state. If there is an error it should be thrown.
338
339Returns a `Duplex` which writes to the request and reads from
340the response.
341
342```js
343const { Client } = require('undici')
344const client = new Client(`http://localhost:3000`)
345const fs = require('fs')
346const stream = require('stream')
347
348stream.pipeline(
349 fs.createReadStream('source.raw'),
350 client.pipeline({
351 path: '/',
352 method: 'PUT',
353 }, ({ statusCode, headers, body }) => {
354 if (statusCode !== 201) {
355 throw new Error('invalid response')
356 }
357
358 if (isZipped(headers)) {
359 return pipeline(body, unzip(), () => {})
360 }
361
362 return body
363 }),
364 fs.createWriteStream('response.raw'),
365 (err) => {
366 if (err) {
367 console.error('failed')
368 } else {
369 console.log('succeeded')
370 }
371 }
372)
373```
374
375<a name='upgrade'></a>
376#### `client.upgrade(opts[, callback(err, data)]): Promise|Void`
377
378Upgrade to a different protocol.
379
380Options:
381
382* `path: String`
383* `opaque: Any`
384* `method: String`
385 Default: `GET`
386* `headers: Object|Null`, an object with header-value pairs.
387 Default: `null`
388* `signal: AbortSignal|EventEmitter|Null`.
389 Default: `null`
390* `requestTimeout: Number`, the timeout after which a request will time out, in
391 milliseconds. Monitors time between request being enqueued and receiving
392 a response. Use `0` to disable it entirely.
393 Default: `30e3` milliseconds (30s).
394* `protocol: String`, a string of comma separated protocols, in descending preference order.
395 Default: `Websocket`.
396
397The `data` parameter in `callback` is defined as follow:
398
399* `headers: Object`, an object where all keys have been lowercased.
400* `socket: Duplex`
401* `opaque`
402
403Returns a promise if no callback is provided.
404
405<a name='connect'></a>
406#### `client.connect(opts[, callback(err, data)]): Promise|Void`
407
408Starts two-way communications with the requested resource.
409
410Options:
411
412* `path: String`
413* `opaque: Any`
414* `headers: Object|Null`, an object with header-value pairs.
415 Default: `null`
416* `signal: AbortSignal|EventEmitter|Null`.
417 Default: `null`
418* `requestTimeout: Number`, the timeout after which a request will time out, in
419 milliseconds. Monitors time between request being enqueued and receiving
420 a response. Use `0` to disable it entirely.
421 Default: `30e3` milliseconds (30s).
422
423The `data` parameter in `callback` is defined as follow:
424
425* `statusCode: Number`
426* `headers: Object`, an object where all keys have been lowercased.
427* `socket: Duplex`
428* `opaque: Any`
429
430Returns a promise if no callback is provided.
431
432<a name='dispatch'></a>
433#### `client.dispatch(opts, handler): Void`
434
435This is the low level API which all the preceeding APIs are implemented on top of.
436
437This API is expected to evolve through semver-major versions and is less stable
438than the preceeding higher level APIs. It is primarily intended for library developers
439who implement higher level APIs on top of this.
440
441Options:
442
443* `path: String`
444* `method: String`
445* `body: String|Buffer|Uint8Array|stream.Readable|Null`
446 Default: `null`.
447* `headers: Object|Null`, an object with header-value pairs.
448 Default: `null`.
449* `requestTimeout: Number`, the timeout after which a request will time out, in
450 milliseconds. Monitors time between request being enqueued and receiving
451 a response. Use `0` to disable it entirely.
452 Default: `30e3` milliseconds (30s).
453* `idempotent: Boolean`, whether the requests can be safely retried or not.
454 If `false` the request won't be sent until all preceeding
455 requests in the pipeline has completed.
456 Default: `true` if `method` is `HEAD` or `GET`.
457
458The `handler` parameter is defined as follow:
459
460* `onConnect(abort)`, invoked before request is dispatched on socket.
461 May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
462 * `abort(): Void`, abort request.
463* `onUpgrade(statusCode, headers, socket): Void`, invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method.
464 * `statusCode: Number`
465 * `headers: Array|Null`
466 * `socket: Duplex`
467* `onHeaders(statusCode, headers, resume): Boolean`, invoked when statusCode and headers have been received.
468 May be invoked multiple times due to 1xx informational headers.
469 * `statusCode: Number`
470 * `headers: Array|Null`, an array of key-value pairs. Keys are not automatically lowercased.
471 * `resume(): Void`, resume `onData` after returning `false`.
472* `onData(chunk): Boolean`, invoked when response payload data is received.
473 * `chunk: Buffer`
474* `onComplete(trailers): Void`, invoked when response payload and trailers have been received and the request has completed.
475 * `trailers: Array|Null`
476* `onError(err): Void`, invoked when an error has occured.
477 * `err: Error`
478
479The caller is responsible for handling the `body` argument, in terms of `'error'` events and `destroy()`:ing up until
480the `onConnect` handler has been invoked.
481
482<a name='close'></a>
483#### `client.close([callback]): Promise|Void`
484
485Closes the client and gracefully waits for enqueued requests to
486complete before invoking the callback.
487
488Returns a promise if no callback is provided.
489
490<a name='destroy'></a>
491#### `client.destroy([err][, callback]): Promise|Void`
492
493Destroy the client abruptly with the given `err`. All the pending and running
494requests will be asynchronously aborted and error. Waits until socket is closed
495before invoking the callback. Since this operation is asynchronously dispatched
496there might still be some progress on dispatched requests.
497
498Returns a promise if no callback is provided.
499
500#### `client.pipelining: Number`
501
502Property to get and set the pipelining factor.
503
504#### `client.pending: Number`
505
506Number of queued requests.
507
508#### `client.running: Number`
509
510Number of inflight requests.
511
512#### `client.size: Number`
513
514Number of pending and running requests.
515
516#### `client.connected: Boolean`
517
518True if the client has an active connection. The client will lazily
519create a connection when it receives a request and will destroy it
520if there is no activity for the duration of the `timeout` value.
521
522#### `client.busy: Boolean`
523
524True if pipeline is saturated or blocked. Indicicates whether dispatching
525further requests is meaningful.
526
527#### `client.closed: Boolean`
528
529True after `client.close()` has been called.
530
531#### `client.destroyed: Boolean`
532
533True after `client.destroyed()` has been called or `client.close()` has been
534called and the client shutdown has completed.
535
536#### Events
537
538* `'drain'`, emitted when pipeline is no longer fully
539 saturated.
540
541* `'connect'`, emitted when a socket has been created and
542 connected. The client will connect once `client.size > 0`.
543
544* `'disconnect'`, emitted when socket has disconnected. The
545 first argument of the event is the error which caused the
546 socket to disconnect. The client will reconnect if or once
547 `client.size > 0`.
548
549<a name='pool'></a>
550### `new undici.Pool(url, opts)`
551
552A pool of [`Client`][] connected to the same upstream target.
553
554Options:
555
556* ... same as [`Client`][].
557* `connections`, the number of clients to create.
558 Default `10`.
559
560`Pool` does not guarantee that requests are dispatched in
561order of invocation.
562
563#### `pool.request(opts[, callback]): Promise|Void`
564
565Calls [`client.request(opts, callback)`][request] on one of the clients.
566
567#### `pool.stream(opts, factory[, callback]): Promise|Void`
568
569Calls [`client.stream(opts, factory, callback)`][stream] on one of the clients.
570
571#### `pool.pipeline(opts, handler): Duplex`
572
573Calls [`client.pipeline(opts, handler)`][pipeline] on one of the clients.
574
575#### `pool.upgrade(opts[, callback]): Promise|Void`
576
577Calls [`client.upgrade(opts, callback)`][upgrade] on one of the clients.
578
579#### `pool.connect(opts[, callback]): Promise|Void`
580
581Calls [`client.connect(opts, callback)`][connect] on one of the clients.
582
583#### `pool.dispatch(opts, handler): Void`
584
585Calls [`client.dispatch(opts, handler)`][dispatch] on one of the clients.
586
587#### `pool.close([callback]): Promise|Void`
588
589Calls [`client.close(callback)`](#close) on all the clients.
590
591#### `pool.destroy([err][, callback]): Promise|Void`
592
593Calls [`client.destroy(err, callback)`](#destroy) on all the clients.
594
595<a name='errors'></a>
596### `undici.errors`
597
598Undici exposes a variety of error objects that you can use to enhance your error handling.
599You can find all the error objects inside the `errors` key.
600
601```js
602const { errors } = require('undici')
603```
604
605| Error | Error Codes | Description |
606| -----------------------------|-----------------------------------|------------------------------------------------|
607| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. |
608| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. |
609| `SocketTimeoutError` | `UND_ERR_SOCKET_TIMEOUT` | a socket exceeds the `socketTimeout` option. |
610| `RequestTimeoutError` | `UND_ERR_REQUEST_TIMEOUT` | a request exceeds the `requestTimeout` option. |
611| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user |
612| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. |
613| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. |
614| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. |
615| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. |
616| `ContentLengthMismatchError` | `UND_ERR_CONTENT_LENGTH_MISMATCH`| body does not match content-length header |
617| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
618| `TrailerMismatchError` | `UND_ERR_TRAILER_MISMATCH` | trailers did not match specification |
619
620## Specification Compliance
621
622This section documents parts of the HTTP/1.1 specification which Undici does
623not support or does not fully implement.
624
625#### Expect
626
627Undici does not support the `Expect` request header field. The request
628body is always immediately sent and the `100 Continue` response will be
629ignored.
630
631Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
632
633### Pipelining
634
635Uncidi will only use pipelining if configured with a `pipelining` factor
636greater than `1`.
637
638Undici always assumes that connections are persistent and will immediatly
639pipeline requests, without checking whether the connection is persistent.
640Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is
641not supported.
642
643Undici will immediately pipeline when retrying requests afters a failed
644connection. However, Undici will not retry the first remaining requests in
645the prior pipeline and instead error the corresponding callback/promise/stream.
646
647Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2<br/>
648Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
649
650## Collaborators
651
652* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
653
654## License
655
656MIT
657
658[`Client`]: #client
659[request]: #request
660[stream]: #stream
661[pipeline]: #pipeline
662[upgrade]: #upgrade
663[connect]: #connect
664[dispatch]: #dispatch