undici 5.1.0 → 5.3.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
@@ -194,6 +194,21 @@ Basic usage example:
194
194
  }
195
195
  ```
196
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
+ ```
197
212
 
198
213
  #### `request.body`
199
214
 
@@ -360,6 +375,7 @@ Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
360
375
  * [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
361
376
  * [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
362
377
  * [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
378
+ * [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
363
379
  * [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
364
380
  * [__Szymon Marczak__](https://github.com/szmarczak), <https://www.npmjs.com/~szmarczak>
365
381
  * [__Tomas Della Vedova__](https://github.com/delvedor), <https://www.npmjs.com/~delvedor>
@@ -194,18 +194,20 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
194
194
  * **method** `string`
195
195
  * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
196
196
  * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
197
+ * **query** `Record<string, any> | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead.
197
198
  * **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
199
  * **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
200
  * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
200
201
  * **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
201
202
  * **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds.
203
+ * **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.
202
204
 
203
205
  #### Parameter: `DispatchHandler`
204
206
 
205
207
  * **onConnect** `(abort: () => void, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
206
208
  * **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
207
209
  * **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
208
- * **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
210
+ * **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void, statusText: string) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
209
211
  * **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests.
210
212
  * **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
211
213
  * **onBodySent** `(chunk: string | Buffer | Uint8Array) => void` - Invoked when a body chunk is sent to the server. Not required. For a stream or iterable body this will be invoked for every chunk. For other body types, it will be invoked once after the body is sent.
@@ -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
 
@@ -101,4 +101,36 @@ const badRequest = await bankTransfer('1234567890', '100')
101
101
  // subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled)
102
102
  ```
103
103
 
104
+ ## Reply with data based on request
104
105
 
106
+ If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`
107
+
108
+ ```js
109
+ mockPool.intercept({
110
+ path: '/bank-transfer',
111
+ method: 'POST',
112
+ headers: {
113
+ 'X-TOKEN-SECRET': 'SuperSecretToken',
114
+ },
115
+ body: JSON.stringify({
116
+ recepient: '1234567890',
117
+ amount: '100'
118
+ })
119
+ }).reply(200, (opts) => {
120
+ // do something with opts
121
+
122
+ return { message: 'transaction processed' }
123
+ })
124
+ ```
125
+
126
+ in this case opts will be
127
+
128
+ ```
129
+ {
130
+ method: 'POST',
131
+ headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
132
+ body: '{"recepient":"1234567890","amount":"100"}',
133
+ origin: 'http://localhost:3000',
134
+ path: '/bank-transfer'
135
+ }
136
+ ```
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 * from './types/diagnostics-channel'
19
20
  export { Interceptable } from './types/mock-interceptor'
20
21
 
21
22
  export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
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
@@ -3,7 +3,8 @@
3
3
  const Readable = require('./readable')
4
4
  const {
5
5
  InvalidArgumentError,
6
- RequestAbortedError
6
+ RequestAbortedError,
7
+ ResponseStatusCodeError
7
8
  } = require('../core/errors')
8
9
  const util = require('../core/util')
9
10
  const { AsyncResource } = require('async_hooks')
@@ -15,7 +16,7 @@ class RequestHandler extends AsyncResource {
15
16
  throw new InvalidArgumentError('invalid opts')
16
17
  }
17
18
 
18
- const { signal, method, opaque, body, onInfo, responseHeaders } = opts
19
+ const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts
19
20
 
20
21
  try {
21
22
  if (typeof callback !== 'function') {
@@ -51,6 +52,7 @@ class RequestHandler extends AsyncResource {
51
52
  this.trailers = {}
52
53
  this.context = null
53
54
  this.onInfo = onInfo || null
55
+ this.throwOnError = throwOnError
54
56
 
55
57
  if (util.isStream(body)) {
56
58
  body.on('error', (err) => {
@@ -70,7 +72,7 @@ class RequestHandler extends AsyncResource {
70
72
  this.context = context
71
73
  }
72
74
 
73
- onHeaders (statusCode, rawHeaders, resume) {
75
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
74
76
  const { callback, opaque, abort, context } = this
75
77
 
76
78
  if (statusCode < 200) {
@@ -89,6 +91,13 @@ class RequestHandler extends AsyncResource {
89
91
  const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
90
92
 
91
93
  if (callback !== null) {
94
+ if (this.throwOnError && statusCode >= 400) {
95
+ this.runInAsyncScope(callback, null,
96
+ new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)
97
+ )
98
+ return
99
+ }
100
+
92
101
  this.runInAsyncScope(callback, null, null, {
93
102
  statusCode,
94
103
  headers,
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())
@@ -56,6 +56,19 @@ class BodyTimeoutError extends UndiciError {
56
56
  }
57
57
  }
58
58
 
59
+ class ResponseStatusCodeError extends UndiciError {
60
+ constructor (message, statusCode, headers) {
61
+ super(message)
62
+ Error.captureStackTrace(this, ResponseStatusCodeError)
63
+ this.name = 'ResponseStatusCodeError'
64
+ this.message = message || 'Response Status Code Error'
65
+ this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
66
+ this.status = statusCode
67
+ this.statusCode = statusCode
68
+ this.headers = headers
69
+ }
70
+ }
71
+
59
72
  class InvalidArgumentError extends UndiciError {
60
73
  constructor (message) {
61
74
  super(message)
@@ -116,16 +129,6 @@ class ResponseContentLengthMismatchError extends UndiciError {
116
129
  }
117
130
  }
118
131
 
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
132
  class ClientDestroyedError extends UndiciError {
130
133
  constructor (message) {
131
134
  super(message)
@@ -196,7 +199,7 @@ module.exports = {
196
199
  BodyTimeoutError,
197
200
  RequestContentLengthMismatchError,
198
201
  ConnectTimeoutError,
199
- TrailerMismatchError,
202
+ ResponseStatusCodeError,
200
203
  InvalidArgumentError,
201
204
  InvalidReturnValueError,
202
205
  RequestAbortedError,
@@ -4,8 +4,8 @@ const {
4
4
  InvalidArgumentError,
5
5
  NotSupportedError
6
6
  } = require('./errors')
7
- const util = require('./util')
8
7
  const assert = require('assert')
8
+ const util = require('./util')
9
9
 
10
10
  const kHandler = Symbol('handler')
11
11
 
@@ -38,11 +38,13 @@ class Request {
38
38
  method,
39
39
  body,
40
40
  headers,
41
+ query,
41
42
  idempotent,
42
43
  blocking,
43
44
  upgrade,
44
45
  headersTimeout,
45
- bodyTimeout
46
+ bodyTimeout,
47
+ throwOnError
46
48
  }, handler) {
47
49
  if (typeof path !== 'string') {
48
50
  throw new InvalidArgumentError('path must be a string')
@@ -70,6 +72,8 @@ class Request {
70
72
 
71
73
  this.bodyTimeout = bodyTimeout
72
74
 
75
+ this.throwOnError = throwOnError === true
76
+
73
77
  this.method = method
74
78
 
75
79
  if (body == null) {
@@ -97,7 +101,7 @@ class Request {
97
101
 
98
102
  this.upgrade = upgrade || null
99
103
 
100
- this.path = path
104
+ this.path = query ? util.buildURL(path, query) : path
101
105
 
102
106
  this.origin = origin
103
107
 
package/lib/core/util.js CHANGED
@@ -26,6 +26,51 @@ function isBlobLike (object) {
26
26
  )
27
27
  }
28
28
 
29
+ function isObject (val) {
30
+ return val !== null && typeof val === 'object'
31
+ }
32
+
33
+ // this escapes all non-uri friendly characters
34
+ function encode (val) {
35
+ return encodeURIComponent(val)
36
+ }
37
+
38
+ // based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license)
39
+ function buildURL (url, queryParams) {
40
+ if (url.includes('?') || url.includes('#')) {
41
+ throw new Error('Query params cannot be passed when url already contains "?" or "#".')
42
+ }
43
+ if (!isObject(queryParams)) {
44
+ throw new Error('Query params must be an object')
45
+ }
46
+
47
+ const parts = []
48
+ for (let [key, val] of Object.entries(queryParams)) {
49
+ if (val === null || typeof val === 'undefined') {
50
+ continue
51
+ }
52
+
53
+ if (!Array.isArray(val)) {
54
+ val = [val]
55
+ }
56
+
57
+ for (const v of val) {
58
+ if (isObject(v)) {
59
+ throw new Error('Passing object as a query param is not supported, please serialize to string up-front')
60
+ }
61
+ parts.push(encode(key) + '=' + encode(v))
62
+ }
63
+ }
64
+
65
+ const serializedParams = parts.join('&')
66
+
67
+ if (serializedParams) {
68
+ url += '?' + serializedParams
69
+ }
70
+
71
+ return url
72
+ }
73
+
29
74
  function parseURL (url) {
30
75
  if (typeof url === 'string') {
31
76
  url = new URL(url)
@@ -357,5 +402,6 @@ module.exports = {
357
402
  isBuffer,
358
403
  validateHandler,
359
404
  getSocketInfo,
360
- isFormDataLike
405
+ isFormDataLike,
406
+ buildURL
361
407
  }
package/lib/fetch/file.js CHANGED
@@ -69,10 +69,6 @@ class File extends Blob {
69
69
  }
70
70
 
71
71
  get [Symbol.toStringTag] () {
72
- if (!(this instanceof File)) {
73
- throw new TypeError('Illegal invocation')
74
- }
75
-
76
72
  return this.constructor.name
77
73
  }
78
74
  }
@@ -190,10 +186,6 @@ class FileLike {
190
186
  }
191
187
 
192
188
  get [Symbol.toStringTag] () {
193
- if (!(this instanceof FileLike)) {
194
- throw new TypeError('Illegal invocation')
195
- }
196
-
197
189
  return 'File'
198
190
  }
199
191
  }
@@ -6,6 +6,8 @@ const { File, FileLike } = require('./file')
6
6
  const { Blob } = require('buffer')
7
7
 
8
8
  class FormData {
9
+ static name = 'FormData'
10
+
9
11
  constructor (...args) {
10
12
  if (args.length > 0 && !(args[0]?.constructor?.name === 'HTMLFormElement')) {
11
13
  throw new TypeError(
@@ -182,10 +184,6 @@ class FormData {
182
184
  }
183
185
 
184
186
  get [Symbol.toStringTag] () {
185
- if (!(this instanceof FormData)) {
186
- throw new TypeError('Illegal invocation')
187
- }
188
-
189
187
  return this.constructor.name
190
188
  }
191
189
 
@@ -269,4 +267,4 @@ function makeEntry (name, value, filename) {
269
267
  return entry
270
268
  }
271
269
 
272
- module.exports = { FormData: globalThis.FormData ?? FormData }
270
+ module.exports = { FormData }