undici 5.9.1 → 5.11.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.
Files changed (42) hide show
  1. package/README.md +33 -11
  2. package/docs/api/Agent.md +2 -1
  3. package/docs/api/Client.md +1 -0
  4. package/docs/api/DispatchInterceptor.md +60 -0
  5. package/docs/api/MockPool.md +1 -1
  6. package/docs/api/Pool.md +1 -0
  7. package/index-fetch.js +7 -2
  8. package/index.d.ts +7 -3
  9. package/index.js +19 -2
  10. package/lib/agent.js +9 -8
  11. package/lib/api/readable.js +1 -1
  12. package/lib/balanced-pool.js +4 -1
  13. package/lib/client.js +7 -7
  14. package/lib/core/symbols.js +2 -1
  15. package/lib/core/util.js +4 -34
  16. package/lib/dispatcher-base.js +34 -2
  17. package/lib/fetch/body.js +93 -14
  18. package/lib/fetch/dataURL.js +50 -5
  19. package/lib/fetch/file.js +13 -1
  20. package/lib/fetch/formdata.js +2 -2
  21. package/lib/fetch/global.js +48 -0
  22. package/lib/fetch/headers.js +3 -1
  23. package/lib/fetch/index.js +19 -26
  24. package/lib/fetch/request.js +11 -2
  25. package/lib/fetch/response.js +5 -4
  26. package/lib/fetch/util.js +289 -22
  27. package/lib/handler/DecoratorHandler.js +35 -0
  28. package/lib/handler/{redirect.js → RedirectHandler.js} +3 -3
  29. package/lib/interceptor/redirectInterceptor.js +21 -0
  30. package/lib/mock/mock-utils.js +21 -68
  31. package/lib/pool.js +7 -1
  32. package/lib/proxy-agent.js +27 -5
  33. package/package.json +13 -8
  34. package/types/agent.d.ts +3 -0
  35. package/types/client.d.ts +7 -3
  36. package/types/connector.d.ts +12 -3
  37. package/types/diagnostics-channel.d.ts +1 -1
  38. package/types/dispatcher.d.ts +61 -3
  39. package/types/global-origin.d.ts +7 -0
  40. package/types/handlers.d.ts +9 -0
  41. package/types/interceptors.d.ts +5 -0
  42. package/types/pool.d.ts +3 -0
package/README.md CHANGED
@@ -185,12 +185,12 @@ Help us improve the test coverage by following instructions at [nodejs/undici/#9
185
185
  Basic usage example:
186
186
 
187
187
  ```js
188
- import { fetch } from 'undici';
188
+ import { fetch } from 'undici'
189
189
 
190
190
 
191
191
  const res = await fetch('https://example.com')
192
192
  const json = await res.json()
193
- console.log(json);
193
+ console.log(json)
194
194
  ```
195
195
 
196
196
  You can pass an optional dispatcher to `fetch` as:
@@ -225,16 +225,16 @@ A body can be of the following types:
225
225
  In this implementation of fetch, ```request.body``` now accepts ```Async Iterables```. It is not present in the [Fetch Standard.](https://fetch.spec.whatwg.org)
226
226
 
227
227
  ```js
228
- import { fetch } from "undici";
228
+ import { fetch } from 'undici'
229
229
 
230
230
  const data = {
231
231
  async *[Symbol.asyncIterator]() {
232
- yield "hello";
233
- yield "world";
232
+ yield 'hello'
233
+ yield 'world'
234
234
  },
235
- };
235
+ }
236
236
 
237
- await fetch("https://example.com", { body: data, method: 'POST' });
237
+ await fetch('https://example.com', { body: data, method: 'POST' })
238
238
  ```
239
239
 
240
240
  #### `response.body`
@@ -242,12 +242,12 @@ await fetch("https://example.com", { body: data, method: 'POST' });
242
242
  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()`.
243
243
 
244
244
  ```js
245
- import { fetch } from 'undici';
246
- import { Readable } from 'node:stream';
245
+ import { fetch } from 'undici'
246
+ import { Readable } from 'node:stream'
247
247
 
248
248
  const response = await fetch('https://example.com')
249
- const readableWebStream = response.body;
250
- const readableNodeStream = Readable.fromWeb(readableWebStream);
249
+ const readableWebStream = response.body
250
+ const readableNodeStream = Readable.fromWeb(readableWebStream)
251
251
  ```
252
252
 
253
253
  #### Specification Compliance
@@ -329,6 +329,28 @@ Gets the global dispatcher used by Common API Methods.
329
329
 
330
330
  Returns: `Dispatcher`
331
331
 
332
+ ### `undici.setGlobalOrigin(origin)`
333
+
334
+ * origin `string | URL | undefined`
335
+
336
+ Sets the global origin used in `fetch`.
337
+
338
+ If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed.
339
+
340
+ ```js
341
+ setGlobalOrigin('http://localhost:3000')
342
+
343
+ const response = await fetch('/api/ping')
344
+
345
+ console.log(response.url) // http://localhost:3000/api/ping
346
+ ```
347
+
348
+ ### `undici.getGlobalOrigin()`
349
+
350
+ Gets the global origin used in `fetch`.
351
+
352
+ Returns: `URL`
353
+
332
354
  ### `UrlObject`
333
355
 
334
356
  * **port** `string | number` (optional)
package/docs/api/Agent.md CHANGED
@@ -16,10 +16,11 @@ Returns: `Agent`
16
16
 
17
17
  ### Parameter: `AgentOptions`
18
18
 
19
- Extends: [`ClientOptions`](Pool.md#parameter-pooloptions)
19
+ Extends: [`PoolOptions`](Pool.md#parameter-pooloptions)
20
20
 
21
21
  * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
22
22
  * **maxRedirections** `Integer` - Default: `0`. The number of HTTP redirection to follow unless otherwise specified in `DispatchOptions`.
23
+ * **interceptors** `{ Agent: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time.
23
24
 
24
25
  ## Instance Properties
25
26
 
@@ -26,6 +26,7 @@ Returns: `Client`
26
26
  * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
27
27
  * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
28
28
  * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
29
+ * **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time.
29
30
 
30
31
  #### Parameter: `ConnectOptions`
31
32
 
@@ -0,0 +1,60 @@
1
+ #Interface: DispatchInterceptor
2
+
3
+ Extends: `Function`
4
+
5
+ A function that can be applied to the `Dispatcher.Dispatch` function before it is invoked with a dispatch request.
6
+
7
+ This allows one to write logic to intercept both the outgoing request, and the incoming response.
8
+
9
+ ### Parameter: `Dispatcher.Dispatch`
10
+
11
+ The base dispatch function you are decorating.
12
+
13
+ ### ReturnType: `Dispatcher.Dispatch`
14
+
15
+ A dispatch function that has been altered to provide additional logic
16
+
17
+ ### Basic Example
18
+
19
+ Here is an example of an interceptor being used to provide a JWT bearer token
20
+
21
+ ```js
22
+ 'use strict'
23
+
24
+ const insertHeaderInterceptor = dispatch => {
25
+ return function InterceptedDispatch(opts, handler){
26
+ opts.headers.push('Authorization', 'Bearer [Some token]')
27
+ return dispatch(opts, handler)
28
+ }
29
+ }
30
+
31
+ const client = new Client('https://localhost:3000', {
32
+ interceptors: { Client: [insertHeaderInterceptor] }
33
+ })
34
+
35
+ ```
36
+
37
+ ### Basic Example 2
38
+
39
+ Here is a contrived example of an interceptor stripping the headers from a response.
40
+
41
+ ```js
42
+ 'use strict'
43
+
44
+ const clearHeadersInterceptor = dispatch => {
45
+ const { DecoratorHandler } = require('undici')
46
+ class ResultInterceptor extends DecoratorHandler {
47
+ onHeaders (statusCode, headers, resume) {
48
+ return super.onHeaders(statusCode, [], resume)
49
+ }
50
+ }
51
+ return function InterceptedDispatch(opts, handler){
52
+ return dispatch(opts, new ResultInterceptor(handler))
53
+ }
54
+ }
55
+
56
+ const client = new Client('https://localhost:3000', {
57
+ interceptors: { Client: [clearHeadersInterceptor] }
58
+ })
59
+
60
+ ```
@@ -54,7 +54,7 @@ Returns: `MockInterceptor` corresponding to the input options.
54
54
  ### Parameter: `MockPoolInterceptOptions`
55
55
 
56
56
  * **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path.
57
- * **method** `string | RegExp | (method: string) => boolean` - a matcher for the HTTP request method.
57
+ * **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`.
58
58
  * **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
59
59
  * **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
60
60
  * **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params.
package/docs/api/Pool.md CHANGED
@@ -19,6 +19,7 @@ Extends: [`ClientOptions`](Client.md#parameter-clientoptions)
19
19
 
20
20
  * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
21
21
  * **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances.
22
+ * **interceptors** `{ Pool: DispatchInterceptor[] } }` - Default: `{ Pool: [] }` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching).
22
23
 
23
24
  ## Instance Properties
24
25
 
package/index-fetch.js CHANGED
@@ -1,11 +1,16 @@
1
1
  'use strict'
2
2
 
3
3
  const { getGlobalDispatcher } = require('./lib/global')
4
- const fetchImpl = require('./lib/fetch')
4
+ const fetchImpl = require('./lib/fetch').fetch
5
5
 
6
6
  module.exports.fetch = async function fetch (resource) {
7
7
  const dispatcher = (arguments[1] && arguments[1].dispatcher) || getGlobalDispatcher()
8
- return fetchImpl.apply(dispatcher, arguments)
8
+ try {
9
+ return await fetchImpl.apply(dispatcher, arguments)
10
+ } catch (err) {
11
+ Error.captureStackTrace(err, this)
12
+ throw err
13
+ }
9
14
  }
10
15
  module.exports.FormData = require('./lib/fetch/formdata').FormData
11
16
  module.exports.Headers = require('./lib/fetch/headers').Headers
package/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import Dispatcher = require('./types/dispatcher')
2
2
  import { setGlobalDispatcher, getGlobalDispatcher } from './types/global-dispatcher'
3
+ import { setGlobalOrigin, getGlobalOrigin } from './types/global-origin'
3
4
  import Pool = require('./types/pool')
5
+ import { RedirectHandler, DecoratorHandler } from './types/handlers'
6
+
4
7
  import BalancedPool = require('./types/balanced-pool')
5
8
  import Client = require('./types/client')
6
9
  import buildConnector = require('./types/connector')
@@ -19,14 +22,15 @@ export * from './types/formdata'
19
22
  export * from './types/diagnostics-channel'
20
23
  export { Interceptable } from './types/mock-interceptor'
21
24
 
22
- export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
25
+ export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler }
23
26
  export default Undici
24
27
 
25
- declare function Undici(url: string, opts: Pool.Options): Pool
26
-
27
28
  declare namespace Undici {
28
29
  var Dispatcher: typeof import('./types/dispatcher')
29
30
  var Pool: typeof import('./types/pool');
31
+ var RedirectHandler: typeof import ('./types/handlers').RedirectHandler
32
+ var DecoratorHandler: typeof import ('./types/handlers').DecoratorHandler
33
+ var createRedirectInterceptor: typeof import ('./types/interceptors').createRedirectInterceptor
30
34
  var BalancedPool: typeof import('./types/balanced-pool');
31
35
  var Client: typeof import('./types/client');
32
36
  var buildConnector: typeof import('./types/connector');
package/index.js CHANGED
@@ -16,6 +16,9 @@ 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
18
  const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
19
+ const DecoratorHandler = require('./lib/handler/DecoratorHandler')
20
+ const RedirectHandler = require('./lib/handler/RedirectHandler')
21
+ const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor')
19
22
 
20
23
  const nodeVersion = process.versions.node.split('.')
21
24
  const nodeMajor = Number(nodeVersion[0])
@@ -30,6 +33,10 @@ module.exports.BalancedPool = BalancedPool
30
33
  module.exports.Agent = Agent
31
34
  module.exports.ProxyAgent = ProxyAgent
32
35
 
36
+ module.exports.DecoratorHandler = DecoratorHandler
37
+ module.exports.RedirectHandler = RedirectHandler
38
+ module.exports.createRedirectInterceptor = createRedirectInterceptor
39
+
33
40
  module.exports.buildConnector = buildConnector
34
41
  module.exports.errors = errors
35
42
 
@@ -89,16 +96,26 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
89
96
  let fetchImpl = null
90
97
  module.exports.fetch = async function fetch (resource) {
91
98
  if (!fetchImpl) {
92
- fetchImpl = require('./lib/fetch')
99
+ fetchImpl = require('./lib/fetch').fetch
93
100
  }
94
101
  const dispatcher = (arguments[1] && arguments[1].dispatcher) || getGlobalDispatcher()
95
- return fetchImpl.apply(dispatcher, arguments)
102
+ try {
103
+ return await fetchImpl.apply(dispatcher, arguments)
104
+ } catch (err) {
105
+ Error.captureStackTrace(err, this)
106
+ throw err
107
+ }
96
108
  }
97
109
  module.exports.Headers = require('./lib/fetch/headers').Headers
98
110
  module.exports.Response = require('./lib/fetch/response').Response
99
111
  module.exports.Request = require('./lib/fetch/request').Request
100
112
  module.exports.FormData = require('./lib/fetch/formdata').FormData
101
113
  module.exports.File = require('./lib/fetch/file').File
114
+
115
+ const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global')
116
+
117
+ module.exports.setGlobalOrigin = setGlobalOrigin
118
+ module.exports.getGlobalOrigin = getGlobalOrigin
102
119
  }
103
120
 
104
121
  module.exports.request = makeDispatcher(api.request)
package/lib/agent.js CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict'
2
2
 
3
3
  const { InvalidArgumentError } = require('./core/errors')
4
- const { kClients, kRunning, kClose, kDestroy, kDispatch } = require('./core/symbols')
4
+ const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols')
5
5
  const DispatcherBase = require('./dispatcher-base')
6
6
  const Pool = require('./pool')
7
7
  const Client = require('./client')
8
8
  const util = require('./core/util')
9
- const RedirectHandler = require('./handler/redirect')
9
+ const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
10
10
  const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')()
11
11
 
12
12
  const kOnConnect = Symbol('onConnect')
@@ -44,7 +44,14 @@ class Agent extends DispatcherBase {
44
44
  connect = { ...connect }
45
45
  }
46
46
 
47
+ this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent)
48
+ ? options.interceptors.Agent
49
+ : [createRedirectInterceptor({ maxRedirections })]
50
+
47
51
  this[kOptions] = { ...util.deepClone(options), connect }
52
+ this[kOptions].interceptors = options.interceptors
53
+ ? { ...options.interceptors }
54
+ : undefined
48
55
  this[kMaxRedirections] = maxRedirections
49
56
  this[kFactory] = factory
50
57
  this[kClients] = new Map()
@@ -108,12 +115,6 @@ class Agent extends DispatcherBase {
108
115
  this[kFinalizer].register(dispatcher, key)
109
116
  }
110
117
 
111
- const { maxRedirections = this[kMaxRedirections] } = opts
112
- if (maxRedirections != null && maxRedirections !== 0) {
113
- opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting.
114
- handler = new RedirectHandler(this, maxRedirections, opts, handler)
115
- }
116
-
117
118
  return dispatcher.dispatch(opts, handler)
118
119
  }
119
120
 
@@ -93,7 +93,7 @@ module.exports = class BodyReadable extends Readable {
93
93
  }
94
94
 
95
95
  push (chunk) {
96
- if (this[kConsume] && chunk !== null) {
96
+ if (this[kConsume] && chunk !== null && this.readableLength === 0) {
97
97
  consumePush(this[kConsume], chunk)
98
98
  return this[kReading] ? super.push(chunk) : true
99
99
  }
@@ -13,7 +13,7 @@ const {
13
13
  kGetDispatcher
14
14
  } = require('./pool-base')
15
15
  const Pool = require('./pool')
16
- const { kUrl } = require('./core/symbols')
16
+ const { kUrl, kInterceptors } = require('./core/symbols')
17
17
  const { parseOrigin } = require('./core/util')
18
18
  const kFactory = Symbol('factory')
19
19
 
@@ -53,6 +53,9 @@ class BalancedPool extends PoolBase {
53
53
  throw new InvalidArgumentError('factory must be a function.')
54
54
  }
55
55
 
56
+ this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool)
57
+ ? opts.interceptors.BalancedPool
58
+ : []
56
59
  this[kFactory] = factory
57
60
 
58
61
  for (const upstream of upstreams) {
package/lib/client.js CHANGED
@@ -7,7 +7,6 @@ const net = require('net')
7
7
  const util = require('./core/util')
8
8
  const Request = require('./core/request')
9
9
  const DispatcherBase = require('./dispatcher-base')
10
- const RedirectHandler = require('./handler/redirect')
11
10
  const {
12
11
  RequestContentLengthMismatchError,
13
12
  ResponseContentLengthMismatchError,
@@ -60,7 +59,8 @@ const {
60
59
  kCounter,
61
60
  kClose,
62
61
  kDestroy,
63
- kDispatch
62
+ kDispatch,
63
+ kInterceptors
64
64
  } = require('./core/symbols')
65
65
 
66
66
  const kClosedResolve = Symbol('kClosedResolve')
@@ -82,6 +82,7 @@ try {
82
82
 
83
83
  class Client extends DispatcherBase {
84
84
  constructor (url, {
85
+ interceptors,
85
86
  maxHeaderSize,
86
87
  headersTimeout,
87
88
  socketTimeout,
@@ -179,6 +180,9 @@ class Client extends DispatcherBase {
179
180
  })
180
181
  }
181
182
 
183
+ this[kInterceptors] = interceptors && interceptors.Client && Array.isArray(interceptors.Client)
184
+ ? interceptors.Client
185
+ : [createRedirectInterceptor({ maxRedirections })]
182
186
  this[kUrl] = util.parseOrigin(url)
183
187
  this[kConnector] = connect
184
188
  this[kSocket] = null
@@ -254,11 +258,6 @@ class Client extends DispatcherBase {
254
258
  }
255
259
 
256
260
  [kDispatch] (opts, handler) {
257
- const { maxRedirections = this[kMaxRedirections] } = opts
258
- if (maxRedirections) {
259
- handler = new RedirectHandler(this, maxRedirections, opts, handler)
260
- }
261
-
262
261
  const origin = opts.origin || this[kUrl].origin
263
262
 
264
263
  const request = new Request(origin, opts, handler)
@@ -319,6 +318,7 @@ class Client extends DispatcherBase {
319
318
  }
320
319
 
321
320
  const constants = require('./llhttp/constants')
321
+ const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
322
322
  const EMPTY_BUF = Buffer.alloc(0)
323
323
 
324
324
  async function lazyllhttp () {
@@ -48,5 +48,6 @@ module.exports = {
48
48
  kMaxRedirections: Symbol('maxRedirections'),
49
49
  kMaxRequests: Symbol('maxRequestsPerClient'),
50
50
  kProxy: Symbol('proxy agent options'),
51
- kCounter: Symbol('socket request counter')
51
+ kCounter: Symbol('socket request counter'),
52
+ kInterceptors: Symbol('dispatch interceptors')
52
53
  }
package/lib/core/util.js CHANGED
@@ -8,6 +8,7 @@ const net = require('net')
8
8
  const { InvalidArgumentError } = require('./errors')
9
9
  const { Blob } = require('buffer')
10
10
  const nodeUtil = require('util')
11
+ const { stringify } = require('querystring')
11
12
 
12
13
  function nop () {}
13
14
 
@@ -26,46 +27,15 @@ function isBlobLike (object) {
26
27
  )
27
28
  }
28
29
 
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
30
  function buildURL (url, queryParams) {
40
31
  if (url.includes('?') || url.includes('#')) {
41
32
  throw new Error('Query params cannot be passed when url already contains "?" or "#".')
42
33
  }
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
34
 
65
- const serializedParams = parts.join('&')
35
+ const stringified = stringify(queryParams)
66
36
 
67
- if (serializedParams) {
68
- url += '?' + serializedParams
37
+ if (stringified) {
38
+ url += '?' + stringified
69
39
  }
70
40
 
71
41
  return url
@@ -6,12 +6,13 @@ const {
6
6
  ClientClosedError,
7
7
  InvalidArgumentError
8
8
  } = require('./core/errors')
9
- const { kDestroy, kClose, kDispatch } = require('./core/symbols')
9
+ const { kDestroy, kClose, kDispatch, kInterceptors } = require('./core/symbols')
10
10
 
11
11
  const kDestroyed = Symbol('destroyed')
12
12
  const kClosed = Symbol('closed')
13
13
  const kOnDestroyed = Symbol('onDestroyed')
14
14
  const kOnClosed = Symbol('onClosed')
15
+ const kInterceptedDispatch = Symbol('Intercepted Dispatch')
15
16
 
16
17
  class DispatcherBase extends Dispatcher {
17
18
  constructor () {
@@ -31,6 +32,23 @@ class DispatcherBase extends Dispatcher {
31
32
  return this[kClosed]
32
33
  }
33
34
 
35
+ get interceptors () {
36
+ return this[kInterceptors]
37
+ }
38
+
39
+ set interceptors (newInterceptors) {
40
+ if (newInterceptors) {
41
+ for (let i = newInterceptors.length - 1; i >= 0; i--) {
42
+ const interceptor = this[kInterceptors][i]
43
+ if (typeof interceptor !== 'function') {
44
+ throw new InvalidArgumentError('interceptor must be an function')
45
+ }
46
+ }
47
+ }
48
+
49
+ this[kInterceptors] = newInterceptors
50
+ }
51
+
34
52
  close (callback) {
35
53
  if (callback === undefined) {
36
54
  return new Promise((resolve, reject) => {
@@ -125,6 +143,20 @@ class DispatcherBase extends Dispatcher {
125
143
  })
126
144
  }
127
145
 
146
+ [kInterceptedDispatch] (opts, handler) {
147
+ if (!this[kInterceptors] || this[kInterceptors].length === 0) {
148
+ this[kInterceptedDispatch] = this[kDispatch]
149
+ return this[kDispatch](opts, handler)
150
+ }
151
+
152
+ let dispatch = this[kDispatch].bind(this)
153
+ for (let i = this[kInterceptors].length - 1; i >= 0; i--) {
154
+ dispatch = this[kInterceptors][i](dispatch)
155
+ }
156
+ this[kInterceptedDispatch] = dispatch
157
+ return dispatch(opts, handler)
158
+ }
159
+
128
160
  dispatch (opts, handler) {
129
161
  if (!handler || typeof handler !== 'object') {
130
162
  throw new InvalidArgumentError('handler must be an object')
@@ -143,7 +175,7 @@ class DispatcherBase extends Dispatcher {
143
175
  throw new ClientClosedError()
144
176
  }
145
177
 
146
- return this[kDispatch](opts, handler)
178
+ return this[kInterceptedDispatch](opts, handler)
147
179
  } catch (err) {
148
180
  if (typeof handler.onError !== 'function') {
149
181
  throw new InvalidArgumentError('invalid onError method')