rekwest 7.2.2 → 7.2.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.
package/README.md CHANGED
@@ -127,9 +127,8 @@ console.log(res.body);
127
127
  with the request
128
128
  * `bufferBody` **{boolean}** `Default: false` Toggles the buffering of the streamable request bodies for redirects and
129
129
  retries
130
- * `cookies` **{boolean | string[] | Array<[k, v]> | Cookies | Object | URLSearchParams}** `Default: true` The
131
- cookies to add to
132
- the request
130
+ * `cookies` **{boolean | string | string[] | [k, v][] | Cookies | Object | URLSearchParams}** `Default: true` The
131
+ cookies to add to the request. Manually set `cookie` header to override.
133
132
  * `cookiesTTL` **{boolean}** `Default: false` Controls enablement of TTL for the cookies cache
134
133
  * `credentials` **{include | omit | same-origin}** `Default: same-origin` Controls credentials in case of cross-origin
135
134
  redirects
package/dist/cookies.cjs CHANGED
@@ -3,10 +3,17 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.Cookies = void 0;
6
+ exports.splitCookie = exports.maxCookieSize = exports.maxCookieLifetimeCap = exports.isValidCookie = exports.illegalCookieChars = exports.cookieRex = exports.cookiePairRex = exports.Cookies = void 0;
7
7
  var _utils = require("./utils.cjs");
8
- const lifetimeCap = 3456e7; // 400 days
9
-
8
+ const cookieRex = exports.cookieRex = /^[\w-]+=(?:"[^"]*"|[^\p{Control};]*)(?:;\s*(?:[\w-]+=(?:"[^"]*"|[^\p{Control};]*)|[\w-]+))*$/u;
9
+ const cookiePairRex = exports.cookiePairRex = /(?:[^;"\s]+="[^"]*"|[^;]+)(?=;|$)/g;
10
+ const illegalCookieChars = exports.illegalCookieChars = /\p{Control}/u;
11
+ const isValidCookie = str => str?.constructor === String && cookieRex.test(str);
12
+ exports.isValidCookie = isValidCookie;
13
+ const maxCookieLifetimeCap = exports.maxCookieLifetimeCap = 3456e7; // 400 days
14
+ const maxCookieSize = exports.maxCookieSize = 4096;
15
+ const splitCookie = str => str.match(cookiePairRex).map(str => str.trim());
16
+ exports.splitCookie = splitCookie;
10
17
  class Cookies extends URLSearchParams {
11
18
  static #finalizers = new Set();
12
19
  static jar = new Map();
@@ -27,27 +34,39 @@ class Cookies extends URLSearchParams {
27
34
  } = {
28
35
  cookiesTTL: false
29
36
  }) {
30
- if (Array.isArray(input) && input.every(it => !Array.isArray(it))) {
31
- input = input.map(it => {
32
- if (!cookiesTTL) {
33
- return [it.split(';').at(0).trim()];
34
- }
35
- const [cookie, ...attrs] = it.split(';').map(it => it.trim());
36
- const ttl = {};
37
- for (const attr of attrs) {
38
- if (/(?:expires|max-age)=/i.test(attr)) {
39
- const [key, val] = attr.toLowerCase().split('=');
40
- const ms = Number.isFinite(Number.parseInt(val, 10)) ? val * 1e3 : Date.parse(val) - Date.now();
41
- ttl[(0, _utils.toCamelCase)(key)] = Math.min(ms, lifetimeCap);
37
+ if (isValidCookie(input)) {
38
+ input = splitCookie(input);
39
+ }
40
+ const ttlMap = new Map();
41
+ if (Array.isArray(input)) {
42
+ if (input.every(it => isValidCookie(it))) {
43
+ input = input.filter(it => !illegalCookieChars.test(it) && it.length <= maxCookieSize);
44
+ input = input.map(splitCookie).map(([cookie, ...attrs]) => {
45
+ try {
46
+ cookie = cookie.split('=').map(it => decodeURIComponent(it.trim()));
47
+ return cookie;
48
+ } finally {
49
+ if (cookiesTTL) {
50
+ for (const attr of attrs) {
51
+ if (/(?:expires|max-age)=/i.test(attr)) {
52
+ const [key, val] = attr.toLowerCase().split('=');
53
+ let interval = val * 1e3 || Date.parse(val) - Date.now();
54
+ if (interval < 0 || Number.isNaN(interval)) {
55
+ interval = 0;
56
+ }
57
+ ttlMap.set(cookie[0], {
58
+ [(0, _utils.toCamelCase)(key.trim())]: Math.min(interval, maxCookieLifetimeCap)
59
+ });
60
+ }
61
+ }
62
+ }
42
63
  }
43
- }
44
- return [cookie.replace(/\u0022/g, ''), Object.keys(ttl).length ? ttl : null];
45
- });
64
+ });
65
+ }
46
66
  }
47
- super(Array.isArray(input) ? input.map(it => it.at(0)).join('&') : input);
48
- if (Array.isArray(input) && cookiesTTL) {
49
- for (const [cookie, ttl] of input.filter(it => it.at(1))) {
50
- const key = cookie.split('=').at(0);
67
+ super(input);
68
+ if (ttlMap.size) {
69
+ for (const [key, attrs] of ttlMap) {
51
70
  if (this.#chronometry.has(key)) {
52
71
  clearTimeout(this.#chronometry.get(key));
53
72
  this.#chronometry.delete(key);
@@ -55,9 +74,9 @@ class Cookies extends URLSearchParams {
55
74
  const {
56
75
  expires,
57
76
  maxAge
58
- } = ttl;
59
- for (const ms of [maxAge, expires]) {
60
- if (!Number.isInteger(ms)) {
77
+ } = attrs;
78
+ for (const interval of [maxAge, expires]) {
79
+ if (!Number.isInteger(interval)) {
61
80
  continue;
62
81
  }
63
82
  const ref = new WeakRef(this);
@@ -67,7 +86,7 @@ class Cookies extends URLSearchParams {
67
86
  ctx.#chronometry.delete(key);
68
87
  ctx.delete(key);
69
88
  }
70
- }, Math.max(ms, 0));
89
+ }, Math.max(interval, 0));
71
90
  this.constructor.#register(this, tid);
72
91
  this.#chronometry.set(key, tid);
73
92
  break;
package/dist/formdata.cjs CHANGED
@@ -7,7 +7,6 @@ exports.FormData = void 0;
7
7
  var _nodeBuffer = require("node:buffer");
8
8
  var _nodeCrypto = require("node:crypto");
9
9
  var _nodeHttp = _interopRequireDefault(require("node:http2"));
10
- var _nodeUtil = require("node:util");
11
10
  var _mediatypes = require("./mediatypes.cjs");
12
11
  var _utils = require("./utils.cjs");
13
12
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -44,15 +43,15 @@ class FormData {
44
43
  return FormData.name === val?.[Symbol.toStringTag];
45
44
  }
46
45
  static #enfoldEntry(name, value, filename) {
47
- name = (0, _nodeUtil.toUSVString)(name);
48
- filename &&= (0, _nodeUtil.toUSVString)(filename);
46
+ name = String(name).toWellFormed();
47
+ filename &&= String(filename).toWellFormed();
49
48
  if ((0, _utils.isFileLike)(value)) {
50
49
  filename ??= value.name || 'blob';
51
50
  value = new _nodeBuffer.File([value], filename, value);
52
51
  } else if (this.#ensureInstance(value)) {
53
52
  value.name = filename;
54
53
  } else {
55
- value = (0, _nodeUtil.toUSVString)(value);
54
+ value = String(value).toWellFormed();
56
55
  }
57
56
  return {
58
57
  name,
@@ -106,7 +105,7 @@ class FormData {
106
105
  delete(...args) {
107
106
  (0, _utils.brandCheck)(this, FormData);
108
107
  this.#ensureArgs(args, 1, 'delete');
109
- const name = (0, _nodeUtil.toUSVString)(args[0]);
108
+ const name = String(args[0]).toWellFormed();
110
109
  this.#entries = this.#entries.filter(it => it.name !== name);
111
110
  }
112
111
  forEach(...args) {
@@ -120,19 +119,19 @@ class FormData {
120
119
  get(...args) {
121
120
  (0, _utils.brandCheck)(this, FormData);
122
121
  this.#ensureArgs(args, 1, 'get');
123
- const name = (0, _nodeUtil.toUSVString)(args[0]);
124
- return (this.#entries.find(it => it.name === name) ?? {}).value ?? null;
122
+ const name = String(args[0]).toWellFormed();
123
+ return this.#entries.find(it => it.name === name)?.value ?? null;
125
124
  }
126
125
  getAll(...args) {
127
126
  (0, _utils.brandCheck)(this, FormData);
128
127
  this.#ensureArgs(args, 1, 'getAll');
129
- const name = (0, _nodeUtil.toUSVString)(args[0]);
128
+ const name = String(args[0]).toWellFormed();
130
129
  return this.#entries.filter(it => it.name === name).map(it => it.value);
131
130
  }
132
131
  has(...args) {
133
132
  (0, _utils.brandCheck)(this, FormData);
134
133
  this.#ensureArgs(args, 1, 'has');
135
- const name = (0, _nodeUtil.toUSVString)(args[0]);
134
+ const name = String(args[0]).toWellFormed();
136
135
  return !!this.#entries.find(it => it.name === name);
137
136
  }
138
137
  set(...args) {
@@ -56,7 +56,7 @@ const preflight = options => {
56
56
  } else {
57
57
  cookie &&= _cookies.Cookies.jar.get(url.origin);
58
58
  }
59
- options.headers = {
59
+ headers = {
60
60
  ...(cookie && {
61
61
  [HTTP2_HEADER_COOKIE]: cookie
62
62
  }),
package/dist/retries.cjs CHANGED
@@ -35,7 +35,7 @@ const retries = (err, options) => {
35
35
  } = retry;
36
36
  if (retry.retryAfter && err.headers?.[HTTP2_HEADER_RETRY_AFTER]) {
37
37
  interval = err.headers[HTTP2_HEADER_RETRY_AFTER];
38
- interval = Number.isFinite(Number.parseInt(interval, 10)) ? interval * 1e3 : new Date(interval) - Date.now();
38
+ interval = interval * 1e3 || Date.parse(interval) - Date.now();
39
39
  if (interval > retry.maxRetryAfter) {
40
40
  throw new _errors.RequestError(`Maximum '${HTTP2_HEADER_RETRY_AFTER}' limit exceeded: ${interval} ms`, {
41
41
  cause: err
@@ -44,7 +44,7 @@ const retries = (err, options) => {
44
44
  } else {
45
45
  interval = new Function('interval', `return Math.ceil(${retry.backoffStrategy});`)(interval);
46
46
  }
47
- if (interval < 0) {
47
+ if (interval < 0 || Number.isNaN(interval)) {
48
48
  interval = 0;
49
49
  }
50
50
  retry.attempts--;
package/dist/utils.cjs CHANGED
@@ -118,37 +118,43 @@ const normalize = (url, options = {}) => {
118
118
  if (!options.redirected) {
119
119
  options = cloneWith(_config.default.defaults, options);
120
120
  }
121
- if (options.trimTrailingSlashes) {
122
- url = `${url}`.replace(/(?<!:)\/+/g, '/');
123
- }
124
- if (options.stripTrailingSlash) {
125
- url = `${url}`.replace(/\/$|\/(?=#)|\/(?=\?)/g, '');
126
- }
127
121
  return Object.assign(options, {
128
122
  headers: normalizeHeaders(options.headers),
129
123
  method: options.method.toUpperCase(),
130
- url: addSearchParams(new URL(url, options.baseURL), options.params)
124
+ url: addSearchParams(normalizeUrl(new URL(url, options.baseURL), options), options.params)
131
125
  });
132
126
  };
133
127
  exports.normalize = normalize;
134
128
  const normalizeHeaders = (headers = {}) => {
135
129
  const acc = {};
136
- for (const [key, val] of Object.entries(headers)) {
137
- const name = key.toLowerCase();
130
+ for (let [key, val] of Object.entries(headers)) {
131
+ key = key.toLowerCase();
138
132
  acc[key] = val;
139
133
  if (key === HTTP2_HEADER_ACCEPT_ENCODING && !_config.isZstdSupported) {
140
- const modified = val.replace(/\s?zstd,?/gi, '').trim();
141
- if (modified) {
142
- acc[key] = modified;
134
+ val = val.replace(/\s?zstd,?/gi, '').trim();
135
+ if (val) {
136
+ acc[key] = val;
143
137
  } else {
144
- Reflect.deleteProperty(acc, name);
138
+ Reflect.deleteProperty(acc, key);
145
139
  }
146
140
  }
147
141
  }
148
142
  return acc;
149
143
  };
150
144
  exports.normalizeHeaders = normalizeHeaders;
151
- const sameOrigin = (a, b) => a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
145
+ function normalizeUrl(url, {
146
+ trimTrailingSlashes,
147
+ stripTrailingSlash
148
+ } = {}) {
149
+ if (trimTrailingSlashes) {
150
+ url.pathname = url.pathname.replace(/\/{2,}/g, '/');
151
+ }
152
+ if (stripTrailingSlash && url.pathname !== '/') {
153
+ url.pathname = url.pathname.replace(/\/$/, '');
154
+ }
155
+ return url;
156
+ }
157
+ const sameOrigin = (a, b) => a.origin === b.origin;
152
158
  exports.sameOrigin = sameOrigin;
153
159
  const snoop = (client, req, options) => {
154
160
  req.once('close', () => client?.close());
@@ -162,9 +168,9 @@ const snoop = (client, req, options) => {
162
168
  });
163
169
  };
164
170
  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())));
171
+ const stripHeaders = (headers = {}, keys = []) => {
172
+ keys = new Set(keys);
173
+ return Object.fromEntries(Object.entries(headers).filter(([key]) => !keys.has(key)));
168
174
  };
169
175
  exports.stripHeaders = stripHeaders;
170
176
  async function* tap(val) {
package/package.json CHANGED
@@ -71,5 +71,5 @@
71
71
  "test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
72
72
  },
73
73
  "type": "module",
74
- "version": "7.2.2"
74
+ "version": "7.2.4"
75
75
  }
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(';').at(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.at(0)).join('&') : input);
73
+ super(input);
56
74
 
57
- if (Array.isArray(input) && cookiesTTL) {
58
- for (const [cookie, ttl] of input.filter((it) => it.at(1))) {
59
- const key = cookie.split('=').at(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
@@ -1,7 +1,6 @@
1
1
  import { File } from 'node:buffer';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import http2 from 'node:http2';
4
- import { toUSVString } from 'node:util';
5
4
  import {
6
5
  APPLICATION_OCTET_STREAM,
7
6
  MULTIPART_FORM_DATA,
@@ -64,8 +63,8 @@ export class FormData {
64
63
  }
65
64
 
66
65
  static #enfoldEntry(name, value, filename) {
67
- name = toUSVString(name);
68
- filename &&= toUSVString(filename);
66
+ name = String(name).toWellFormed();
67
+ filename &&= String(filename).toWellFormed();
69
68
 
70
69
  if (isFileLike(value)) {
71
70
  filename ??= value.name || 'blob';
@@ -73,7 +72,7 @@ export class FormData {
73
72
  } else if (this.#ensureInstance(value)) {
74
73
  value.name = filename;
75
74
  } else {
76
- value = toUSVString(value);
75
+ value = String(value).toWellFormed();
77
76
  }
78
77
 
79
78
  return {
@@ -152,7 +151,7 @@ export class FormData {
152
151
  delete(...args) {
153
152
  brandCheck(this, FormData);
154
153
  this.#ensureArgs(args, 1, 'delete');
155
- const name = toUSVString(args[0]);
154
+ const name = String(args[0]).toWellFormed();
156
155
 
157
156
  this.#entries = this.#entries.filter((it) => it.name !== name);
158
157
  }
@@ -173,15 +172,15 @@ export class FormData {
173
172
  get(...args) {
174
173
  brandCheck(this, FormData);
175
174
  this.#ensureArgs(args, 1, 'get');
176
- const name = toUSVString(args[0]);
175
+ const name = String(args[0]).toWellFormed();
177
176
 
178
- return (this.#entries.find((it) => it.name === name) ?? {}).value ?? null;
177
+ return this.#entries.find((it) => it.name === name)?.value ?? null;
179
178
  }
180
179
 
181
180
  getAll(...args) {
182
181
  brandCheck(this, FormData);
183
182
  this.#ensureArgs(args, 1, 'getAll');
184
- const name = toUSVString(args[0]);
183
+ const name = String(args[0]).toWellFormed();
185
184
 
186
185
  return this.#entries.filter((it) => it.name === name).map((it) => it.value);
187
186
  }
@@ -189,7 +188,7 @@ export class FormData {
189
188
  has(...args) {
190
189
  brandCheck(this, FormData);
191
190
  this.#ensureArgs(args, 1, 'has');
192
- const name = toUSVString(args[0]);
191
+ const name = String(args[0]).toWellFormed();
193
192
 
194
193
  return !!this.#entries.find((it) => it.name === name);
195
194
  }
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/utils.js CHANGED
@@ -126,36 +126,28 @@ export const normalize = (url, options = {}) => {
126
126
  options = cloneWith(config.defaults, options);
127
127
  }
128
128
 
129
- if (options.trimTrailingSlashes) {
130
- url = `${ url }`.replace(/(?<!:)\/+/g, '/');
131
- }
132
-
133
- if (options.stripTrailingSlash) {
134
- url = `${ url }`.replace(/\/$|\/(?=#)|\/(?=\?)/g, '');
135
- }
136
-
137
129
  return Object.assign(options, {
138
130
  headers: normalizeHeaders(options.headers),
139
131
  method: options.method.toUpperCase(),
140
- url: addSearchParams(new URL(url, options.baseURL), options.params),
132
+ url: addSearchParams(normalizeUrl(new URL(url, options.baseURL), options), options.params),
141
133
  });
142
134
  };
143
135
 
144
136
  export const normalizeHeaders = (headers = {}) => {
145
137
  const acc = {};
146
138
 
147
- for (const [key, val] of Object.entries(headers)) {
148
- const name = key.toLowerCase();
139
+ for (let [key, val] of Object.entries(headers)) {
140
+ key = key.toLowerCase();
149
141
 
150
142
  acc[key] = val;
151
143
 
152
144
  if (key === HTTP2_HEADER_ACCEPT_ENCODING && !isZstdSupported) {
153
- const modified = val.replace(/\s?zstd,?/gi, '').trim();
145
+ val = val.replace(/\s?zstd,?/gi, '').trim();
154
146
 
155
- if (modified) {
156
- acc[key] = modified;
147
+ if (val) {
148
+ acc[key] = val;
157
149
  } else {
158
- Reflect.deleteProperty(acc, name);
150
+ Reflect.deleteProperty(acc, key);
159
151
  }
160
152
  }
161
153
  }
@@ -163,7 +155,19 @@ export const normalizeHeaders = (headers = {}) => {
163
155
  return acc;
164
156
  };
165
157
 
166
- export const sameOrigin = (a, b) => a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
158
+ function normalizeUrl(url, { trimTrailingSlashes, stripTrailingSlash } = {}) {
159
+ if (trimTrailingSlashes) {
160
+ url.pathname = url.pathname.replace(/\/{2,}/g, '/');
161
+ }
162
+
163
+ if (stripTrailingSlash && url.pathname !== '/') {
164
+ url.pathname = url.pathname.replace(/\/$/, '');
165
+ }
166
+
167
+ return url;
168
+ }
169
+
170
+ export const sameOrigin = (a, b) => a.origin === b.origin;
167
171
 
168
172
  export const snoop = (client, req, options) => {
169
173
  req.once('close', () => client?.close());
@@ -177,14 +181,10 @@ export const snoop = (client, req, options) => {
177
181
  });
178
182
  };
179
183
 
180
- export const stripHeaders = (headers = {}, names = []) => {
181
- names = new Set(names);
184
+ export const stripHeaders = (headers = {}, keys = []) => {
185
+ keys = new Set(keys);
182
186
 
183
- return Object.fromEntries(
184
- Object.entries(headers).filter(
185
- ([key]) => !names.has(key.toLowerCase()),
186
- ),
187
- );
187
+ return Object.fromEntries(Object.entries(headers).filter(([key]) => !keys.has(key)));
188
188
  };
189
189
 
190
190
  export async function* tap(val) {