undici 5.2.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
@@ -198,7 +198,7 @@ You can pass an optional dispatcher to `fetch` as:
198
198
 
199
199
  ```js
200
200
  import { fetch, Agent } from 'undici'
201
-
201
+
202
202
  const res = await fetch('https://example.com', {
203
203
  // Mocks are also supported
204
204
  dispatcher: new Agent({
@@ -375,6 +375,7 @@ Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
375
375
  * [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
376
376
  * [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
377
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>
378
379
  * [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
379
380
  * [__Szymon Marczak__](https://github.com/szmarczak), <https://www.npmjs.com/~szmarczak>
380
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.
@@ -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.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 }
@@ -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,
@@ -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)
@@ -186,6 +199,7 @@ module.exports = {
186
199
  BodyTimeoutError,
187
200
  RequestContentLengthMismatchError,
188
201
  ConnectTimeoutError,
202
+ ResponseStatusCodeError,
189
203
  InvalidArgumentError,
190
204
  InvalidReturnValueError,
191
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 }
@@ -31,7 +31,6 @@ const {
31
31
  coarsenedSharedCurrentTime,
32
32
  createDeferredPromise,
33
33
  isBlobLike,
34
- CORBCheck,
35
34
  sameOrigin,
36
35
  isCancelled,
37
36
  isAborted
@@ -52,7 +51,6 @@ const EE = require('events')
52
51
  const { Readable, pipeline } = require('stream')
53
52
  const { isErrored, isReadable } = require('../core/util')
54
53
  const { dataURLProcessor } = require('./dataURL')
55
- const { kIsMockActive } = require('../mock/mock-symbols')
56
54
  const { TransformStream } = require('stream/web')
57
55
 
58
56
  /** @type {import('buffer').resolveObjectURL} */
@@ -588,18 +586,8 @@ async function mainFetch (fetchParams, recursive = false) {
588
586
  // 2. Set request’s response tainting to "opaque".
589
587
  request.responseTainting = 'opaque'
590
588
 
591
- // 3. Let noCorsResponse be the result of running scheme fetch given
592
- // fetchParams.
593
- const noCorsResponse = await schemeFetch(fetchParams)
594
-
595
- // 4. If noCorsResponse is a filtered response or the CORB check with
596
- // request and noCorsResponse returns allowed, then return noCorsResponse.
597
- if (noCorsResponse.status === 0 || CORBCheck(request, noCorsResponse) === 'allowed') {
598
- return noCorsResponse
599
- }
600
-
601
- // 5. Return a new response whose status is noCorsResponse’s status.
602
- return makeResponse({ status: noCorsResponse.status })
589
+ // 3. Return the result of running scheme fetch given fetchParams.
590
+ return await schemeFetch(fetchParams)
603
591
  }
604
592
 
605
593
  // request’s current URL’s scheme is not an HTTP(S) scheme
@@ -1923,7 +1911,7 @@ async function httpNetworkFetch (
1923
1911
  path: url.pathname + url.search,
1924
1912
  origin: url.origin,
1925
1913
  method: request.method,
1926
- body: fetchParams.controller.dispatcher[kIsMockActive] ? request.body && request.body.source : body,
1914
+ body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body,
1927
1915
  headers: [...request.headersList].flat(),
1928
1916
  maxRedirections: 0,
1929
1917
  bodyTimeout: 300_000,
@@ -516,10 +516,6 @@ class Request {
516
516
  }
517
517
 
518
518
  get [Symbol.toStringTag] () {
519
- if (!(this instanceof Request)) {
520
- throw new TypeError('Illegal invocation')
521
- }
522
-
523
519
  return this.constructor.name
524
520
  }
525
521
 
@@ -5,7 +5,7 @@ const { AbortError } = require('../core/errors')
5
5
  const { extractBody, cloneBody, mixinBody } = require('./body')
6
6
  const util = require('../core/util')
7
7
  const { kEnumerableProperty } = util
8
- const { responseURL, isValidReasonPhrase, toUSVString, isCancelled, isAborted } = require('./util')
8
+ const { responseURL, isValidReasonPhrase, toUSVString, isCancelled, isAborted, serializeJavascriptValueToJSONString } = require('./util')
9
9
  const {
10
10
  redirectStatus,
11
11
  nullBodyStatus,
@@ -35,6 +35,50 @@ class Response {
35
35
  return responseObject
36
36
  }
37
37
 
38
+ // https://fetch.spec.whatwg.org/#dom-response-json
39
+ static json (data, init = {}) {
40
+ if (arguments.length === 0) {
41
+ throw new TypeError(
42
+ 'Failed to execute \'json\' on \'Response\': 1 argument required, but 0 present.'
43
+ )
44
+ }
45
+
46
+ if (init === null || typeof init !== 'object') {
47
+ throw new TypeError(
48
+ `Failed to execute 'json' on 'Response': init must be a RequestInit, found ${typeof init}.`
49
+ )
50
+ }
51
+
52
+ init = {
53
+ status: 200,
54
+ statusText: '',
55
+ headers: new HeadersList(),
56
+ ...init
57
+ }
58
+
59
+ // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
60
+ const bytes = new TextEncoder('utf-8').encode(
61
+ serializeJavascriptValueToJSONString(data)
62
+ )
63
+
64
+ // 2. Let body be the result of extracting bytes.
65
+ const body = extractBody(bytes)
66
+
67
+ // 3. Let responseObject be the result of creating a Response object, given a new response,
68
+ // "response", and this’s relevant Realm.
69
+ const relevantRealm = { settingsObject: {} }
70
+ const responseObject = new Response()
71
+ responseObject[kRealm] = relevantRealm
72
+ responseObject[kHeaders][kGuard] = 'response'
73
+ responseObject[kHeaders][kRealm] = relevantRealm
74
+
75
+ // 4. Perform initialize a response given responseObject, init, and (body, "application/json").
76
+ initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
77
+
78
+ // 5. Return responseObject.
79
+ return responseObject
80
+ }
81
+
38
82
  // Creates a redirect Response that redirects to url with status status.
39
83
  static redirect (...args) {
40
84
  const relevantRealm = { settingsObject: {} }
@@ -105,34 +149,10 @@ class Response {
105
149
  // TODO
106
150
  this[kRealm] = { settingsObject: {} }
107
151
 
108
- // 1. If init["status"] is not in the range 200 to 599, inclusive, then
109
- // throw a RangeError.
110
- if ('status' in init && init.status !== undefined) {
111
- if (!Number.isFinite(init.status)) {
112
- throw new TypeError()
113
- }
114
-
115
- if (init.status < 200 || init.status > 599) {
116
- throw new RangeError(
117
- `Failed to construct 'Response': The status provided (${init.status}) is outside the range [200, 599].`
118
- )
119
- }
120
- }
121
-
122
- if ('statusText' in init && init.statusText !== undefined) {
123
- // 2. If init["statusText"] does not match the reason-phrase token
124
- // production, then throw a TypeError.
125
- // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
126
- // reason-phrase = *( HTAB / SP / VCHAR / obs-text )
127
- if (!isValidReasonPhrase(String(init.statusText))) {
128
- throw new TypeError('Invalid statusText')
129
- }
130
- }
131
-
132
- // 3. Set this’s response to a new response.
152
+ // 1. Set this’s response to a new response.
133
153
  this[kState] = makeResponse({})
134
154
 
135
- // 4. Set this’s headers to a new Headers object with this’s relevant
155
+ // 2. Set this’s headers to a new Headers object with this’s relevant
136
156
  // Realm, whose header list is this’s response’s header list and guard
137
157
  // is "response".
138
158
  this[kHeaders] = new Headers()
@@ -140,48 +160,20 @@ class Response {
140
160
  this[kHeaders][kHeadersList] = this[kState].headersList
141
161
  this[kHeaders][kRealm] = this[kRealm]
142
162
 
143
- // 5. Set this’s response’s status to init["status"].
144
- if ('status' in init && init.status !== undefined) {
145
- this[kState].status = init.status
146
- }
163
+ // 3. Let bodyWithType be null.
164
+ let bodyWithType = null
147
165
 
148
- // 6. Set this’s response’s status message to init["statusText"].
149
- if ('statusText' in init && init.statusText !== undefined) {
150
- this[kState].statusText = String(init.statusText)
151
- }
152
-
153
- // 7. If init["headers"] exists, then fill this’s headers with init["headers"].
154
- if ('headers' in init) {
155
- fill(this[kState].headersList, init.headers)
156
- }
157
-
158
- // 8. If body is non-null, then:
166
+ // 4. If body is non-null, then set bodyWithType to the result of extracting body.
159
167
  if (body != null) {
160
- // 1. If init["status"] is a null body status, then throw a TypeError.
161
- if (nullBodyStatus.includes(init.status)) {
162
- throw new TypeError('Response with null body status cannot have body')
163
- }
164
-
165
- // 2. Let Content-Type be null.
166
- // 3. Set this’s response’s body and Content-Type to the result of
167
- // extracting body.
168
- const [extractedBody, contentType] = extractBody(body)
169
- this[kState].body = extractedBody
170
-
171
- // 4. If Content-Type is non-null and this’s response’s header list does
172
- // not contain `Content-Type`, then append `Content-Type`/Content-Type
173
- // to this’s response’s header list.
174
- if (contentType && !this.headers.has('content-type')) {
175
- this.headers.append('content-type', contentType)
176
- }
168
+ const [extractedBody, type] = extractBody(body)
169
+ bodyWithType = { body: extractedBody, type }
177
170
  }
171
+
172
+ // 5. Perform initialize a response given this, init, and bodyWithType.
173
+ initializeResponse(this, init, bodyWithType)
178
174
  }
179
175
 
180
176
  get [Symbol.toStringTag] () {
181
- if (!(this instanceof Response)) {
182
- throw new TypeError('Illegal invocation')
183
- }
184
-
185
177
  return this.constructor.name
186
178
  }
187
179
 
@@ -477,6 +469,57 @@ function makeAppropriateNetworkError (fetchParams) {
477
469
  : makeNetworkError(fetchParams.controller.terminated.reason)
478
470
  }
479
471
 
472
+ // https://whatpr.org/fetch/1392.html#initialize-a-response
473
+ function initializeResponse (response, init, body) {
474
+ // 1. If init["status"] is not in the range 200 to 599, inclusive, then
475
+ // throw a RangeError.
476
+ if (init.status != null && (init.status < 200 || init.status > 599)) {
477
+ throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
478
+ }
479
+
480
+ // 2. If init["statusText"] does not match the reason-phrase token production,
481
+ // then throw a TypeError.
482
+ if ('statusText' in init && init.statusText != null) {
483
+ // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
484
+ // reason-phrase = *( HTAB / SP / VCHAR / obs-text )
485
+ if (!isValidReasonPhrase(String(init.statusText))) {
486
+ throw new TypeError('Invalid statusText')
487
+ }
488
+ }
489
+
490
+ // 3. Set response’s response’s status to init["status"].
491
+ if ('status' in init && init.status != null) {
492
+ response[kState].status = init.status
493
+ }
494
+
495
+ // 4. Set response’s response’s status message to init["statusText"].
496
+ if ('statusText' in init && init.statusText != null) {
497
+ response[kState].statusText = init.statusText
498
+ }
499
+
500
+ // 5. If init["headers"] exists, then fill response’s headers with init["headers"].
501
+ if ('headers' in init && init.headers != null) {
502
+ fill(response[kState].headersList, init.headers)
503
+ }
504
+
505
+ // 6. If body was given, then:
506
+ if (body) {
507
+ // 1. If response's status is a null body status, then throw a TypeError.
508
+ if (nullBodyStatus.includes(response.status)) {
509
+ throw new TypeError()
510
+ }
511
+
512
+ // 2. Set response's body to body's body.
513
+ response[kState].body = body.body
514
+
515
+ // 3. If body's type is non-null and response's header list does not contain
516
+ // `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
517
+ if (body.type != null && !response[kState].headersList.has('Content-Type')) {
518
+ response[kState].headersList.append('content-type', body.type)
519
+ }
520
+ }
521
+ }
522
+
480
523
  module.exports = {
481
524
  makeNetworkError,
482
525
  makeResponse,
package/lib/fetch/util.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { redirectStatus } = require('./constants')
4
4
  const { performance } = require('perf_hooks')
5
5
  const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
6
+ const assert = require('assert')
6
7
 
7
8
  let File
8
9
 
@@ -316,47 +317,6 @@ function sameOrigin (A, B) {
316
317
  return false
317
318
  }
318
319
 
319
- // https://fetch.spec.whatwg.org/#corb-check
320
- function CORBCheck (request, response) {
321
- // 1. If request’s initiator is "download", then return allowed.
322
- if (request.initiator === 'download') {
323
- return 'allowed'
324
- }
325
-
326
- // 2. If request’s current URL’s scheme is not an HTTP(S) scheme, then return allowed.
327
- if (!/^https?$/.test(request.currentURL.scheme)) {
328
- return 'allowed'
329
- }
330
-
331
- // 3. Let mimeType be the result of extracting a MIME type from response’s header list.
332
- const mimeType = response.headersList.get('content-type')
333
-
334
- // 4. If mimeType is failure, then return allowed.
335
- if (mimeType === '') {
336
- return 'allowed'
337
- }
338
-
339
- // 5. If response’s status is 206 and mimeType is a CORB-protected MIME type, then return blocked.
340
-
341
- const isCORBProtectedMIME =
342
- (/^text\/html\b/.test(mimeType) ||
343
- /^application\/javascript\b/.test(mimeType) ||
344
- /^application\/xml\b/.test(mimeType)) && !/^application\/xml\+svg\b/.test(mimeType)
345
-
346
- if (response.status === 206 && isCORBProtectedMIME) {
347
- return 'blocked'
348
- }
349
-
350
- // 6. If determine nosniff with response’s header list is true and mimeType is a CORB-protected MIME type or its essence is "text/plain", then return blocked.
351
- // https://fetch.spec.whatwg.org/#determinenosniff
352
- if (response.headersList.get('x-content-type-options') && isCORBProtectedMIME) {
353
- return 'blocked'
354
- }
355
-
356
- // 7. Return allowed.
357
- return 'allowed'
358
- }
359
-
360
320
  function createDeferredPromise () {
361
321
  let res
362
322
  let rej
@@ -384,6 +344,23 @@ function normalizeMethod (method) {
384
344
  : method
385
345
  }
386
346
 
347
+ // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
348
+ function serializeJavascriptValueToJSONString (value) {
349
+ // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
350
+ const result = JSON.stringify(value)
351
+
352
+ // 2. If result is undefined, then throw a TypeError.
353
+ if (result === undefined) {
354
+ throw new TypeError('Value is not JSON serializable')
355
+ }
356
+
357
+ // 3. Assert: result is a string.
358
+ assert(typeof result === 'string')
359
+
360
+ // 4. Return result.
361
+ return result
362
+ }
363
+
387
364
  module.exports = {
388
365
  isAborted,
389
366
  isCancelled,
@@ -412,6 +389,6 @@ module.exports = {
412
389
  isFileLike,
413
390
  isValidReasonPhrase,
414
391
  sameOrigin,
415
- CORBCheck,
416
- normalizeMethod
392
+ normalizeMethod,
393
+ serializeJavascriptValueToJSONString
417
394
  }
@@ -96,6 +96,12 @@ class MockAgent extends Dispatcher {
96
96
  this[kNetConnect] = false
97
97
  }
98
98
 
99
+ // This is required to bypass issues caused by using global symbols - see:
100
+ // https://github.com/nodejs/undici/issues/1447
101
+ get isMockActive () {
102
+ return this[kIsMockActive]
103
+ }
104
+
99
105
  [kMockAgentSet] (origin, dispatcher) {
100
106
  this[kClients].set(origin, new FakeWeakRef(dispatcher))
101
107
  }
@@ -6,7 +6,6 @@ const {
6
6
  kMockAgent,
7
7
  kOriginalDispatch,
8
8
  kOrigin,
9
- kIsMockActive,
10
9
  kGetNetConnect
11
10
  } = require('./mock-symbols')
12
11
 
@@ -302,7 +301,7 @@ function buildMockDispatch () {
302
301
  const originalDispatch = this[kOriginalDispatch]
303
302
 
304
303
  return function dispatch (opts, handler) {
305
- if (agent[kIsMockActive]) {
304
+ if (agent.isMockActive) {
306
305
  try {
307
306
  mockDispatch.call(this, opts, handler)
308
307
  } catch (error) {
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { kProxy, kClose, kDestroy } = require('./core/symbols')
4
- const { URL } = require('url')
5
4
  const Agent = require('./agent')
6
5
  const DispatcherBase = require('./dispatcher-base')
7
6
  const { InvalidArgumentError } = require('./core/errors')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "5.2.0",
3
+ "version": "5.3.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": {
@@ -48,7 +48,7 @@
48
48
  "lint:fix": "standard --fix | snazzy",
49
49
  "test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:jest && tsd",
50
50
  "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch",
51
- "test:fetch": "node scripts/verifyVersion.js 16 || tap test/fetch/*.js",
51
+ "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js)",
52
52
  "test:jest": "jest",
53
53
  "test:tap": "tap test/*.js test/diagnostics-channel/*.js",
54
54
  "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w",
@@ -91,6 +91,7 @@
91
91
  "sinon": "^14.0.0",
92
92
  "snazzy": "^9.0.0",
93
93
  "standard": "^17.0.0",
94
+ "table": "^6.8.0",
94
95
  "tap": "^16.1.0",
95
96
  "tsd": "^0.20.0",
96
97
  "wait-on": "^6.0.0"
@@ -0,0 +1,66 @@
1
+ import { Socket } from "net";
2
+ import { connector } from "./connector";
3
+ import { HttpMethod } from "./dispatcher";
4
+
5
+ declare namespace DiagnosticsChannel {
6
+ interface Request {
7
+ origin?: string | URL;
8
+ completed: boolean;
9
+ method?: HttpMethod;
10
+ path: string;
11
+ headers: string;
12
+ addHeader(key: string, value: string): Request;
13
+ }
14
+ interface Response {
15
+ statusCode: number;
16
+ statusText: string;
17
+ headers: Array<Buffer>;
18
+ }
19
+ type Error = unknown;
20
+ interface ConnectParams {
21
+ host: URL["host"];
22
+ hostname: URL["hostname"];
23
+ protocol: URL["protocol"];
24
+ port: URL["port"];
25
+ servername: string | null;
26
+ }
27
+ type Connector = typeof connector;
28
+ export interface RequestCreateMessage {
29
+ request: Request;
30
+ }
31
+ export interface RequestBodySentMessage {
32
+ request: Request;
33
+ }
34
+ export interface RequestHeadersMessage {
35
+ request: Request;
36
+ response: Response;
37
+ }
38
+ export interface RequestTrailersMessage {
39
+ request: Request;
40
+ trailers: Array<Buffer>;
41
+ }
42
+ export interface RequestErrorMessage {
43
+ request: Request;
44
+ error: Error;
45
+ }
46
+ export interface ClientSendHeadersMessage {
47
+ request: Request;
48
+ headers: string;
49
+ socket: Socket;
50
+ }
51
+ export interface ClientBeforeConnectMessage {
52
+ connectParams: ConnectParams;
53
+ connector: Connector;
54
+ }
55
+ export interface ClientConnectedMessage {
56
+ socket: Socket;
57
+ connectParams: ConnectParams;
58
+ connector: Connector;
59
+ }
60
+ export interface ClientConnectErrorMessage {
61
+ error: Error;
62
+ socket: Socket;
63
+ connectParams: ConnectParams;
64
+ connector: Connector;
65
+ }
66
+ }
@@ -47,6 +47,8 @@ declare namespace Dispatcher {
47
47
  body?: string | Buffer | Uint8Array | Readable | null | FormData;
48
48
  /** Default: `null` */
49
49
  headers?: IncomingHttpHeaders | string[] | null;
50
+ /** Query string params to be embedded in the request URL. Default: `null` */
51
+ query?: Record<string, any>;
50
52
  /** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */
51
53
  idempotent?: boolean;
52
54
  /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */
@@ -55,6 +57,8 @@ declare namespace Dispatcher {
55
57
  headersTimeout?: number | null;
56
58
  /** 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. */
57
59
  bodyTimeout?: number | null;
60
+ /** Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. Defaults to false */
61
+ throwOnError?: boolean;
58
62
  }
59
63
  export interface ConnectOptions {
60
64
  path: string;
package/types/fetch.d.ts CHANGED
@@ -101,19 +101,19 @@ type RequestDestination =
101
101
  | 'xslt'
102
102
 
103
103
  export interface RequestInit {
104
- readonly method?: string
105
- readonly keepalive?: boolean
106
- readonly headers?: HeadersInit
107
- readonly body?: BodyInit
108
- readonly redirect?: RequestRedirect
109
- readonly integrity?: string
110
- readonly signal?: AbortSignal
111
- readonly credentials?: RequestCredentials
112
- readonly mode?: RequestMode
113
- readonly referrer?: string
114
- readonly referrerPolicy?: ReferrerPolicy
115
- readonly window?: null
116
- readonly dispatcher?: Dispatcher
104
+ method?: string
105
+ keepalive?: boolean
106
+ headers?: HeadersInit
107
+ body?: BodyInit
108
+ redirect?: RequestRedirect
109
+ integrity?: string
110
+ signal?: AbortSignal
111
+ credentials?: RequestCredentials
112
+ mode?: RequestMode
113
+ referrer?: string
114
+ referrerPolicy?: ReferrerPolicy
115
+ window?: null
116
+ dispatcher?: Dispatcher
117
117
  }
118
118
 
119
119
  export type ReferrerPolicy =
@@ -199,5 +199,6 @@ export declare class Response implements BodyMixin {
199
199
  readonly clone: () => Response
200
200
 
201
201
  static error (): Response
202
+ static json(data: any, init?: ResponseInit): Response
202
203
  static redirect (url: string | URL, status: ResponseRedirectStatus): Response
203
204
  }