undici 7.0.0-alpha.3 → 7.0.0-alpha.4

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.
@@ -11,7 +11,7 @@ const { File: NodeFile } = require('node:buffer')
11
11
  const File = globalThis.File ?? NodeFile
12
12
 
13
13
  const formDataNameBuffer = Buffer.from('form-data; name="')
14
- const filenameBuffer = Buffer.from('; filename')
14
+ const filenameBuffer = Buffer.from('filename')
15
15
  const dd = Buffer.from('--')
16
16
  const ddcrlf = Buffer.from('--\r\n')
17
17
 
@@ -75,7 +75,7 @@ function multipartFormDataParser (input, mimeType) {
75
75
  // Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
76
76
  // parameters["boundary"].
77
77
  if (boundaryString === undefined) {
78
- return 'failure'
78
+ throw parsingError('missing boundary in content-type header')
79
79
  }
80
80
 
81
81
  const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
@@ -111,7 +111,7 @@ function multipartFormDataParser (input, mimeType) {
111
111
  if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
112
112
  position.position += boundary.length
113
113
  } else {
114
- return 'failure'
114
+ throw parsingError('expected a value starting with -- and the boundary')
115
115
  }
116
116
 
117
117
  // 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
@@ -127,7 +127,7 @@ function multipartFormDataParser (input, mimeType) {
127
127
  // 5.3. If position does not point to a sequence of bytes starting with 0x0D
128
128
  // 0x0A (CR LF), return failure.
129
129
  if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
130
- return 'failure'
130
+ throw parsingError('expected CRLF')
131
131
  }
132
132
 
133
133
  // 5.4. Advance position by 2. (This skips past the newline.)
@@ -138,10 +138,6 @@ function multipartFormDataParser (input, mimeType) {
138
138
  // is not failure. Otherwise, return failure.
139
139
  const result = parseMultipartFormDataHeaders(input, position)
140
140
 
141
- if (result === 'failure') {
142
- return 'failure'
143
- }
144
-
145
141
  let { name, filename, contentType, encoding } = result
146
142
 
147
143
  // 5.6. Advance position by 2. (This skips past the empty line that marks
@@ -157,7 +153,7 @@ function multipartFormDataParser (input, mimeType) {
157
153
  const boundaryIndex = input.indexOf(boundary.subarray(2), position.position)
158
154
 
159
155
  if (boundaryIndex === -1) {
160
- return 'failure'
156
+ throw parsingError('expected boundary after body')
161
157
  }
162
158
 
163
159
  body = input.subarray(position.position, boundaryIndex - 4)
@@ -174,7 +170,7 @@ function multipartFormDataParser (input, mimeType) {
174
170
  // 5.9. If position does not point to a sequence of bytes starting with
175
171
  // 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
176
172
  if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
177
- return 'failure'
173
+ throw parsingError('expected CRLF')
178
174
  } else {
179
175
  position.position += 2
180
176
  }
@@ -230,7 +226,7 @@ function parseMultipartFormDataHeaders (input, position) {
230
226
  if (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
231
227
  // 2.1.1. If name is null, return failure.
232
228
  if (name === null) {
233
- return 'failure'
229
+ throw parsingError('header name is null')
234
230
  }
235
231
 
236
232
  // 2.1.2. Return name, filename and contentType.
@@ -250,12 +246,12 @@ function parseMultipartFormDataHeaders (input, position) {
250
246
 
251
247
  // 2.4. If header name does not match the field-name token production, return failure.
252
248
  if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) {
253
- return 'failure'
249
+ throw parsingError('header name does not match the field-name token production')
254
250
  }
255
251
 
256
252
  // 2.5. If the byte at position is not 0x3A (:), return failure.
257
253
  if (input[position.position] !== 0x3a) {
258
- return 'failure'
254
+ throw parsingError('expected :')
259
255
  }
260
256
 
261
257
  // 2.6. Advance position by 1.
@@ -278,7 +274,7 @@ function parseMultipartFormDataHeaders (input, position) {
278
274
  // 2. If position does not point to a sequence of bytes starting with
279
275
  // `form-data; name="`, return failure.
280
276
  if (!bufferStartsWith(input, formDataNameBuffer, position)) {
281
- return 'failure'
277
+ throw parsingError('expected form-data; name=" for content-disposition header')
282
278
  }
283
279
 
284
280
  // 3. Advance position so it points at the byte after the next 0x22 (")
@@ -290,34 +286,61 @@ function parseMultipartFormDataHeaders (input, position) {
290
286
  // failure.
291
287
  name = parseMultipartFormDataName(input, position)
292
288
 
293
- if (name === null) {
294
- return 'failure'
295
- }
296
-
297
289
  // 5. If position points to a sequence of bytes starting with `; filename="`:
298
- if (bufferStartsWith(input, filenameBuffer, position)) {
299
- // Note: undici also handles filename*
300
- let check = position.position + filenameBuffer.length
301
-
302
- if (input[check] === 0x2a) {
303
- position.position += 1
304
- check += 1
305
- }
306
-
307
- if (input[check] !== 0x3d || input[check + 1] !== 0x22) { // ="
308
- return 'failure'
309
- }
310
-
311
- // 1. Advance position so it points at the byte after the next 0x22 (") byte
312
- // (the one in the sequence of bytes matched above).
313
- position.position += 12
314
-
315
- // 2. Set filename to the result of parsing a multipart/form-data name given
316
- // input and position, if the result is not failure. Otherwise, return failure.
317
- filename = parseMultipartFormDataName(input, position)
318
-
319
- if (filename === null) {
320
- return 'failure'
290
+ if (input[position.position] === 0x3b /* ; */ && input[position.position + 1] === 0x20 /* ' ' */) {
291
+ const at = { position: position.position + 2 }
292
+
293
+ if (bufferStartsWith(input, filenameBuffer, at)) {
294
+ if (input[at.position + 8] === 0x2a /* '*' */) {
295
+ at.position += 10 // skip past filename*=
296
+
297
+ // Remove leading http tab and spaces. See RFC for examples.
298
+ // https://datatracker.ietf.org/doc/html/rfc6266#section-5
299
+ collectASequenceOfBytes(
300
+ (char) => char === 0x20 || char === 0x09,
301
+ input,
302
+ at
303
+ )
304
+
305
+ const headerValue = collectASequenceOfBytes(
306
+ (char) => char !== 0x20 && char !== 0x0d && char !== 0x0a, // ' ' or CRLF
307
+ input,
308
+ at
309
+ )
310
+
311
+ if (
312
+ (headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
313
+ (headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
314
+ (headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
315
+ headerValue[3] !== 0x2d || // -
316
+ headerValue[4] !== 0x38 // 8
317
+ ) {
318
+ throw parsingError('unknown encoding, expected utf-8\'\'')
319
+ }
320
+
321
+ // skip utf-8''
322
+ filename = decodeURIComponent(new TextDecoder().decode(headerValue.subarray(7)))
323
+
324
+ position.position = at.position
325
+ } else {
326
+ // 1. Advance position so it points at the byte after the next 0x22 (") byte
327
+ // (the one in the sequence of bytes matched above).
328
+ position.position += 11
329
+
330
+ // Remove leading http tab and spaces. See RFC for examples.
331
+ // https://datatracker.ietf.org/doc/html/rfc6266#section-5
332
+ collectASequenceOfBytes(
333
+ (char) => char === 0x20 || char === 0x09,
334
+ input,
335
+ position
336
+ )
337
+
338
+ position.position++ // skip past " after removing whitespace
339
+
340
+ // 2. Set filename to the result of parsing a multipart/form-data name given
341
+ // input and position, if the result is not failure. Otherwise, return failure.
342
+ filename = parseMultipartFormDataName(input, position)
343
+ }
321
344
  }
322
345
  }
323
346
 
@@ -367,7 +390,7 @@ function parseMultipartFormDataHeaders (input, position) {
367
390
  // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
368
391
  // (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
369
392
  if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
370
- return 'failure'
393
+ throw parsingError('expected CRLF')
371
394
  } else {
372
395
  position.position += 2
373
396
  }
@@ -393,7 +416,7 @@ function parseMultipartFormDataName (input, position) {
393
416
 
394
417
  // 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
395
418
  if (input[position.position] !== 0x22) {
396
- return null // name could be 'failure'
419
+ throw parsingError('expected "')
397
420
  } else {
398
421
  position.position++
399
422
  }
@@ -468,6 +491,10 @@ function bufferStartsWith (buffer, start, position) {
468
491
  return true
469
492
  }
470
493
 
494
+ function parsingError (cause) {
495
+ return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
496
+ }
497
+
471
498
  module.exports = {
472
499
  multipartFormDataParser,
473
500
  validateBoundary
@@ -451,7 +451,7 @@ class Headers {
451
451
 
452
452
  // 2. If init is given, then fill this with init.
453
453
  if (init !== undefined) {
454
- init = webidl.converters.HeadersInit(init, 'Headers contructor', 'init')
454
+ init = webidl.converters.HeadersInit(init, 'Headers constructor', 'init')
455
455
  fill(this, init)
456
456
  }
457
457
  }
@@ -1943,8 +1943,10 @@ async function httpNetworkFetch (
1943
1943
  // 19. Run these steps in parallel:
1944
1944
 
1945
1945
  // 1. Run these steps, but abort when fetchParams is canceled:
1946
- fetchParams.controller.onAborted = onAborted
1947
- fetchParams.controller.on('terminated', onAborted)
1946
+ if (!fetchParams.controller.resume) {
1947
+ fetchParams.controller.on('terminated', onAborted)
1948
+ }
1949
+
1948
1950
  fetchParams.controller.resume = async () => {
1949
1951
  // 1. While true
1950
1952
  while (true) {
@@ -2205,10 +2207,6 @@ async function httpNetworkFetch (
2205
2207
  fetchParams.controller.off('terminated', this.abort)
2206
2208
  }
2207
2209
 
2208
- if (fetchParams.controller.onAborted) {
2209
- fetchParams.controller.off('terminated', fetchParams.controller.onAborted)
2210
- }
2211
-
2212
2210
  fetchParams.controller.ended = true
2213
2211
 
2214
2212
  this.body.push(null)
@@ -345,12 +345,14 @@ webidl.recordConverter = function (keyConverter, valueConverter) {
345
345
  const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]
346
346
 
347
347
  for (const key of keys) {
348
+ const keyName = webidl.util.Stringify(key)
349
+
348
350
  // 1. Let typedKey be key converted to an IDL value of type K.
349
- const typedKey = keyConverter(key, prefix, argument)
351
+ const typedKey = keyConverter(key, prefix, `Key ${keyName} in ${argument}`)
350
352
 
351
353
  // 2. Let value be ? Get(O, key).
352
354
  // 3. Let typedValue be value converted to an IDL value of type V.
353
- const typedValue = valueConverter(O[key], prefix, argument)
355
+ const typedValue = valueConverter(O[key], prefix, `${argument}[${keyName}]`)
354
356
 
355
357
  // 4. Set result[typedKey] to typedValue.
356
358
  result[typedKey] = typedValue
@@ -501,8 +503,14 @@ webidl.converters.DOMString = function (V, prefix, argument, opts) {
501
503
  // https://webidl.spec.whatwg.org/#es-ByteString
502
504
  webidl.converters.ByteString = function (V, prefix, argument) {
503
505
  // 1. Let x be ? ToString(V).
504
- // Note: DOMString converter perform ? ToString(V)
505
- const x = webidl.converters.DOMString(V, prefix, argument)
506
+ if (typeof V === 'symbol') {
507
+ throw webidl.errors.exception({
508
+ header: prefix,
509
+ message: `${argument} is a symbol, which cannot be converted to a ByteString.`
510
+ })
511
+ }
512
+
513
+ const x = String(V)
506
514
 
507
515
  // 2. If the value of any element of x is greater than
508
516
  // 255, then throw a TypeError.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.0.0-alpha.3",
3
+ "version": "7.0.0-alpha.4",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -63,7 +63,6 @@
63
63
  "types": "index.d.ts",
64
64
  "scripts": {
65
65
  "build:node": "esbuild index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names && node scripts/strip-comments.js",
66
- "prebuild:wasm": "node build/wasm.js --prebuild",
67
66
  "build:wasm": "node build/wasm.js --docker",
68
67
  "generate-pem": "node scripts/generate-pem.js",
69
68
  "lint": "eslint --cache",
@@ -108,7 +107,7 @@
108
107
  "devDependencies": {
109
108
  "@fastify/busboy": "3.0.0",
110
109
  "@matteo.collina/tspl": "^0.1.1",
111
- "@sinonjs/fake-timers": "^11.1.0",
110
+ "@sinonjs/fake-timers": "^12.0.0",
112
111
  "@types/node": "^18.19.50",
113
112
  "abort-controller": "^3.0.0",
114
113
  "borp": "^0.18.0",
@@ -1,5 +1,4 @@
1
1
  import { Readable, Writable } from 'node:stream'
2
- import Dispatcher from './dispatcher'
3
2
 
4
3
  export default CacheHandler
5
4
 
@@ -19,6 +18,21 @@ declare namespace CacheHandler {
19
18
  methods?: CacheMethods[]
20
19
  }
21
20
 
21
+ export interface CacheKey {
22
+ origin: string
23
+ method: string
24
+ path: string
25
+ headers?: Record<string, string | string[]>
26
+ }
27
+
28
+ export interface DeleteByUri {
29
+ origin: string
30
+ method: string
31
+ path: string
32
+ }
33
+
34
+ type GetResult = CachedResponse & { body: null | Readable | Iterable<Buffer> | Buffer | Iterable<string> | string }
35
+
22
36
  /**
23
37
  * Underlying storage provider for cached responses
24
38
  */
@@ -26,72 +40,62 @@ declare namespace CacheHandler {
26
40
  /**
27
41
  * Whether or not the cache is full and can not store any more responses
28
42
  */
29
- get isFull(): boolean
43
+ get isFull(): boolean | undefined
30
44
 
31
- createReadStream(req: Dispatcher.RequestOptions): CacheStoreReadable | Promise<CacheStoreReadable | undefined> | undefined
45
+ get(key: CacheKey): GetResult | Promise<GetResult | undefined> | undefined
32
46
 
33
- createWriteStream(req: Dispatcher.RequestOptions, value: Omit<CacheStoreValue, 'rawTrailers'>): CacheStoreWriteable | undefined
47
+ createWriteStream(key: CacheKey, value: CachedResponse): Writable | undefined
34
48
 
35
- /**
36
- * Delete all of the cached responses from a certain origin (host)
37
- */
38
- deleteByOrigin(origin: string): void | Promise<void>
49
+ delete(key: CacheKey): void | Promise<void>
39
50
  }
40
51
 
41
- export interface CacheStoreReadable extends Readable {
42
- get value(): CacheStoreValue
43
- }
44
-
45
- export interface CacheStoreWriteable extends Writable {
46
- set rawTrailers(rawTrailers: string[] | undefined)
47
- }
48
-
49
- export interface CacheStoreValue {
52
+ export interface CachedResponse {
50
53
  statusCode: number;
51
54
  statusMessage: string;
52
- rawHeaders: (Buffer | Buffer[])[];
53
- rawTrailers?: string[];
55
+ rawHeaders: Buffer[];
54
56
  /**
55
57
  * Headers defined by the Vary header and their respective values for
56
58
  * later comparison
57
59
  */
58
- vary?: Record<string, string>;
60
+ vary?: Record<string, string | string[]>
59
61
  /**
60
62
  * Time in millis that this value was cached
61
63
  */
62
- cachedAt: number;
64
+ cachedAt: number
63
65
  /**
64
66
  * Time in millis that this value is considered stale
65
67
  */
66
- staleAt: number;
68
+ staleAt: number
67
69
  /**
68
70
  * Time in millis that this value is to be deleted from the cache. This is
69
71
  * either the same as staleAt or the `max-stale` caching directive.
70
72
  */
71
- deleteAt: number;
73
+ deleteAt: number
72
74
  }
73
75
 
74
76
  export interface MemoryCacheStoreOpts {
75
77
  /**
76
- * @default Infinity
77
- */
78
- maxEntries?: number
78
+ * @default Infinity
79
+ */
80
+ maxCount?: number
81
+
79
82
  /**
80
- * @default Infinity
81
- */
83
+ * @default Infinity
84
+ */
82
85
  maxEntrySize?: number
86
+
83
87
  errorCallback?: (err: Error) => void
84
88
  }
85
89
 
86
90
  export class MemoryCacheStore implements CacheStore {
87
91
  constructor (opts?: MemoryCacheStoreOpts)
88
92
 
89
- get isFull (): boolean
93
+ get isFull (): boolean | undefined
90
94
 
91
- createReadStream (req: Dispatcher.RequestOptions): CacheStoreReadable | undefined
95
+ get (key: CacheKey): GetResult | Promise<GetResult | undefined> | undefined
92
96
 
93
- createWriteStream (req: Dispatcher.RequestOptions, value: CacheStoreValue): CacheStoreWriteable
97
+ createWriteStream (key: CacheKey, value: CachedResponse): Writable | undefined
94
98
 
95
- deleteByOrigin (origin: string): void
99
+ delete (key: CacheKey): void | Promise<void>
96
100
  }
97
101
  }
@@ -26,3 +26,5 @@ export function getCookies (headers: Headers): Record<string, string>
26
26
  export function getSetCookies (headers: Headers): Cookie[]
27
27
 
28
28
  export function setCookie (headers: Headers, cookie: Cookie): void
29
+
30
+ export function parseCookie (cookie: string): Cookie | null
@@ -108,7 +108,7 @@ declare namespace Dispatcher {
108
108
  query?: Record<string, any>;
109
109
  /** 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`. */
110
110
  idempotent?: boolean;
111
- /** 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. */
111
+ /** 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. Defaults to `method !== 'HEAD'`. */
112
112
  blocking?: boolean;
113
113
  /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */
114
114
  upgrade?: boolean | string | null;
package/types/index.d.ts CHANGED
@@ -40,7 +40,6 @@ declare namespace Undici {
40
40
  const RedirectHandler: typeof import ('./handlers').RedirectHandler
41
41
  const DecoratorHandler: typeof import ('./handlers').DecoratorHandler
42
42
  const RetryHandler: typeof import ('./retry-handler').default
43
- const createRedirectInterceptor: typeof import ('./interceptors').default.createRedirectInterceptor
44
43
  const BalancedPool: typeof import('./balanced-pool').default
45
44
  const Client: typeof import('./client').default
46
45
  const buildConnector: typeof import('./connector').default
@@ -25,7 +25,6 @@ declare namespace Interceptors {
25
25
  affinity?: 4 | 6
26
26
  }
27
27
 
28
- export function createRedirectInterceptor (opts: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
29
28
  export function dump (opts?: DumpInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
30
29
  export function retry (opts?: RetryInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
31
30
  export function redirect (opts?: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor