rekwest 7.2.3 → 7.2.5

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/dist/utils.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.stripHeaders = exports.snoop = exports.sameOrigin = exports.normalizeHeaders = exports.normalize = exports.isReadableStream = exports.isPipeStream = exports.isLikelyH2cPrefaceError = exports.isFileLike = exports.dispatch = exports.deepMerge = exports.cloneWith = exports.brandCheck = exports.augment = exports.addSearchParams = void 0;
6
+ exports.stripHeaders = exports.snoop = exports.sameOrigin = exports.normalizeHeaders = exports.normalize = exports.isReadableStream = exports.isPipeStream = exports.isLikelyH2cPrefaceError = exports.isBlobLike = exports.dispatch = exports.deepMerge = exports.cloneWith = exports.brandCheck = exports.augment = exports.addSearchParams = void 0;
7
7
  exports.tap = tap;
8
8
  exports.unwind = exports.toCamelCase = void 0;
9
9
  var _nodeBuffer = require("node:buffer");
@@ -31,9 +31,7 @@ const addSearchParams = (url, params = {}) => {
31
31
  };
32
32
  exports.addSearchParams = addSearchParams;
33
33
  const augment = (res, headers, options) => {
34
- const {
35
- h2
36
- } = options;
34
+ const h2 = /\bh2c?\b/i.test(res.session?.alpnProtocol);
37
35
  if (h2) {
38
36
  Reflect.defineProperty(res, 'headers', {
39
37
  enumerable: true,
@@ -92,67 +90,74 @@ const dispatch = (req, {
92
90
  body
93
91
  }) => {
94
92
  if ((0, _nodeStream.isReadable)(body)) {
93
+ body.once('error', err => req.session && req.emit('error', err) && req.destroy() || req.destroy(err));
95
94
  body.pipe(req);
96
95
  } else {
97
96
  req.end(body);
98
97
  }
99
98
  };
100
99
  exports.dispatch = dispatch;
101
- const isFileLike = val => {
102
- return [_nodeBuffer.Blob, _nodeBuffer.File].some(it => val instanceof it);
103
- };
104
- exports.isFileLike = isFileLike;
105
- const isLikelyH2cPrefaceError = err => {
106
- return err.code === 'HPE_INVALID_CONSTANT';
107
- };
100
+ const isBlobLike = val => val instanceof _nodeBuffer.Blob;
101
+ exports.isBlobLike = isBlobLike;
102
+ const isLikelyH2cPrefaceError = err => err.code === 'HPE_INVALID_CONSTANT';
108
103
  exports.isLikelyH2cPrefaceError = isLikelyH2cPrefaceError;
109
- const isPipeStream = val => {
110
- return val instanceof _nodeStream.Readable;
111
- };
104
+ const isPipeStream = val => val instanceof _nodeStream.Readable;
112
105
  exports.isPipeStream = isPipeStream;
113
- const isReadableStream = val => {
114
- return val instanceof ReadableStream;
115
- };
106
+ const isReadableStream = val => val instanceof ReadableStream;
116
107
  exports.isReadableStream = isReadableStream;
117
108
  const normalize = (url, options = {}) => {
118
109
  if (!options.redirected) {
119
110
  options = cloneWith(_config.default.defaults, options);
120
111
  }
121
- if (options.trimTrailingSlashes) {
122
- url = `${url}`.replace(/(?<!:)\/+/g, '/');
123
- }
124
- if (options.stripTrailingSlash) {
125
- url = `${url}`.replace(/\/$|\/(?=#)|\/(?=\?)/g, '');
126
- }
127
112
  return Object.assign(options, {
128
113
  headers: normalizeHeaders(options.headers),
129
- method: options.method.toUpperCase(),
130
- url: addSearchParams(new URL(url, options.baseURL), options.params)
114
+ method: options.method?.toUpperCase(),
115
+ url: addSearchParams(normalizeUrl(new URL(url, options.baseURL), options), options.params)
131
116
  });
132
117
  };
133
118
  exports.normalize = normalize;
134
119
  const normalizeHeaders = (headers = {}) => {
135
120
  const acc = {};
136
- for (const [key, val] of Object.entries(headers)) {
137
- const name = key.toLowerCase();
121
+ for (let [key, val] of Object.entries(headers)) {
122
+ key = key.toLowerCase();
138
123
  acc[key] = val;
139
124
  if (key === HTTP2_HEADER_ACCEPT_ENCODING && !_config.isZstdSupported) {
140
- const modified = val.replace(/\s?zstd,?/gi, '').trim();
141
- if (modified) {
142
- acc[key] = modified;
125
+ val = val.replace(/\s?zstd,?/gi, '').trim();
126
+ if (val) {
127
+ acc[key] = val;
143
128
  } else {
144
- Reflect.deleteProperty(acc, name);
129
+ Reflect.deleteProperty(acc, key);
145
130
  }
146
131
  }
147
132
  }
148
133
  return acc;
149
134
  };
150
135
  exports.normalizeHeaders = normalizeHeaders;
151
- const sameOrigin = (a, b) => a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
136
+ function normalizeUrl(url, {
137
+ trimTrailingSlashes,
138
+ stripTrailingSlash
139
+ } = {}) {
140
+ if (trimTrailingSlashes) {
141
+ url.pathname = url.pathname.replace(/\/{2,}/g, '/');
142
+ }
143
+ if (stripTrailingSlash && url.pathname !== '/') {
144
+ url.pathname = url.pathname.replace(/\/$/, '');
145
+ }
146
+ return url;
147
+ }
148
+ const sameOrigin = (a, b) => a.origin === b.origin;
152
149
  exports.sameOrigin = sameOrigin;
153
- const snoop = (client, req, options) => {
150
+ const snoop = (client, req, options, {
151
+ reject
152
+ } = {
153
+ reject: () => void 0
154
+ }) => {
155
+ req.once('aborted', reject);
154
156
  req.once('close', () => client?.close());
155
157
  req.once('end', () => client?.close());
158
+ req.once('error', reject);
159
+ req.once('frameError', reject);
160
+ req.once('goaway', reject);
156
161
  req.once('timeout', () => req.destroy(new _errors.TimeoutError(`Timed out after ${options.timeout} ms`)));
157
162
  req.once('trailers', trailers => {
158
163
  Reflect.defineProperty(req, 'trailers', {
@@ -162,9 +167,9 @@ const snoop = (client, req, options) => {
162
167
  });
163
168
  };
164
169
  exports.snoop = snoop;
165
- const stripHeaders = (headers = {}, names = []) => {
166
- names = new Set(names);
167
- return Object.fromEntries(Object.entries(headers).filter(([key]) => !names.has(key.toLowerCase())));
170
+ const stripHeaders = (headers = {}, keys = []) => {
171
+ keys = new Set(keys);
172
+ return Object.fromEntries(Object.entries(headers).filter(([key]) => !keys.has(key)));
168
173
  };
169
174
  exports.stripHeaders = stripHeaders;
170
175
  async function* tap(val) {
@@ -172,8 +177,6 @@ async function* tap(val) {
172
177
  yield* val;
173
178
  } else if (val.stream) {
174
179
  yield* val.stream();
175
- } else {
176
- yield await val.arrayBuffer();
177
180
  }
178
181
  }
179
182
  const toCamelCase = str => str?.toLowerCase().replace(/\p{Punctuation}.|\p{White_Space}./gu, val => val.replace(/\p{Punctuation}+|\p{White_Space}+/gu, '').toUpperCase());
package/package.json CHANGED
@@ -12,9 +12,10 @@
12
12
  "@babel/cli": "^7.28.6",
13
13
  "@babel/core": "^7.29.0",
14
14
  "@babel/preset-env": "^7.29.0",
15
+ "@eslint/markdown": "^7.5.1",
15
16
  "c8": "^10.1.3",
16
- "eslint": "^10.0.0",
17
- "eslint-config-ultra-refined": "^4.0.1",
17
+ "eslint": "^10.0.1",
18
+ "eslint-config-ultra-refined": "^4.1.2",
18
19
  "mocha": "^11.7.5"
19
20
  },
20
21
  "engines": {
@@ -60,16 +61,16 @@
60
61
  "url": "git+https://github.com/bricss/rekwest.git"
61
62
  },
62
63
  "scripts": {
63
- "build": "rm -rf dist && npx babel src --out-dir dist --out-file-extension .cjs",
64
+ "build": "rm -rf dist && npx babel src --out-dir dist --out-file-extension .cjs && sh misc.sh",
64
65
  "cert:gen": "openssl req -days 365 -keyout localhost.key -newkey ec -nodes -pkeyopt ec_paramgen_curve:prime256v1 -subj //SKIP=1/CN=localhost -out localhost.cert -x509",
65
66
  "cert:ken": "openssl x509 -in localhost.cert -noout -text",
66
67
  "lint": "eslint --concurrency=auto",
67
- "prepack": "npm run build && sh misc.sh && npm run lint",
68
+ "prepack": "npm run build && npm run lint",
68
69
  "pretest": "rm -rf coverage && npm run cert:gen",
69
70
  "test": "mocha",
70
71
  "test:bail": "mocha --bail",
71
72
  "test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
72
73
  },
73
74
  "type": "module",
74
- "version": "7.2.3"
75
+ "version": "7.2.5"
75
76
  }
package/src/ackn.js CHANGED
@@ -23,11 +23,11 @@ export const ackn = (options) => new Promise((resolve, reject) => {
23
23
  createConnection() {
24
24
  return socket;
25
25
  },
26
- h2: /h2c?/i.test(alpnProtocol),
26
+ h2: /\bh2\b/i.test(alpnProtocol),
27
27
  protocol: url.protocol,
28
28
  });
29
29
  });
30
30
 
31
- socket.on('error', reject);
32
- socket.on('timeout', reject);
31
+ socket.once('error', reject);
32
+ socket.once('timeout', reject);
33
33
  });
package/src/cookies.js CHANGED
@@ -3,7 +3,13 @@ import {
3
3
  toCamelCase,
4
4
  } from './utils.js';
5
5
 
6
- const lifetimeCap = 3456e7; // 400 days
6
+ export const cookieRex = /^[\w-]+=(?:"[^"]*"|[^\p{Control};]*)(?:;\s*(?:[\w-]+=(?:"[^"]*"|[^\p{Control};]*)|[\w-]+))*$/u;
7
+ export const cookiePairRex = /(?:[^;"\s]+="[^"]*"|[^;]+)(?=;|$)/g;
8
+ export const illegalCookieChars = /\p{Control}/u;
9
+ export const isValidCookie = (str) => str?.constructor === String && cookieRex.test(str);
10
+ export const maxCookieLifetimeCap = 3456e7; // 400 days
11
+ export const maxCookieSize = 4096;
12
+ export const splitCookie = (str) => str.match(cookiePairRex).map((str) => str.trim());
7
13
 
8
14
  export class Cookies extends URLSearchParams {
9
15
 
@@ -27,49 +33,60 @@ export class Cookies extends URLSearchParams {
27
33
  }
28
34
 
29
35
  constructor(input, { cookiesTTL } = { cookiesTTL: false }) {
30
- if (Array.isArray(input) && input.every((it) => !Array.isArray(it))) {
31
- input = input.map((it) => {
32
- if (!cookiesTTL) {
33
- return [it.split(';')[0].trim()];
34
- }
35
-
36
- const [cookie, ...attrs] = it.split(';').map((it) => it.trim());
37
- const ttl = {};
38
-
39
- for (const attr of attrs) {
40
- if (/(?:expires|max-age)=/i.test(attr)) {
41
- const [key, val] = attr.toLowerCase().split('=');
42
- const ms = Number.isFinite(Number.parseInt(val, 10)) ? val * 1e3 : Date.parse(val) - Date.now();
36
+ if (isValidCookie(input)) {
37
+ input = splitCookie(input);
38
+ }
43
39
 
44
- ttl[toCamelCase(key)] = Math.min(ms, lifetimeCap);
40
+ const ttlMap = new Map();
41
+
42
+ if (Array.isArray(input)) {
43
+ if (input.every((it) => isValidCookie(it))) {
44
+ input = input.filter((it) => !illegalCookieChars.test(it) && it.length <= maxCookieSize);
45
+ input = input.map(splitCookie).map(([cookie, ...attrs]) => {
46
+ try {
47
+ cookie = cookie.split('=').map((it) => decodeURIComponent(it.trim()));
48
+
49
+ return cookie;
50
+ } finally {
51
+ if (cookiesTTL) {
52
+ for (const attr of attrs) {
53
+ if (/(?:expires|max-age)=/i.test(attr)) {
54
+ const [key, val] = attr.toLowerCase().split('=');
55
+ let interval = val * 1e3 || Date.parse(val) - Date.now();
56
+
57
+ if (interval < 0 || Number.isNaN(interval)) {
58
+ interval = 0;
59
+ }
60
+
61
+ ttlMap.set(
62
+ cookie[0],
63
+ { [toCamelCase(key.trim())]: Math.min(interval, maxCookieLifetimeCap) },
64
+ );
65
+ }
66
+ }
67
+ }
45
68
  }
46
- }
47
-
48
- return [
49
- cookie.replace(/\u0022/g, ''),
50
- Object.keys(ttl).length ? ttl : null,
51
- ];
52
- });
69
+ });
70
+ }
53
71
  }
54
72
 
55
- super(Array.isArray(input) ? input.map((it) => it[0]).join('&') : input);
73
+ super(input);
56
74
 
57
- if (Array.isArray(input) && cookiesTTL) {
58
- for (const [cookie, ttl] of input.filter((it) => it[1])) {
59
- const key = cookie.split('=')[0];
75
+ if (ttlMap.size) {
76
+ for (const [key, attrs] of ttlMap) {
60
77
 
61
78
  if (this.#chronometry.has(key)) {
62
79
  clearTimeout(this.#chronometry.get(key));
63
80
  this.#chronometry.delete(key);
64
81
  }
65
82
 
66
- const { expires, maxAge } = ttl;
83
+ const { expires, maxAge } = attrs;
67
84
 
68
- for (const ms of [
85
+ for (const interval of [
69
86
  maxAge,
70
87
  expires,
71
88
  ]) {
72
- if (!Number.isInteger(ms)) {
89
+ if (!Number.isInteger(interval)) {
73
90
  continue;
74
91
  }
75
92
 
@@ -81,7 +98,7 @@ export class Cookies extends URLSearchParams {
81
98
  ctx.#chronometry.delete(key);
82
99
  ctx.delete(key);
83
100
  }
84
- }, Math.max(ms, 0));
101
+ }, Math.max(interval, 0));
85
102
 
86
103
  this.constructor.#register(this, tid);
87
104
  this.#chronometry.set(key, tid);
package/src/formdata.js CHANGED
@@ -7,7 +7,9 @@ import {
7
7
  } from './mediatypes.js';
8
8
  import {
9
9
  brandCheck,
10
- isFileLike,
10
+ isBlobLike,
11
+ isPipeStream,
12
+ isReadableStream,
11
13
  tap,
12
14
  } from './utils.js';
13
15
 
@@ -19,58 +21,31 @@ const {
19
21
 
20
22
  export class FormData {
21
23
 
22
- static actuate(fd) {
23
- const boundary = randomBytes(24).toString('hex');
24
- const contentType = `${ MULTIPART_FORM_DATA }; boundary=${ boundary }`;
25
- const prefix = `--${ boundary }${ CRLF }${ HTTP2_HEADER_CONTENT_DISPOSITION }: form-data`;
26
-
27
- const escape = (str) => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22');
28
- const redress = (str) => str.replace(/\r?\n|\r/g, CRLF);
29
-
30
- return {
31
- contentType,
32
- async* [Symbol.asyncIterator]() {
33
- const encoder = new TextEncoder();
34
-
35
- for (const [name, val] of fd) {
36
- if (val.constructor === String) {
37
- yield encoder.encode(`${ prefix }; name="${
38
- escape(redress(name))
39
- }"${ CRLF.repeat(2) }${ redress(val) }${ CRLF }`);
40
- } else {
41
- yield encoder.encode(`${ prefix }; name="${
42
- escape(redress(name))
43
- }"${ val.name ? `; filename="${ escape(val.name) }"` : '' }${ CRLF }${
44
- HTTP2_HEADER_CONTENT_TYPE
45
- }: ${
46
- val.type || APPLICATION_OCTET_STREAM
47
- }${ CRLF.repeat(2) }`);
48
- yield* tap(val);
49
- yield new Uint8Array([
50
- 13,
51
- 10,
52
- ]);
53
- }
54
- }
55
-
56
- yield encoder.encode(`--${ boundary }--`);
57
- },
58
- };
59
- }
24
+ static #ensureArgs(args, expect, method) {
25
+ if (args.length < expect) {
26
+ throw new TypeError(`Failed to execute '${ method }' on '${
27
+ this[Symbol.toStringTag]
28
+ }': ${ expect } arguments required, but only ${ args.length } present`);
29
+ }
60
30
 
61
- static alike(val) {
62
- return FormData.name === val?.[Symbol.toStringTag];
31
+ if (method === 'forEach') {
32
+ if (args[0]?.constructor !== Function) {
33
+ throw new TypeError(`Failed to execute '${ method }' on '${
34
+ this[Symbol.toStringTag]
35
+ }': parameter ${ expect } is not of type 'Function'`);
36
+ }
37
+ }
63
38
  }
64
39
 
65
- static #enfoldEntry(name, value, filename) {
40
+ static #formEntry(name, value, filename) {
66
41
  name = String(name).toWellFormed();
67
42
  filename &&= String(filename).toWellFormed();
68
43
 
69
- if (isFileLike(value)) {
70
- filename ??= value.name || 'blob';
44
+ if (isBlobLike(value)) {
45
+ filename ??= String(value.name ?? 'blob').toWellFormed();
71
46
  value = new File([value], filename, value);
72
- } else if (this.#ensureInstance(value)) {
73
- value.name = filename;
47
+ } else if (isPipeStream(value) || isReadableStream(value)) {
48
+ value.name = filename ?? 'blob';
74
49
  } else {
75
50
  value = String(value).toWellFormed();
76
51
  }
@@ -81,10 +56,6 @@ export class FormData {
81
56
  };
82
57
  }
83
58
 
84
- static #ensureInstance(val) {
85
- return isFileLike(val) || (Object(val) === val && Reflect.has(val, Symbol.asyncIterator));
86
- }
87
-
88
59
  #entries = [];
89
60
 
90
61
  get [Symbol.toStringTag]() {
@@ -98,13 +69,13 @@ export class FormData {
98
69
  throw new TypeError(`Failed to construct '${
99
70
  this[Symbol.toStringTag]
100
71
  }': The provided value cannot be converted to a sequence`);
101
- } else if (!input.every((it) => it.length === 2)) {
72
+ }
73
+
74
+ if (!input.every((it) => it.length === 2)) {
102
75
  throw new TypeError(`Failed to construct '${
103
76
  this[Symbol.toStringTag]
104
77
  }': Sequence initializer must only contain pair elements`);
105
78
  }
106
-
107
- input = Array.from(input);
108
79
  } else if (!Reflect.has(input, Symbol.iterator)) {
109
80
  input = Object.entries(input);
110
81
  }
@@ -115,42 +86,15 @@ export class FormData {
115
86
  }
116
87
  }
117
88
 
118
- #ensureArgs(args, expected, method) {
119
- if (args.length < expected) {
120
- throw new TypeError(`Failed to execute '${ method }' on '${
121
- this[Symbol.toStringTag]
122
- }': ${ expected } arguments required, but only ${ args.length } present`);
123
- }
124
-
125
- if ([
126
- 'append',
127
- 'set',
128
- ].includes(method)) {
129
- if (args.length === 3 && !this.constructor.#ensureInstance(args[1])) {
130
- throw new TypeError(`Failed to execute '${ method }' on '${
131
- this[Symbol.toStringTag]
132
- }': parameter ${ expected } is not of type 'Blob'`);
133
- }
134
- }
135
-
136
- if (method === 'forEach') {
137
- if (args[0]?.constructor !== Function) {
138
- throw new TypeError(`Failed to execute '${ method }' on '${
139
- this[Symbol.toStringTag]
140
- }': parameter ${ expected } is not of type 'Function'`);
141
- }
142
- }
143
- }
144
-
145
89
  append(...args) {
146
90
  brandCheck(this, FormData);
147
- this.#ensureArgs(args, 2, 'append');
148
- this.#entries.push(this.constructor.#enfoldEntry(...args));
91
+ this.constructor.#ensureArgs(args, 2, this.append.name);
92
+ this.#entries.push(this.constructor.#formEntry(...args));
149
93
  }
150
94
 
151
95
  delete(...args) {
152
96
  brandCheck(this, FormData);
153
- this.#ensureArgs(args, 1, 'delete');
97
+ this.constructor.#ensureArgs(args, 1, this.delete.name);
154
98
  const name = String(args[0]).toWellFormed();
155
99
 
156
100
  this.#entries = this.#entries.filter((it) => it.name !== name);
@@ -158,7 +102,7 @@ export class FormData {
158
102
 
159
103
  forEach(...args) {
160
104
  brandCheck(this, FormData);
161
- this.#ensureArgs(args, 1, 'forEach');
105
+ this.constructor.#ensureArgs(args, 1, this.forEach.name);
162
106
  const [callback, thisArg] = args;
163
107
 
164
108
  for (const entry of this) {
@@ -171,7 +115,7 @@ export class FormData {
171
115
 
172
116
  get(...args) {
173
117
  brandCheck(this, FormData);
174
- this.#ensureArgs(args, 1, 'get');
118
+ this.constructor.#ensureArgs(args, 1, this.get.name);
175
119
  const name = String(args[0]).toWellFormed();
176
120
 
177
121
  return this.#entries.find((it) => it.name === name)?.value ?? null;
@@ -179,7 +123,7 @@ export class FormData {
179
123
 
180
124
  getAll(...args) {
181
125
  brandCheck(this, FormData);
182
- this.#ensureArgs(args, 1, 'getAll');
126
+ this.constructor.#ensureArgs(args, 1, this.getAll.name);
183
127
  const name = String(args[0]).toWellFormed();
184
128
 
185
129
  return this.#entries.filter((it) => it.name === name).map((it) => it.value);
@@ -187,7 +131,7 @@ export class FormData {
187
131
 
188
132
  has(...args) {
189
133
  brandCheck(this, FormData);
190
- this.#ensureArgs(args, 1, 'has');
134
+ this.constructor.#ensureArgs(args, 1, this.has.name);
191
135
  const name = String(args[0]).toWellFormed();
192
136
 
193
137
  return !!this.#entries.find((it) => it.name === name);
@@ -195,8 +139,8 @@ export class FormData {
195
139
 
196
140
  set(...args) {
197
141
  brandCheck(this, FormData);
198
- this.#ensureArgs(args, 2, 'set');
199
- const entry = this.constructor.#enfoldEntry(...args);
142
+ this.constructor.#ensureArgs(args, 2, this.set.name);
143
+ const entry = this.constructor.#formEntry(...args);
200
144
  const idx = this.#entries.findIndex((it) => it.name === entry.name);
201
145
 
202
146
  if (idx !== -1) {
@@ -237,3 +181,50 @@ export class FormData {
237
181
  }
238
182
 
239
183
  }
184
+
185
+ export const fdToAsyncIterable = (fd) => {
186
+ const boundary = randomBytes(32).toString('hex');
187
+ const contentType = `${ MULTIPART_FORM_DATA }; boundary=${ boundary }`;
188
+ const prefix = `--${ boundary }${ CRLF }${ HTTP2_HEADER_CONTENT_DISPOSITION }: form-data`;
189
+
190
+ const escape = (str) => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22');
191
+ const normalize = (str) => str.replace(/\r?\n|\r/g, CRLF);
192
+
193
+ return {
194
+ contentType,
195
+ async* [Symbol.asyncIterator]() {
196
+ const encoder = new TextEncoder();
197
+
198
+ for (const [name, val] of fd) {
199
+ if (val.constructor === String) {
200
+ yield encoder.encode(`${ prefix }; name="${
201
+ escape(normalize(name))
202
+ }"${ CRLF.repeat(2) }${ normalize(val) }${ CRLF }`);
203
+ } else {
204
+ yield encoder.encode(`${ prefix }; name="${
205
+ escape(normalize(name))
206
+ }"${ val.name ? `; filename="${ escape(val.name) }"` : '' }${ CRLF }${
207
+ HTTP2_HEADER_CONTENT_TYPE
208
+ }: ${
209
+ val.type || APPLICATION_OCTET_STREAM
210
+ }${ CRLF.repeat(2) }`);
211
+ yield* tap(val);
212
+ yield new Uint8Array([
213
+ 13,
214
+ 10,
215
+ ]);
216
+ }
217
+ }
218
+
219
+ yield encoder.encode(`--${ boundary }--${ CRLF }`);
220
+ },
221
+ };
222
+ };
223
+
224
+ export const isFormData = (val) => FormData.name === val?.[Symbol.toStringTag];
225
+
226
+ export const parseFormData = (str) => {
227
+ const rex = /^-+[^\r\n]+\r?\ncontent-disposition:\s*form-data;\s*name="(?<name>[^"]+)"(?:;\s*filename="(?<filename>[^"]+)")?(?:\r?\n[^\r\n:]+:[^\r\n]*)*\r?\n\r?\n(?<content>.*?)(?=\r?\n-+[^\r\n]+)/gims;
228
+
229
+ return [...str.matchAll(rex)].map(({ groups }) => structuredClone(groups));
230
+ };
package/src/preflight.js CHANGED
@@ -64,7 +64,7 @@ export const preflight = (options) => {
64
64
  cookie &&= Cookies.jar.get(url.origin);
65
65
  }
66
66
 
67
- options.headers = {
67
+ headers = {
68
68
  ...cookie && { [HTTP2_HEADER_COOKIE]: cookie },
69
69
  ...headers,
70
70
  };
package/src/retries.js CHANGED
@@ -27,7 +27,7 @@ export const retries = (err, options) => {
27
27
 
28
28
  if (retry.retryAfter && err.headers?.[HTTP2_HEADER_RETRY_AFTER]) {
29
29
  interval = err.headers[HTTP2_HEADER_RETRY_AFTER];
30
- interval = Number.isFinite(Number.parseInt(interval, 10)) ? interval * 1e3 : new Date(interval) - Date.now();
30
+ interval = interval * 1e3 || Date.parse(interval) - Date.now();
31
31
  if (interval > retry.maxRetryAfter) {
32
32
  throw new RequestError(
33
33
  `Maximum '${ HTTP2_HEADER_RETRY_AFTER }' limit exceeded: ${ interval } ms`,
@@ -38,7 +38,7 @@ export const retries = (err, options) => {
38
38
  interval = new Function('interval', `return Math.ceil(${ retry.backoffStrategy });`)(interval);
39
39
  }
40
40
 
41
- if (interval < 0) {
41
+ if (interval < 0 || Number.isNaN(interval)) {
42
42
  interval = 0;
43
43
  }
44
44
 
package/src/transfer.js CHANGED
@@ -53,12 +53,8 @@ export const transfer = async (options) => {
53
53
  req = request(url, options);
54
54
  }
55
55
 
56
- snoop(client, req, options);
56
+ snoop(client, req, options, { reject });
57
57
 
58
- req.once('aborted', reject);
59
- req.once('error', reject);
60
- req.once('frameError', reject);
61
- req.once('goaway', reject);
62
58
  req.once('response', (res) => postflight(req, res, options, {
63
59
  reject, resolve,
64
60
  }));
package/src/transform.js CHANGED
@@ -6,14 +6,17 @@ import {
6
6
  import { buffer } from 'node:stream/consumers';
7
7
  import { types } from 'node:util';
8
8
  import { encode } from './codecs.js';
9
- import { FormData } from './formdata.js';
9
+ import {
10
+ fdToAsyncIterable,
11
+ isFormData,
12
+ } from './formdata.js';
10
13
  import {
11
14
  APPLICATION_FORM_URLENCODED,
12
15
  APPLICATION_JSON,
13
16
  APPLICATION_OCTET_STREAM,
14
17
  } from './mediatypes.js';
15
18
  import {
16
- isFileLike,
19
+ isBlobLike,
17
20
  isReadableStream,
18
21
  } from './utils.js';
19
22
 
@@ -32,7 +35,7 @@ export const transform = async (options) => {
32
35
 
33
36
  if (!Buffer.isBuffer(body)) {
34
37
  switch (true) {
35
- case isFileLike(body): {
38
+ case isBlobLike(body): {
36
39
  headers = {
37
40
  [HTTP2_HEADER_CONTENT_LENGTH]: body.size,
38
41
  [HTTP2_HEADER_CONTENT_TYPE]: body.type || APPLICATION_OCTET_STREAM,
@@ -41,8 +44,8 @@ export const transform = async (options) => {
41
44
  break;
42
45
  }
43
46
 
44
- case FormData.alike(body): {
45
- body = FormData.actuate(body);
47
+ case isFormData(body): {
48
+ body = fdToAsyncIterable(body);
46
49
  headers = { [HTTP2_HEADER_CONTENT_TYPE]: body.contentType };
47
50
  break;
48
51
  }