undici 6.10.1 → 6.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.
package/README.md CHANGED
@@ -7,8 +7,12 @@ An HTTP/1.1 client, written from scratch for Node.js.
7
7
  > Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
8
8
  It is also a Stranger Things reference.
9
9
 
10
+ ## How to get involved
11
+
10
12
  Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel.
11
13
 
14
+ Looking to contribute? Start by reading the [contributing guide](./CONTRIBUTING.md)
15
+
12
16
  ## Install
13
17
 
14
18
  ```
@@ -17,7 +21,7 @@ npm i undici
17
21
 
18
22
  ## Benchmarks
19
23
 
20
- The benchmark is a simple getting data [example](benchmarks/benchmark.js) using a
24
+ The benchmark is a simple getting data [example](https://github.com/nodejs/undici/blob/main/benchmarks/benchmark.js) using a
21
25
  50 TCP connections with a pipelining depth of 10 running on Node 20.10.0.
22
26
 
23
27
  | _Tests_ | _Samples_ | _Result_ | _Tolerance_ | _Difference with slowest_ |
@@ -35,7 +39,7 @@ The benchmark is a simple getting data [example](benchmarks/benchmark.js) using
35
39
  | undici - stream | 15 | 20317.29 req/sec | ± 2.13 % | + 448.46 % |
36
40
  | undici - dispatch | 10 | 24883.28 req/sec | ± 1.54 % | + 571.72 % |
37
41
 
38
- The benchmark is a simple sending data [example](benchmarks/post-benchmark.js) using a
42
+ The benchmark is a simple sending data [example](https://github.com/nodejs/undici/blob/main/benchmarks/post-benchmark.js) using a
39
43
  50 TCP connections with a pipelining depth of 10 running on Node 20.10.0.
40
44
 
41
45
  | _Tests_ | _Samples_ | _Result_ | _Tolerance_ | _Difference with slowest_ |
@@ -20,8 +20,6 @@ diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
20
20
  console.log('method', request.method)
21
21
  console.log('path', request.path)
22
22
  console.log('headers') // array of strings, e.g: ['foo', 'bar']
23
- request.addHeader('hello', 'world')
24
- console.log('headers', request.headers) // e.g. ['foo', 'bar', 'hello', 'world']
25
23
  })
26
24
  ```
27
25
 
@@ -185,7 +185,7 @@ function setupTimeout (onConnectTimeout, timeout) {
185
185
  function onConnectTimeout (socket) {
186
186
  let message = 'Connect Timeout Error'
187
187
  if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
188
- message = +` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')})`
188
+ message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')})`
189
189
  }
190
190
  util.destroy(socket, new ConnectTimeoutError(message))
191
191
  }
@@ -91,6 +91,8 @@ class Request {
91
91
 
92
92
  this.abort = null
93
93
 
94
+ this.publicInterface = null
95
+
94
96
  if (body == null) {
95
97
  this.body = null
96
98
  } else if (isStream(body)) {
@@ -187,10 +189,32 @@ class Request {
187
189
  this[kHandler] = handler
188
190
 
189
191
  if (channels.create.hasSubscribers) {
190
- channels.create.publish({ request: this })
192
+ channels.create.publish({ request: this.getPublicInterface() })
191
193
  }
192
194
  }
193
195
 
196
+ getPublicInterface () {
197
+ const self = this
198
+ this.publicInterface ??= {
199
+ get origin () {
200
+ return self.origin
201
+ },
202
+ get method () {
203
+ return self.method
204
+ },
205
+ get path () {
206
+ return self.path
207
+ },
208
+ get headers () {
209
+ return self.headers
210
+ },
211
+ get completed () {
212
+ return self.completed
213
+ }
214
+ }
215
+ return this.publicInterface
216
+ }
217
+
194
218
  onBodySent (chunk) {
195
219
  if (this[kHandler].onBodySent) {
196
220
  try {
@@ -203,7 +227,7 @@ class Request {
203
227
 
204
228
  onRequestSent () {
205
229
  if (channels.bodySent.hasSubscribers) {
206
- channels.bodySent.publish({ request: this })
230
+ channels.bodySent.publish({ request: this.getPublicInterface() })
207
231
  }
208
232
 
209
233
  if (this[kHandler].onRequestSent) {
@@ -236,7 +260,7 @@ class Request {
236
260
  assert(!this.completed)
237
261
 
238
262
  if (channels.headers.hasSubscribers) {
239
- channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
263
+ channels.headers.publish({ request: this.getPublicInterface(), response: { statusCode, headers, statusText } })
240
264
  }
241
265
 
242
266
  try {
@@ -272,7 +296,7 @@ class Request {
272
296
 
273
297
  this.completed = true
274
298
  if (channels.trailers.hasSubscribers) {
275
- channels.trailers.publish({ request: this, trailers })
299
+ channels.trailers.publish({ request: this.getPublicInterface(), trailers })
276
300
  }
277
301
 
278
302
  try {
@@ -287,7 +311,7 @@ class Request {
287
311
  this.onFinally()
288
312
 
289
313
  if (channels.error.hasSubscribers) {
290
- channels.error.publish({ request: this, error })
314
+ channels.error.publish({ request: this.getPublicInterface(), error })
291
315
  }
292
316
 
293
317
  if (this.aborted) {
@@ -309,11 +333,6 @@ class Request {
309
333
  this.endHandler = null
310
334
  }
311
335
  }
312
-
313
- addHeader (key, value) {
314
- processHeader(this, key, value)
315
- return this
316
- }
317
336
  }
318
337
 
319
338
  function processHeader (request, key, val) {
package/lib/core/util.js CHANGED
@@ -246,9 +246,6 @@ function bufferToLowerCasedHeaderName (value) {
246
246
  * @returns {Record<string, string | string[]>}
247
247
  */
248
248
  function parseHeaders (headers, obj) {
249
- // For H2 support
250
- if (!Array.isArray(headers)) return headers
251
-
252
249
  if (obj === undefined) obj = {}
253
250
  for (let i = 0; i < headers.length; i += 2) {
254
251
  const key = headerNameToString(headers[i])
@@ -993,7 +993,7 @@ function writeH1 (client, request) {
993
993
  }
994
994
 
995
995
  if (channels.sendHeaders.hasSubscribers) {
996
- channels.sendHeaders.publish({ request, headers: header, socket })
996
+ channels.sendHeaders.publish({ request: request.getPublicInterface(), headers: header, socket })
997
997
  }
998
998
 
999
999
  /* istanbul ignore else: assertion */
@@ -54,6 +54,20 @@ const {
54
54
  }
55
55
  } = http2
56
56
 
57
+ function parseH2Headers (headers) {
58
+ // set-cookie is always an array. Duplicates are added to the array.
59
+ // For duplicate cookie headers, the values are joined together with '; '.
60
+ headers = Object.entries(headers).flat(2)
61
+
62
+ const result = []
63
+
64
+ for (const header of headers) {
65
+ result.push(Buffer.from(header))
66
+ }
67
+
68
+ return result
69
+ }
70
+
57
71
  async function connectH2 (client, socket) {
58
72
  client[kSocket] = socket
59
73
 
@@ -391,9 +405,27 @@ function writeH2 (client, request) {
391
405
  const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
392
406
  request.onResponseStarted()
393
407
 
394
- if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
408
+ // Due to the stream nature, it is possible we face a race condition
409
+ // where the stream has been assigned, but the request has been aborted
410
+ // the request remains in-flight and headers hasn't been received yet
411
+ // for those scenarios, best effort is to destroy the stream immediately
412
+ // as there's no value to keep it open.
413
+ if (request.aborted || request.completed) {
414
+ const err = new RequestAbortedError()
415
+ errorRequest(client, request, err)
416
+ util.destroy(stream, err)
417
+ return
418
+ }
419
+
420
+ if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
395
421
  stream.pause()
396
422
  }
423
+
424
+ stream.on('data', (chunk) => {
425
+ if (request.onData(chunk) === false) {
426
+ stream.pause()
427
+ }
428
+ })
397
429
  })
398
430
 
399
431
  stream.once('end', () => {
@@ -418,12 +450,6 @@ function writeH2 (client, request) {
418
450
  util.destroy(stream, err)
419
451
  })
420
452
 
421
- stream.on('data', (chunk) => {
422
- if (request.onData(chunk) === false) {
423
- stream.pause()
424
- }
425
- })
426
-
427
453
  stream.once('close', () => {
428
454
  session[kOpenStreams] -= 1
429
455
  // TODO(HTTP/2): unref only if current streams count is 0
@@ -242,14 +242,12 @@ class RetryHandler {
242
242
  }
243
243
 
244
244
  const { start, size, end = size } = range
245
-
246
245
  assert(
247
- start != null && Number.isFinite(start) && this.start !== start,
246
+ start != null && Number.isFinite(start),
248
247
  'content-range mismatch'
249
248
  )
250
- assert(Number.isFinite(start))
251
249
  assert(
252
- end != null && Number.isFinite(end) && this.end !== end,
250
+ end != null && Number.isFinite(end),
253
251
  'invalid content-length'
254
252
  )
255
253
 
@@ -3,6 +3,9 @@
3
3
  const { Transform } = require('node:stream')
4
4
  const { Console } = require('node:console')
5
5
 
6
+ const PERSISTENT = process.versions.icu ? '✅' : 'Y '
7
+ const NOT_PERSISTENT = process.versions.icu ? '❌' : 'N '
8
+
6
9
  /**
7
10
  * Gets the output of `console.table(…)` as a string.
8
11
  */
@@ -29,7 +32,7 @@ module.exports = class PendingInterceptorsFormatter {
29
32
  Origin: origin,
30
33
  Path: path,
31
34
  'Status code': statusCode,
32
- Persistent: persist ? '✅' : '❌',
35
+ Persistent: persist ? PERSISTENT : NOT_PERSISTENT,
33
36
  Invocations: timesInvoked,
34
37
  Remaining: persist ? Infinity : times - timesInvoked
35
38
  }))
@@ -102,7 +102,7 @@ function setCookie (headers, cookie) {
102
102
  const str = stringify(cookie)
103
103
 
104
104
  if (str) {
105
- headers.append('Set-Cookie', stringify(cookie))
105
+ headers.append('Set-Cookie', str)
106
106
  }
107
107
  }
108
108
 
@@ -10,6 +10,7 @@ const { parseMIMEType } = require('../fetch/data-url')
10
10
  const { MessageEvent } = require('../websocket/events')
11
11
  const { isNetworkError } = require('../fetch/response')
12
12
  const { delay } = require('./util')
13
+ const { kEnumerableProperty } = require('../../core/util')
13
14
 
14
15
  let experimentalWarned = false
15
16
 
@@ -459,6 +460,16 @@ const constantsPropertyDescriptors = {
459
460
  Object.defineProperties(EventSource, constantsPropertyDescriptors)
460
461
  Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors)
461
462
 
463
+ Object.defineProperties(EventSource.prototype, {
464
+ close: kEnumerableProperty,
465
+ onerror: kEnumerableProperty,
466
+ onmessage: kEnumerableProperty,
467
+ onopen: kEnumerableProperty,
468
+ readyState: kEnumerableProperty,
469
+ url: kEnumerableProperty,
470
+ withCredentials: kEnumerableProperty
471
+ })
472
+
462
473
  webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
463
474
  { key: 'withCredentials', converter: webidl.converters.boolean, defaultValue: false }
464
475
  ])
@@ -8,12 +8,12 @@ const encoder = new TextEncoder()
8
8
  * @see https://mimesniff.spec.whatwg.org/#http-token-code-point
9
9
  */
10
10
  const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/
11
- const HTTP_WHITESPACE_REGEX = /[\u000A|\u000D|\u0009|\u0020]/ // eslint-disable-line
11
+ const HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/ // eslint-disable-line
12
12
  const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line
13
13
  /**
14
14
  * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
15
15
  */
16
- const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line
16
+ const HTTP_QUOTED_STRING_TOKENS = /[\u0009\u0020-\u007E\u0080-\u00FF]/ // eslint-disable-line
17
17
 
18
18
  // https://fetch.spec.whatwg.org/#data-url-processor
19
19
  /** @param {URL} dataURL */
@@ -12,7 +12,7 @@ const {
12
12
  } = require('./util')
13
13
  const { webidl } = require('./webidl')
14
14
  const assert = require('node:assert')
15
- const util = require('util')
15
+ const util = require('node:util')
16
16
 
17
17
  const kHeadersMap = Symbol('headers map')
18
18
  const kHeadersSortedMap = Symbol('headers map sorted')
@@ -2141,29 +2141,6 @@ async function httpNetworkFetch (
2141
2141
  codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim())
2142
2142
  }
2143
2143
  location = headersList.get('location', true)
2144
- } else {
2145
- const keys = Object.keys(rawHeaders)
2146
- for (let i = 0; i < keys.length; ++i) {
2147
- // The header names are already in lowercase.
2148
- const key = keys[i]
2149
- const value = rawHeaders[key]
2150
- if (key === 'set-cookie') {
2151
- for (let j = 0; j < value.length; ++j) {
2152
- headersList.append(key, value[j], true)
2153
- }
2154
- } else {
2155
- headersList.append(key, value, true)
2156
- }
2157
- }
2158
- // For H2, The header names are already in lowercase,
2159
- // so we can avoid the `HeadersList#get` call here.
2160
- const contentEncoding = rawHeaders['content-encoding']
2161
- if (contentEncoding) {
2162
- // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2163
- // "All content-coding values are case-insensitive..."
2164
- codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()).reverse()
2165
- }
2166
- location = rawHeaders.location
2167
2144
  }
2168
2145
 
2169
2146
  this.body = new Readable({ read: resume })
@@ -44,6 +44,12 @@ function responseLocationURL (response, requestFragment) {
44
44
  // 3. If location is a header value, then set location to the result of
45
45
  // parsing location with response’s URL.
46
46
  if (location !== null && isValidHeaderValue(location)) {
47
+ if (!isValidEncodedURL(location)) {
48
+ // Some websites respond location header in UTF-8 form without encoding them as ASCII
49
+ // and major browsers redirect them to correctly UTF-8 encoded addresses.
50
+ // Here, we handle that behavior in the same way.
51
+ location = normalizeBinaryStringToUtf8(location)
52
+ }
47
53
  location = new URL(location, responseURL(response))
48
54
  }
49
55
 
@@ -57,6 +63,36 @@ function responseLocationURL (response, requestFragment) {
57
63
  return location
58
64
  }
59
65
 
66
+ /**
67
+ * @see https://www.rfc-editor.org/rfc/rfc1738#section-2.2
68
+ * @param {string} url
69
+ * @returns {boolean}
70
+ */
71
+ function isValidEncodedURL (url) {
72
+ for (const c of url) {
73
+ const code = c.charCodeAt(0)
74
+ // Not used in US-ASCII
75
+ if (code >= 0x80) {
76
+ return false
77
+ }
78
+ // Control characters
79
+ if ((code >= 0x00 && code <= 0x1F) || code === 0x7F) {
80
+ return false
81
+ }
82
+ }
83
+ return true
84
+ }
85
+
86
+ /**
87
+ * If string contains non-ASCII characters, assumes it's UTF-8 encoded and decodes it.
88
+ * Since UTF-8 is a superset of ASCII, this will work for ASCII strings as well.
89
+ * @param {string} value
90
+ * @returns {string}
91
+ */
92
+ function normalizeBinaryStringToUtf8 (value) {
93
+ return Buffer.from(value, 'binary').toString('utf8')
94
+ }
95
+
60
96
  /** @returns {URL} */
61
97
  function requestCurrentURL (request) {
62
98
  return request.urlList[request.urlList.length - 1]
@@ -12,8 +12,6 @@ const { WebsocketFrameSend } = require('./frame')
12
12
  // Copyright (c) 2013 Arnout Kazemier and contributors
13
13
  // Copyright (c) 2016 Luigi Pinca and contributors
14
14
 
15
- const textDecoder = new TextDecoder('utf-8', { fatal: true })
16
-
17
15
  class ByteParser extends Writable {
18
16
  #buffers = []
19
17
  #byteOffset = 0
@@ -316,7 +314,8 @@ class ByteParser extends Writable {
316
314
  }
317
315
 
318
316
  try {
319
- reason = textDecoder.decode(reason)
317
+ // TODO: optimize this
318
+ reason = new TextDecoder('utf-8', { fatal: true }).decode(reason)
320
319
  } catch {
321
320
  return null
322
321
  }
@@ -68,8 +68,6 @@ function fireEvent (e, target, eventConstructor = Event, eventInitDict = {}) {
68
68
  target.dispatchEvent(event)
69
69
  }
70
70
 
71
- const textDecoder = new TextDecoder('utf-8', { fatal: true })
72
-
73
71
  /**
74
72
  * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
75
73
  * @param {import('./websocket').WebSocket} ws
@@ -89,7 +87,7 @@ function websocketMessageReceived (ws, type, data) {
89
87
  // -> type indicates that the data is Text
90
88
  // a new DOMString containing data
91
89
  try {
92
- dataForEvent = textDecoder.decode(data)
90
+ dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data)
93
91
  } catch {
94
92
  failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.')
95
93
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.10.1",
3
+ "version": "6.11.0",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -69,10 +69,13 @@
69
69
  "lint:fix": "standard --fix | snazzy",
70
70
  "test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript",
71
71
  "test:javascript": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:jest",
72
+ "test:javascript:withoutintl": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch:nobuild && npm run test:cookies && npm run test:eventsource:nobuild && npm run test:wpt:withoutintl && npm run test:node-test",
72
73
  "test:cookies": "borp -p \"test/cookie/*.js\"",
73
74
  "test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"",
74
- "test:eventsource": "npm run build:node && borp --expose-gc -p \"test/eventsource/*.js\"",
75
- "test:fetch": "npm run build:node && borp --expose-gc -p \"test/fetch/*.js\" && borp -p \"test/webidl/*.js\" && borp -p \"test/busboy/*.js\"",
75
+ "test:eventsource": "npm run build:node && npm run test:eventsource:nobuild",
76
+ "test:eventsource:nobuild": "borp --expose-gc -p \"test/eventsource/*.js\"",
77
+ "test:fetch": "npm run build:node && npm run test:fetch:nobuild",
78
+ "test:fetch:nobuild": "borp --expose-gc -p \"test/fetch/*.js\" && borp -p \"test/webidl/*.js\" && borp -p \"test/busboy/*.js\"",
76
79
  "test:jest": "cross-env NODE_V8_COVERAGE= jest",
77
80
  "test:unit": "borp --expose-gc -p \"test/*.js\"",
78
81
  "test:node-test": "borp -p \"test/node-test/**/*.js\"",
@@ -81,6 +84,7 @@
81
84
  "test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
82
85
  "test:websocket": "borp -p \"test/websocket/*.js\"",
83
86
  "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
87
+ "test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
84
88
  "coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",
85
89
  "coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report:ci",
86
90
  "coverage:clean": "node ./scripts/clean-coverage.js",
@@ -96,7 +100,7 @@
96
100
  "@sinonjs/fake-timers": "^11.1.0",
97
101
  "@types/node": "^18.0.3",
98
102
  "abort-controller": "^3.0.0",
99
- "borp": "^0.9.1",
103
+ "borp": "^0.10.0",
100
104
  "c8": "^9.1.0",
101
105
  "cross-env": "^7.0.3",
102
106
  "dns-packet": "^5.4.0",
@@ -112,7 +116,7 @@
112
116
  "proxy": "^2.1.1",
113
117
  "snazzy": "^9.0.0",
114
118
  "standard": "^17.0.0",
115
- "tsd": "^0.30.1",
119
+ "tsd": "^0.31.0",
116
120
  "typescript": "^5.0.2",
117
121
  "ws": "^8.11.0"
118
122
  },
@@ -9,8 +9,7 @@ declare namespace DiagnosticsChannel {
9
9
  completed: boolean;
10
10
  method?: Dispatcher.HttpMethod;
11
11
  path: string;
12
- headers: string;
13
- addHeader(key: string, value: string): Request;
12
+ headers: any;
14
13
  }
15
14
  interface Response {
16
15
  statusCode: number;
@@ -19,8 +19,8 @@ declare class Dispatcher extends EventEmitter {
19
19
  connect(options: Dispatcher.ConnectOptions): Promise<Dispatcher.ConnectData>;
20
20
  connect(options: Dispatcher.ConnectOptions, callback: (err: Error | null, data: Dispatcher.ConnectData) => void): void;
21
21
  /** Compose a chain of dispatchers */
22
- compose(dispatchers: Dispatcher['dispatch'][]): Dispatcher.ComposedDispatcher;
23
- compose(...dispatchers: Dispatcher['dispatch'][]): Dispatcher.ComposedDispatcher;
22
+ compose(dispatchers: Dispatcher.DispatcherInterceptor[]): Dispatcher.ComposedDispatcher;
23
+ compose(...dispatchers: Dispatcher.DispatcherInterceptor[]): Dispatcher.ComposedDispatcher;
24
24
  /** Performs an HTTP request. */
25
25
  request(options: Dispatcher.RequestOptions): Promise<Dispatcher.ResponseData>;
26
26
  request(options: Dispatcher.RequestOptions, callback: (err: Error | null, data: Dispatcher.ResponseData) => void): void;