undici 6.10.2 → 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
  ```
@@ -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
 
@@ -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,7 +405,19 @@ 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
  }
397
423
 
@@ -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
  }))
@@ -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 })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.10.2",
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;