@e-mc/request 0.13.5 → 0.13.7

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
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## Interface
11
11
 
12
- * [View Source](https://www.unpkg.com/@e-mc/types@0.13.5/lib/index.d.ts)
12
+ * [View Source](https://www.unpkg.com/@e-mc/types@0.13.7/lib/index.d.ts)
13
13
 
14
14
  ```typescript
15
15
  import type { IModule, ModuleConstructor } from "./index";
@@ -252,9 +252,9 @@ instance.get("http://hostname/path/config.yml", options).then(data => {
252
252
 
253
253
  ## References
254
254
 
255
- - https://www.unpkg.com/@e-mc/types@0.13.5/lib/http.d.ts
256
- - https://www.unpkg.com/@e-mc/types@0.13.5/lib/request.d.ts
257
- - https://www.unpkg.com/@e-mc/types@0.13.5/lib/settings.d.ts
255
+ - https://www.unpkg.com/@e-mc/types@0.13.7/lib/http.d.ts
256
+ - https://www.unpkg.com/@e-mc/types@0.13.7/lib/request.d.ts
257
+ - https://www.unpkg.com/@e-mc/types@0.13.7/lib/settings.d.ts
258
258
 
259
259
  * https://www.npmjs.com/package/@types/node
260
260
 
@@ -355,7 +355,12 @@ class HttpAdapter {
355
355
  result = new (require(packageName = 'fast-xml-parser').XMLParser)(parser).parse(buffer);
356
356
  break;
357
357
  case 'toml':
358
- result = require(packageName = 'toml').parse(buffer);
358
+ try {
359
+ result = require('smol-toml').parse(buffer);
360
+ }
361
+ catch {
362
+ result = require(packageName = 'toml').parse(buffer);
363
+ }
359
364
  break;
360
365
  default:
361
366
  result = JSON.parse(buffer);
@@ -417,6 +422,9 @@ class HttpAdapter {
417
422
  }
418
423
  else if (++this.redirects <= this.redirectLimit) {
419
424
  this.abortResponse();
425
+ if (this.opts.method?.toUpperCase() === 'PUT' && statusCode !== 307 && statusCode !== 308) {
426
+ this.#options.method = 'GET';
427
+ }
420
428
  this.setOpts((0, util_1.fromURL)(this.opts.url, location));
421
429
  this.init();
422
430
  }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ class HttpHostAltSvc {
3
+ host;
4
+ #location = {};
5
+ #available = [];
6
+ #errors = [];
7
+ #versionData;
8
+ constructor(host, versionData) {
9
+ this.host = host;
10
+ this.#versionData = versionData;
11
+ }
12
+ did(version) {
13
+ return this.#versionData[version].status !== -1;
14
+ }
15
+ next() {
16
+ const queue = this.#available.shift();
17
+ if (queue) {
18
+ const { hostname, port, version, expires } = queue;
19
+ const ms = expires - Date.now();
20
+ if (ms < 0) {
21
+ return this.next();
22
+ }
23
+ let timeout = null;
24
+ if (!isNaN(ms)) {
25
+ timeout = setTimeout(() => {
26
+ if (!this.next()) {
27
+ this.host.version = 1;
28
+ }
29
+ }, ms);
30
+ }
31
+ this.#location = { hostname, port, version, timeout, origin: this.host.protocol + '//' + hostname + ':' + port };
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ close(error) {
37
+ const { hostname, port, version, timeout } = this.#location;
38
+ if (hostname) {
39
+ this.#location = {};
40
+ if (timeout) {
41
+ clearTimeout(timeout);
42
+ }
43
+ if (error) {
44
+ this.#errors.push({ hostname, port, version });
45
+ return this.next();
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ clear(version) {
51
+ if (version) {
52
+ if (this.#available.length === 0) {
53
+ this.flag(version, 0);
54
+ }
55
+ else if (!this.close(true)) {
56
+ this.flag(version, -1);
57
+ }
58
+ }
59
+ else {
60
+ this.close();
61
+ this.#available = [];
62
+ this.#errors = [];
63
+ for (const item of this.#versionData) {
64
+ if (item.alpn !== 0) {
65
+ item.status = -1;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ flag(version, value) {
71
+ this.#versionData[version - 1].status = value;
72
+ }
73
+ valid(hostname, port, version) {
74
+ return !this.errors.find(item => item.hostname === hostname && item.port === port && item.version === version);
75
+ }
76
+ set available(value) {
77
+ this.#available = value;
78
+ }
79
+ get errors() {
80
+ return this.#errors;
81
+ }
82
+ get hostname() {
83
+ return this.#location.hostname;
84
+ }
85
+ get port() {
86
+ return this.#location.port;
87
+ }
88
+ get origin() {
89
+ return this.#location.origin;
90
+ }
91
+ }
92
+ module.exports = HttpHostAltSvc;
@@ -1,19 +1,18 @@
1
1
  "use strict";
2
2
  const tls = require("node:tls");
3
3
  const types_1 = require("@e-mc/types");
4
+ const altsvc_1 = require("@e-mc/request/http/host/altsvc");
4
5
  const HOST_LOCAL = new Set(['localhost']);
5
6
  const HOST_STREAM = new Map();
6
7
  const HOST_HTTP_1_1 = [];
7
8
  const HOST_ALPN_H2C = [];
8
9
  const HOST_ALPN_H2 = [];
9
- (function () {
10
- const nic = require('node:os').networkInterfaces();
11
- for (const name in nic) {
12
- for (const { address } of nic[name].filter(item => item.internal)) {
13
- HOST_LOCAL.add(address);
14
- }
10
+ for (const network of Object.entries(require('node:os').networkInterfaces())) {
11
+ for (const { address } of network[1].filter(item => item.internal)) {
12
+ HOST_LOCAL.add(address);
15
13
  }
16
- })();
14
+ }
15
+ const createData = () => ({ success: 0, failed: 0, errors: 0, alpn: 1, status: -1 });
17
16
  class HttpHost {
18
17
  static normalizeOrigin(value) {
19
18
  return (value = value.trim()).replace(/\/+$/, '') + (!/:\d+$/.test(value) ? ':' + (value.startsWith('https') ? '443' : '80') : '');
@@ -64,19 +63,14 @@ class HttpHost {
64
63
  localhost;
65
64
  _tlsConnect = null;
66
65
  #version;
67
- #altSvc = [];
68
- #altSvcQueue = [];
69
- #altSvcError = [];
70
66
  #protocol;
71
67
  #secure;
72
68
  #hostname;
73
69
  #port;
74
70
  #origin;
75
71
  #streamSize;
76
- #versionData = [
77
- [0, 0, 0, 1, -1],
78
- [0, 0, 0, -1, -1]
79
- ];
72
+ #versionData = [createData(), createData()];
73
+ #altSvc;
80
74
  constructor(url, httpVersion = 1) {
81
75
  const { protocol, hostname } = url;
82
76
  const secure = protocol === 'https:';
@@ -89,6 +83,7 @@ class HttpHost {
89
83
  this.#origin = url.origin;
90
84
  this.localhost = HOST_LOCAL.has(hostname);
91
85
  this.#streamSize = HOST_STREAM.get(address) || (this.localhost ? 65536 : 4096);
86
+ this.#altSvc = new altsvc_1(this, this.#versionData);
92
87
  if (protocol !== 'file:' && !HOST_HTTP_1_1.includes(address = protocol + '//' + address)) {
93
88
  if (secure) {
94
89
  this.#version = HOST_ALPN_H2.includes(address) ? 2 : httpVersion;
@@ -101,7 +96,7 @@ class HttpHost {
101
96
  }
102
97
  this.#version = 1;
103
98
  for (const version of this.#versionData) {
104
- version[4] = 0;
99
+ version.status = 0;
105
100
  }
106
101
  }
107
102
  async hasProtocol(version) {
@@ -110,17 +105,17 @@ class HttpHost {
110
105
  if (!data || !this.secure) {
111
106
  return 0;
112
107
  }
113
- const status = data[3];
114
- switch (status) {
108
+ switch (data.alpn) {
115
109
  case 0:
116
110
  case 1:
117
- return status;
111
+ return data.alpn;
118
112
  default:
119
113
  return this._tlsConnect ||= new Promise(resolve => {
120
114
  const alpn = 'h' + version;
121
115
  const socket = tls.connect(+this.port, this.hostname, { ALPNProtocols: [alpn], requestCert: true, rejectUnauthorized: false }, () => {
122
116
  this._tlsConnect = null;
123
- resolve(data[3] = alpn === socket.alpnProtocol ? 1 : 0);
117
+ data.alpn = alpn === socket.alpnProtocol ? 1 : 0;
118
+ resolve(data.alpn);
124
119
  });
125
120
  socket
126
121
  .setNoDelay(false)
@@ -130,7 +125,8 @@ class HttpHost {
130
125
  if (this._tlsConnect) {
131
126
  this._tlsConnect = null;
132
127
  if (this.error(version) >= 10) {
133
- resolve(data[3] = 0);
128
+ data.alpn = 0;
129
+ resolve(0);
134
130
  }
135
131
  else {
136
132
  resolve(2);
@@ -140,7 +136,8 @@ class HttpHost {
140
136
  .on('error', () => {
141
137
  this.failed(version);
142
138
  this._tlsConnect = null;
143
- resolve(data[3] = 0);
139
+ data.alpn = 0;
140
+ resolve(0);
144
141
  })
145
142
  .end();
146
143
  });
@@ -150,179 +147,132 @@ class HttpHost {
150
147
  }
151
148
  success(version, status) {
152
149
  const data = this.#versionData[version - 1];
153
- return status ? data[0] : ++data[0];
150
+ return status ? data.success : ++data.success;
154
151
  }
155
152
  failed(version, status) {
156
153
  const data = this.#versionData[version - 1];
157
154
  if (status) {
158
- return data[1];
155
+ return data.failed;
159
156
  }
160
- this.clearAltSvc(version);
161
- return ++data[1];
157
+ this.altSvc.clear(version);
158
+ return ++data.failed;
162
159
  }
163
160
  error(version, status) {
164
161
  const data = this.#versionData[version - 1];
165
162
  if (status) {
166
- return data[2];
163
+ return data.errors;
167
164
  }
168
- if (data[4] !== 2) {
169
- this.closeAltSvc(true);
165
+ if (data.status !== 2) {
166
+ this.altSvc.close(true);
170
167
  }
171
- return ++data[2];
168
+ return ++data.errors;
172
169
  }
173
170
  upgrade(version, altSvc) {
174
171
  if (altSvc && this.secure) {
175
172
  if (altSvc === 'clear') {
176
- this.clearAltSvc();
173
+ this.altSvc.clear();
177
174
  return;
178
175
  }
179
176
  const data = this.#versionData;
180
177
  for (let i = data.length - 1; i >= version; --i) {
181
178
  const host = data[i];
182
- if (host[4] === 0) {
179
+ if (host.status === 0) {
183
180
  continue;
184
181
  }
182
+ const h = i + 1;
185
183
  const increment = (flag) => {
186
- host[4] = flag;
187
- if (this.#version < i + 1) {
188
- this.#version = i + 1;
184
+ host.status = flag;
185
+ if (this.#version < h) {
186
+ this.#version = h;
189
187
  return true;
190
188
  }
191
189
  return false;
192
190
  };
193
- const addresses = [];
194
- const time = Date.now();
191
+ const available = [];
195
192
  const hostname = this.#hostname;
196
- const excluded = this.#altSvcError;
197
- for (const match of altSvc.matchAll(new RegExp(`h${i + 1}(?:-\\d+)?="([^:]*):(\\d+)"([^,]*)`, 'g'))) {
193
+ for (const match of altSvc.matchAll(new RegExp(`h${h}(?:-\\d+)?="([^:]*):(\\d+)"([^,]*)`, 'g'))) {
198
194
  const port = match[2];
199
195
  if (!match[1] && port === this.port) {
200
196
  increment(2);
201
- addresses.length = 0;
197
+ available.length = 0;
202
198
  break;
203
199
  }
204
200
  const address = match[1] || hostname;
205
201
  const ma = +(/ma=(\d+)/.exec(match[3])?.[1] || 86400);
206
- if (!excluded.includes(`h${i + 1}:${address}:${port}`)) {
207
- addresses.push([
208
- address,
202
+ if (this.altSvc.valid(address, port, h)) {
203
+ available.push({
204
+ hostname: address,
209
205
  port,
210
- ma >= 2592000 ? NaN : time + Math.min(ma * 1000, 2147483647),
211
- i + 1,
212
- match[3].includes('persist=1')
213
- ]);
206
+ version: h,
207
+ expires: ma >= 2592000 ? NaN : Date.now() + Math.min(ma * 1000, 2147483647),
208
+ persist: match[3].includes('persist=1')
209
+ });
214
210
  }
215
211
  }
216
- if (addresses.length > 0) {
217
- this.closeAltSvc();
218
- this.#altSvcQueue = addresses.sort((a, b) => {
219
- if (a[0] === hostname) {
212
+ if (available.length > 0) {
213
+ this.altSvc.close();
214
+ this.altSvc.available = available.sort((a, b) => {
215
+ if (a.hostname === hostname) {
220
216
  return -1;
221
217
  }
222
- if (b[0] === hostname) {
218
+ if (b.hostname === hostname) {
223
219
  return 1;
224
220
  }
225
- if (isNaN(a[2]) || a[2] > b[2]) {
221
+ if (isNaN(a.expires) || a.expires > b.expires) {
226
222
  return -1;
227
223
  }
228
- if (isNaN(b[2]) || a[2] < b[2]) {
224
+ if (isNaN(b.expires) || a.expires < b.expires) {
229
225
  return 1;
230
226
  }
231
- if (a[3] && !b[3]) {
232
- return -1;
233
- }
234
- if (!a[3] && b[3]) {
235
- return -1;
236
- }
237
227
  return 0;
238
228
  });
239
229
  if (increment(1)) {
240
- this.nextAltSvc();
230
+ this.altSvc.next();
241
231
  }
242
232
  }
243
233
  }
244
234
  }
245
235
  }
246
236
  didAltSvc(version) {
247
- return this.#versionData[version][4] !== -1;
237
+ return this.#versionData[version].status !== -1;
248
238
  }
249
239
  nextAltSvc() {
250
- const queue = this.#altSvcQueue.shift();
251
- if (queue) {
252
- const [hostname, port, expires, version] = queue;
253
- const timeout = expires - Date.now();
254
- if (timeout < 0) {
255
- return this.nextAltSvc();
256
- }
257
- let timer = null;
258
- if (!isNaN(timeout)) {
259
- timer = setTimeout(() => {
260
- if (!this.nextAltSvc()) {
261
- this.version = 1;
262
- }
263
- }, timeout);
264
- }
265
- this.#altSvc = [hostname, port, timer, version, this.protocol + '//' + hostname + ':' + port];
266
- return true;
267
- }
268
- return false;
240
+ return this.altSvc.next();
269
241
  }
270
242
  closeAltSvc(error) {
271
- const [hostname, port, timeout, version] = this.#altSvc;
272
- if (hostname) {
273
- this.#altSvc = [];
274
- if (timeout) {
275
- clearTimeout(timeout);
276
- }
277
- if (error) {
278
- this.#altSvcError.push(`h${version}:${hostname}:${port}`);
279
- return this.nextAltSvc();
280
- }
281
- }
282
- return false;
243
+ return this.altSvc.close(error);
283
244
  }
284
245
  clearAltSvc(version) {
285
- if (version) {
286
- if (this.#altSvcQueue.length === 0) {
287
- this.flagAltSvc(version, 0);
288
- }
289
- else if (!this.closeAltSvc(true)) {
290
- this.flagAltSvc(version, -1);
291
- }
292
- }
293
- else {
294
- this.closeAltSvc();
295
- this.#altSvcQueue = [];
296
- this.#altSvcError = [];
297
- for (const item of this.#versionData) {
298
- if (item[3]) {
299
- item[4] = -1;
300
- }
301
- }
302
- }
246
+ this.altSvc.clear(version);
303
247
  }
304
248
  flagAltSvc(version, value) {
305
- this.#versionData[version - 1][4] = value;
249
+ this.altSvc.flag(version, value);
306
250
  }
307
251
  reset() {
308
- this.clearAltSvc();
252
+ this.altSvc.clear();
309
253
  this.#versionData.forEach((item, index) => {
310
- item[0] = 0;
311
- item[1] = 0;
312
- item[2] = 0;
254
+ item.success = 0;
255
+ item.failed = 0;
256
+ item.errors = 0;
313
257
  if (index > 0) {
314
- item[3] = -1;
258
+ item.alpn = -1;
315
259
  }
316
260
  });
317
261
  }
262
+ v1() {
263
+ return this.#version === 1;
264
+ }
318
265
  v2() {
319
266
  return this.#version === 2;
320
267
  }
268
+ get altSvc() {
269
+ return this.#altSvc;
270
+ }
321
271
  set version(value) {
322
272
  switch (value) {
323
273
  case 1:
324
274
  case 2:
325
- this.flagAltSvc(this.#version = value, -1);
275
+ this.altSvc.flag(this.#version = value, -1);
326
276
  break;
327
277
  }
328
278
  }
@@ -336,13 +286,13 @@ class HttpHost {
336
286
  return this.#secure;
337
287
  }
338
288
  get hostname() {
339
- return this.#altSvc[0] || this.#hostname;
289
+ return this.altSvc.hostname || this.#hostname;
340
290
  }
341
291
  get port() {
342
- return this.#altSvc[1] || this.#port;
292
+ return this.altSvc.port || this.#port;
343
293
  }
344
294
  get origin() {
345
- return this.#altSvc[4] || this.#origin;
295
+ return this.altSvc.origin || this.#origin;
346
296
  }
347
297
  get streamSize() {
348
298
  return this.#streamSize;
package/index.js CHANGED
@@ -28,13 +28,14 @@ const REGEXP_GLOBWITHIN = /\\\?|(?:(?<!\\)(?:\*|\[!?[^!\]]+\]|\{(?:[^,]+,)+[^}]+
28
28
  const REGEXP_RCLONE = /^rclone:\?/i;
29
29
  const HTTP = {
30
30
  HOST: {},
31
- HEADERS: {},
31
+ HEADERS: Object.create(null),
32
+ CACHE: new WeakMap(),
32
33
  VERSION: 1,
33
34
  PROXY: null
34
35
  };
35
36
  const TLS = {
36
- TEXT: {},
37
- FILE: {}
37
+ TEXT: Object.create(null),
38
+ FILE: Object.create(null)
38
39
  };
39
40
  const DNS = {
40
41
  CACHE: Object.create(null),
@@ -61,7 +62,7 @@ const ARIA2 = {
61
62
  LOWEST_SPEED_LIMIT: null,
62
63
  ALWAYS_RESUME: false,
63
64
  FILE_ALLOCATION: 'none',
64
- PROXY: {},
65
+ PROXY: Object.create(null),
65
66
  NO_PROXY: '',
66
67
  CONF_PATH: ''
67
68
  };
@@ -115,21 +116,6 @@ let READ_TIMEOUT = 0;
115
116
  let AGENT_TIMEOUT = 0;
116
117
  let LOG_HTTP = false;
117
118
  let LOG_TIMEPROCESS = true;
118
- function getBaseHeaders(uri, headers) {
119
- let result;
120
- uri = (0, util_1.trimPath)(uri);
121
- for (const pathname in headers) {
122
- if (pathname === uri || uri.startsWith(pathname + '/')) {
123
- (result ||= []).push([pathname, headers[pathname]]);
124
- }
125
- }
126
- if (result) {
127
- if (result.length > 1) {
128
- result.sort((a, b) => b[0].length - a[0].length);
129
- }
130
- return result[0][1];
131
- }
132
- }
133
119
  function setDnsCache(hostname, value, expires) {
134
120
  expires ??= DNS.EXPIRES;
135
121
  if (expires > 0 && !DNS.CACHE[hostname]) {
@@ -146,11 +132,6 @@ function setDnsCache(hostname, value, expires) {
146
132
  }
147
133
  }
148
134
  }
149
- function setOutgoingHeaders(output, headers) {
150
- for (const href in headers) {
151
- output[href] = (0, util_1.normalizeHeaders)(headers[href]);
152
- }
153
- }
154
135
  function getProxySettings(request, agentTimeout) {
155
136
  const proxy = request.proxy;
156
137
  if (proxy && (proxy.origin || proxy.address && proxy.port)) {
@@ -175,11 +156,9 @@ function getProxySettings(request, agentTimeout) {
175
156
  return null;
176
157
  }
177
158
  function closeTorrent(pid) {
178
- if (typeof pid === 'number') {
179
- const index = ARIA2.PID_QUEUE.findIndex(value => pid === value[0]);
180
- if (index !== -1) {
181
- ARIA2.PID_QUEUE.splice(index, 1);
182
- }
159
+ const index = ARIA2.PID_QUEUE.findIndex(value => value[0] === pid);
160
+ if (index !== -1) {
161
+ ARIA2.PID_QUEUE.splice(index, 1);
183
162
  }
184
163
  }
185
164
  function clearDnsLookup() {
@@ -199,9 +178,9 @@ function resetHttpHost(version) {
199
178
  case 2:
200
179
  for (const origin in HTTP.HOST) {
201
180
  const host = HTTP.HOST[origin];
202
- if (host.secure && host.version === 1) {
181
+ if (host.secure && host.v1()) {
203
182
  const failed = host.failed(2, true);
204
- if (failed === 0 && host.failed(2, true) < 10 || failed < 3 && host.success(2, true) > 0) {
183
+ if (failed === 0 && host.error(2, true) < 10 || failed < 3 && host.success(2, true) > 0) {
205
184
  host.version = version;
206
185
  }
207
186
  }
@@ -296,50 +275,16 @@ function copySearchParams(url, base) {
296
275
  }
297
276
  });
298
277
  }
299
- function checkEncoding(request, response, statusCode, outStream, contentEncoding) {
278
+ function checkEncoding(request, statusCode, contentEncoding) {
300
279
  switch (statusCode) {
301
280
  case 206:
302
281
  request.emit('error', (0, types_1.errorValue)("Aborted", 'Partial content'));
303
282
  case 204:
304
283
  case 205:
305
284
  case 304:
306
- return;
307
- }
308
- if (!contentEncoding) {
309
- return;
310
- }
311
- contentEncoding = contentEncoding.trim().toLowerCase();
312
- const chunkSize = outStream?.writableHighWaterMark;
313
- let pipeTo;
314
- if (!contentEncoding.includes(',')) {
315
- pipeTo = decompressEncoding(contentEncoding, chunkSize);
316
- }
317
- else {
318
- for (const value of contentEncoding.split(/\s*,\s*/).reverse()) {
319
- const next = decompressEncoding(value, chunkSize);
320
- if (!next) {
321
- return;
322
- }
323
- pipeTo = pipeTo ? pipeTo.pipe(next) : next;
324
- }
325
- }
326
- if (pipeTo) {
327
- if (outStream) {
328
- stream.pipeline(response, pipeTo, outStream, err => {
329
- if (err) {
330
- response.emit('error', err);
331
- }
332
- });
333
- }
334
- else {
335
- stream.pipeline(response, pipeTo, err => {
336
- if (err) {
337
- response.emit('error', err);
338
- }
339
- });
340
- }
341
- return pipeTo;
285
+ return false;
342
286
  }
287
+ return !!contentEncoding;
343
288
  }
344
289
  function sendBody(request, options) {
345
290
  const postData = options.postData;
@@ -351,26 +296,11 @@ function sendBody(request, options) {
351
296
  }
352
297
  request.end();
353
298
  }
354
- function decompressEncoding(value, chunkSize) {
355
- switch (value) {
356
- case 'gzip':
357
- return zlib.createGunzip({ chunkSize });
358
- case 'br':
359
- return zlib.createBrotliDecompress({ chunkSize });
360
- case 'deflate':
361
- return zlib.createInflate({ chunkSize });
362
- case 'deflate-raw':
363
- return zlib.createInflateRaw({ chunkSize });
364
- case 'zstd':
365
- if (SUPPORTED_ZSTD) {
366
- return zlib.createZstdDecompress({ chunkSize });
367
- }
368
- break;
369
- }
370
- }
371
299
  function resetAria2() {
372
- clearInterval(ARIA2.PID_TIMER);
373
- ARIA2.PID_TIMER = null;
300
+ if (ARIA2.PID_TIMER) {
301
+ clearInterval(ARIA2.PID_TIMER);
302
+ ARIA2.PID_TIMER = null;
303
+ }
374
304
  }
375
305
  function escapeShellQuote(value) {
376
306
  value = value.replace(/(?<!\\)"/g, '\\"');
@@ -464,32 +394,7 @@ function setBinHeaders(args, headers) {
464
394
  args.push(...items.map(value => `--header="${name}: ${escapeShellQuote(value)}"`));
465
395
  }
466
396
  }
467
- function finalizeBinArgs(instance, name, bin, args, opts, binOpts, cmd = []) {
468
- if (binOpts) {
469
- for (const leading of binOpts) {
470
- if (leading.startsWith('-')) {
471
- const pattern = new RegExp(`^${leading}(?:=|$)`);
472
- for (let i = 0; i < opts.length; ++i) {
473
- if (pattern.test(opts[i])) {
474
- opts.splice(i--, i);
475
- break;
476
- }
477
- }
478
- }
479
- }
480
- args = binOpts.concat(args);
481
- }
482
- if (args.length > 0) {
483
- if (module_1.hasLogType(32768)) {
484
- instance.formatMessage(32768, name.toUpperCase(), [bin].concat(cmd).join(' '), args.join(' '), { ...module_1.LOG_STYLE_WARN });
485
- }
486
- else {
487
- instance.addLog(4, path.basename(bin) + ' ' + args.join(' '), name);
488
- }
489
- }
490
- return args;
491
- }
492
- function addAria2Proxy(args, protocol, host) {
397
+ function appendAria2Proxy(args, protocol, host) {
493
398
  const { origin, username, password } = host;
494
399
  args.push(`--${protocol}-proxy="${origin}"`);
495
400
  if (username) {
@@ -716,7 +621,9 @@ class Request extends module_1 {
716
621
  }
717
622
  }
718
623
  if ((0, types_1.isPlainObject)(headers)) {
719
- setOutgoingHeaders(HTTP.HEADERS, headers);
624
+ for (const href in headers) {
625
+ HTTP.HEADERS[href] = (0, util_1.normalizeHeaders)(headers[href]);
626
+ }
720
627
  }
721
628
  if ((0, types_1.isPlainObject)(certs)) {
722
629
  [TLS.TEXT, TLS.FILE] = validateCerts(certs);
@@ -827,9 +734,9 @@ class Request extends module_1 {
827
734
  };
828
735
  #singleton = false;
829
736
  #httpVersion = null;
737
+ #headers = null;
830
738
  #ipVersion;
831
739
  #agentTimeout;
832
- #headers = null;
833
740
  #baseUrl = null;
834
741
  #connectDns = Object.create(null);
835
742
  #pendingDns = Object.create(null);
@@ -839,7 +746,7 @@ class Request extends module_1 {
839
746
  #adapter = HTTP_ADAPTER;
840
747
  #certs = null;
841
748
  #downloading = new Set();
842
- #hostInfo = {};
749
+ #hostInfo = Object.create(null);
843
750
  #session = [Object.create(null)];
844
751
  constructor(data) {
845
752
  super();
@@ -850,9 +757,6 @@ class Request extends module_1 {
850
757
  this.readTimeout = (value = (0, util_1.fromSeconds)(read_timeout)) >= 0 ? value : READ_TIMEOUT;
851
758
  this.keepAlive = typeof (value = agent?.keep_alive) === 'boolean' ? value : KEEP_ALIVE;
852
759
  this.acceptEncoding = typeof (value = use?.accept_encoding) === 'boolean' ? value : ACCEPT_ENCODING;
853
- if ((value = (0, util_1.asInt)(use?.http_version)) === 1 || value === 2) {
854
- this.#httpVersion = value;
855
- }
856
760
  this.#ipVersion = (value = (0, util_1.asInt)(data.dns?.family)) && (value === 4 || value === 6) ? value : 0;
857
761
  if ((value = (0, util_1.fromSeconds)(agent?.timeout)) >= 0) {
858
762
  this.#agentTimeout = value;
@@ -861,13 +765,14 @@ class Request extends module_1 {
861
765
  this.#agentTimeout = AGENT_TIMEOUT;
862
766
  value = undefined;
863
767
  }
768
+ if ((value = (0, util_1.asInt)(use?.http_version)) === 1 || value === 2) {
769
+ this.#httpVersion = value;
770
+ }
864
771
  const proxy = getProxySettings(data, value);
865
772
  if (proxy) {
866
773
  this.proxy = proxy;
867
774
  }
868
- if ((0, types_1.isObject)(headers)) {
869
- setOutgoingHeaders(this.#headers = {}, headers);
870
- }
775
+ this.parseHeaders(headers);
871
776
  if ((0, types_1.isObject)(certs)) {
872
777
  this.#certs = validateCerts(certs);
873
778
  }
@@ -890,8 +795,8 @@ class Request extends module_1 {
890
795
  this.readTimeout = READ_TIMEOUT;
891
796
  this.keepAlive = KEEP_ALIVE;
892
797
  this.acceptEncoding = ACCEPT_ENCODING;
893
- this.#agentTimeout = AGENT_TIMEOUT;
894
798
  this.#ipVersion = DNS.FAMILY;
799
+ this.#agentTimeout = AGENT_TIMEOUT;
895
800
  }
896
801
  this.module = data;
897
802
  }
@@ -975,9 +880,7 @@ class Request extends module_1 {
975
880
  init(config) {
976
881
  if (config) {
977
882
  const { headers, httpVersion, ipVersion, readTimeout } = config;
978
- if ((0, types_1.isObject)(headers)) {
979
- setOutgoingHeaders(this.#headers ||= {}, headers);
980
- }
883
+ this.parseHeaders(headers);
981
884
  if (httpVersion !== undefined) {
982
885
  this.httpVersion = httpVersion;
983
886
  }
@@ -1201,8 +1104,7 @@ class Request extends module_1 {
1201
1104
  }
1202
1105
  }
1203
1106
  headersOf(uri) {
1204
- const headers = this.#headers;
1205
- return headers && getBaseHeaders(uri, headers) || (this.host ? getBaseHeaders(uri, HTTP.HEADERS) : undefined);
1107
+ return this.findHeadersByUri(uri) || (this.host ? this.findHeadersByUri(uri, HTTP.HEADERS) : undefined);
1206
1108
  }
1207
1109
  async aria2c(uri, options = {}) {
1208
1110
  if (!ARIA2.BIN) {
@@ -1220,7 +1122,7 @@ class Request extends module_1 {
1220
1122
  ({ pathname, headers, binOpts } = this.parseBinOpts(options, ['--daemon'], ['--input-file']));
1221
1123
  }
1222
1124
  try {
1223
- if (typeof uri === 'string' && module_1.isURL(uri)) {
1125
+ if ((0, types_1.isString)(uri) && module_1.isURL(uri)) {
1224
1126
  uri = new URL(uri);
1225
1127
  }
1226
1128
  pathname = checkBinTarget(this, "aria2", uri, pathname, 'aria2', binOpts);
@@ -1346,12 +1248,12 @@ class Request extends module_1 {
1346
1248
  }
1347
1249
  }
1348
1250
  if (proxy) {
1349
- addAria2Proxy(args, protocol, proxy.host);
1251
+ appendAria2Proxy(args, protocol, proxy.host);
1350
1252
  }
1351
1253
  else if (ARIA2.PROXY.all) {
1352
- addAria2Proxy(opts, 'all', ARIA2.PROXY.all.host);
1254
+ appendAria2Proxy(opts, 'all', ARIA2.PROXY.all.host);
1353
1255
  }
1354
- args = finalizeBinArgs(this, "aria2", ARIA2.BIN, args, opts, binOpts);
1256
+ args = this.mergeBinOpts(args, opts, binOpts, { name: "aria2", bin: ARIA2.BIN });
1355
1257
  opts.push(`"${escapeShellQuote(uri)}"`);
1356
1258
  args = args.concat(init, opts);
1357
1259
  const startTime = Date.now();
@@ -1640,7 +1542,7 @@ class Request extends module_1 {
1640
1542
  setBinHeaders(args, headers);
1641
1543
  const cwd = module_1.isDir(pathname) ? pathname : path.dirname(pathname);
1642
1544
  const cmd = [source, pathname];
1643
- args = finalizeBinArgs(this, "rclone", RCLONE.BIN, args, opts, binOpts, cmd).concat(init, opts);
1545
+ args = this.mergeBinOpts(args, opts, binOpts, { name: "rclone", bin: RCLONE.BIN, cmd }).concat(init, opts);
1644
1546
  args.push(...cmd.map(value => (0, types_1.sanitizeCmd)(value)));
1645
1547
  args.unshift(command);
1646
1548
  const startTime = Date.now();
@@ -1739,7 +1641,7 @@ class Request extends module_1 {
1739
1641
  return { ...options, host, url };
1740
1642
  }
1741
1643
  open(uri, options) {
1742
- let { host, url, httpVersion, method = 'GET', search, encoding, format, headers: outgoing, socketPath, expectContinue = false, timeout = this._config.connectTimeout, outStream } = options, headers = (0, util_1.parseOutgoingHeaders)(outgoing), getting = false, posting = false;
1644
+ let { host, url, httpVersion, method = 'GET', search, encoding, format, socketPath, expectContinue = false, timeout = this._config.connectTimeout, outStream } = options, headers = (0, util_1.parseOutgoingHeaders)(options.headers), getting = false, posting = false;
1743
1645
  switch (method = method.toUpperCase()) {
1744
1646
  case 'GET':
1745
1647
  case 'DELETE':
@@ -1866,7 +1768,8 @@ class Request extends module_1 {
1866
1768
  }
1867
1769
  connected = true;
1868
1770
  if (this.matchStatus(statusCode, url, response, request, options) && hasResponse(statusCode)) {
1869
- if (emitter = checkEncoding(request, request, statusCode, outStream, response['content-encoding'])) {
1771
+ const contentEncoding = response['content-encoding'];
1772
+ if (checkEncoding(request, statusCode, contentEncoding) && (emitter = this.pipeline(request, contentEncoding, outStream))) {
1870
1773
  for (const event in listenerMap) {
1871
1774
  const [name, type] = event.split('-');
1872
1775
  for (const listener of listenerMap[event]) {
@@ -1942,7 +1845,7 @@ class Request extends module_1 {
1942
1845
  if (proxy) {
1943
1846
  keepAlive ??= proxy.keepAlive;
1944
1847
  agentTimeout ??= proxy.agentTimeout;
1945
- const proxyHeaders = this.#headers && getBaseHeaders(proxy.host.href, this.#headers) || getBaseHeaders(proxy.host.href, HTTP.HEADERS);
1848
+ const proxyHeaders = this.findHeadersByUri(proxy.host.href) || this.findHeadersByUri(proxy.host.href, HTTP.HEADERS);
1946
1849
  const pkg = secure ? 'https-proxy-agent' : 'http-proxy-agent';
1947
1850
  try {
1948
1851
  agent = require(pkg)(proxy.host, keepAlive === true || keepAlive === false && agentTimeout !== 0 || agentTimeout > 0 ? { ...agentOptions, keepAlive: keepAlive ?? true, timeout: agentTimeout, headers: proxyHeaders } : { ...agentOptions, headers: proxyHeaders });
@@ -1993,8 +1896,9 @@ class Request extends module_1 {
1993
1896
  const statusCode = response.statusCode;
1994
1897
  const incoming = response.headers;
1995
1898
  if (!expectContinue && this.matchStatus(statusCode, url, incoming, request, options) && (getting || posting) && hasResponse(statusCode)) {
1996
- let source = checkEncoding(request, response, statusCode, outStream, incoming['content-encoding']);
1997
- if (source) {
1899
+ const contentEncoding = incoming['content-encoding'];
1900
+ let source;
1901
+ if (checkEncoding(request, statusCode, contentEncoding) && (source = this.pipeline(response, contentEncoding, outStream))) {
1998
1902
  source.once('finish', () => {
1999
1903
  request.emit('end');
2000
1904
  });
@@ -2042,7 +1946,7 @@ class Request extends module_1 {
2042
1946
  if (version === 2 && incoming.upgrade?.includes('h2')) {
2043
1947
  host.version = 2;
2044
1948
  }
2045
- else if (!host.didAltSvc(1)) {
1949
+ else if (!host.altSvc.did(1)) {
2046
1950
  host.upgrade(1, incoming['alt-svc']);
2047
1951
  }
2048
1952
  }
@@ -2162,13 +2066,7 @@ class Request extends module_1 {
2162
2066
  else {
2163
2067
  options = {};
2164
2068
  }
2165
- const headers = (0, util_1.parseOutgoingHeaders)(options.headers) || {};
2166
- for (const attr in headers) {
2167
- const name = attr.toLowerCase();
2168
- if (name === 'content-type' || name === 'content-length') {
2169
- delete headers[attr];
2170
- }
2171
- }
2069
+ const headers = (0, util_1.parseOutgoingHeaders)(options.headers, 'content-type', 'content-length') || {};
2172
2070
  if (!putting && (parts || contentType === "multipart/form-data" || contentType === 'form-data')) {
2173
2071
  let valid = false;
2174
2072
  if ((0, types_1.isArray)(parts)) {
@@ -2196,7 +2094,7 @@ class Request extends module_1 {
2196
2094
  }
2197
2095
  }
2198
2096
  for (let { name, data: target, value, contentType: type, filename } of parts) {
2199
- if (!(0, types_1.isString)(name)) {
2097
+ if (!name) {
2200
2098
  continue;
2201
2099
  }
2202
2100
  if (target) {
@@ -2282,10 +2180,9 @@ class Request extends module_1 {
2282
2180
  }
2283
2181
  const singleton = this.#singleton;
2284
2182
  const verbose = !opts.silent && !singleton || opts.silent === false;
2285
- const log = verbose && LOG_HTTP && LOG_TIMEPROCESS;
2286
2183
  const state = Object.freeze({
2287
2184
  verbose,
2288
- log,
2185
+ log: verbose && LOG_HTTP && LOG_TIMEPROCESS,
2289
2186
  singleton,
2290
2187
  config: this._config
2291
2188
  });
@@ -2302,10 +2199,11 @@ class Request extends module_1 {
2302
2199
  this.#downloading.delete(ac);
2303
2200
  adapter.abortController = null;
2304
2201
  }
2305
- return;
2306
2202
  }
2307
- this.#pendingDns = Object.create(null);
2308
- this.#downloading.clear();
2203
+ else {
2204
+ this.#pendingDns = Object.create(null);
2205
+ this.#downloading.clear();
2206
+ }
2309
2207
  }
2310
2208
  close() {
2311
2209
  const session = this.#session;
@@ -2317,6 +2215,57 @@ class Request extends module_1 {
2317
2215
  });
2318
2216
  this.#downloading.clear();
2319
2217
  }
2218
+ pipeline(response, encoding, outStream) {
2219
+ const chunkSize = outStream?.writableHighWaterMark;
2220
+ let pipeTo;
2221
+ encoding = encoding.trim().toLowerCase();
2222
+ if (!encoding.includes(',')) {
2223
+ pipeTo = this.fromEncoding(encoding, { chunkSize });
2224
+ }
2225
+ else {
2226
+ for (const value of encoding.split(/\s*,\s*/).reverse()) {
2227
+ const next = this.fromEncoding(value, { chunkSize });
2228
+ if (!next) {
2229
+ return;
2230
+ }
2231
+ pipeTo = pipeTo ? pipeTo.pipe(next) : next;
2232
+ }
2233
+ }
2234
+ if (pipeTo) {
2235
+ if (outStream) {
2236
+ stream.pipeline(response, pipeTo, outStream, err => {
2237
+ if (err) {
2238
+ response.emit('error', err);
2239
+ }
2240
+ });
2241
+ }
2242
+ else {
2243
+ stream.pipeline(response, pipeTo, err => {
2244
+ if (err) {
2245
+ response.emit('error', err);
2246
+ }
2247
+ });
2248
+ }
2249
+ return pipeTo;
2250
+ }
2251
+ }
2252
+ fromEncoding(value, options) {
2253
+ switch (value) {
2254
+ case 'gzip':
2255
+ return zlib.createGunzip(options);
2256
+ case 'br':
2257
+ return zlib.createBrotliDecompress(options);
2258
+ case 'deflate':
2259
+ return zlib.createInflate(options);
2260
+ case 'deflate-raw':
2261
+ return zlib.createInflateRaw(options);
2262
+ case 'zstd':
2263
+ if (SUPPORTED_ZSTD) {
2264
+ return zlib.createZstdDecompress(options);
2265
+ }
2266
+ break;
2267
+ }
2268
+ }
2320
2269
  matchStatus(code, url, headers, request, options) {
2321
2270
  const status = this.#statusOn?.get(code);
2322
2271
  if (status) {
@@ -2395,7 +2344,7 @@ class Request extends module_1 {
2395
2344
  }
2396
2345
  if ((0, types_1.isArray)(options.binOpts)) {
2397
2346
  let next = false;
2398
- binOpts = options.binOpts.filter((opt) => !((0, types_1.isString)(opt) && /^-[a-z].*$/i.test(opt.trim()))).map((opt) => {
2347
+ binOpts = options.binOpts.filter((opt) => !((0, types_1.isString)(opt) && /^-[a-z].*$/i.test(opt))).map((opt) => {
2399
2348
  if (next) {
2400
2349
  if (!module_1.asString(opt).startsWith('--')) {
2401
2350
  return [];
@@ -2445,6 +2394,58 @@ class Request extends module_1 {
2445
2394
  }
2446
2395
  return { pathname, headers: (0, util_1.parseOutgoingHeaders)(options.headers), binOpts };
2447
2396
  }
2397
+ mergeBinOpts(args, opts, binOpts, options) {
2398
+ if (binOpts) {
2399
+ for (const leading of binOpts) {
2400
+ if (leading.charAt(0) === '-') {
2401
+ const pattern = new RegExp(`^${leading}(?:=|$)`);
2402
+ for (let i = 0; i < opts.length; ++i) {
2403
+ if (pattern.test(opts[i])) {
2404
+ opts.splice(i--, i);
2405
+ break;
2406
+ }
2407
+ }
2408
+ }
2409
+ }
2410
+ args = binOpts.concat(args);
2411
+ }
2412
+ if (options && args.length > 0) {
2413
+ const { name, bin, cmd = [] } = options;
2414
+ if (module_1.hasLogType(32768)) {
2415
+ this.formatMessage(32768, name.toUpperCase(), [bin].concat(cmd).join(' '), args.join(' '), { ...module_1.LOG_STYLE_WARN });
2416
+ }
2417
+ else {
2418
+ this.addLog(4, path.basename(bin) + ' ' + args.join(' '), name);
2419
+ }
2420
+ }
2421
+ return args;
2422
+ }
2423
+ parseHeaders(outgoing) {
2424
+ if ((0, types_1.isPlainObject)(outgoing)) {
2425
+ Object.assign(this.#headers ||= {}, outgoing);
2426
+ }
2427
+ }
2428
+ findHeadersByUri(uri, outgoing = this.#headers) {
2429
+ if (outgoing) {
2430
+ const data = [];
2431
+ uri = (0, util_1.trimPath)(uri);
2432
+ for (const pathname in outgoing) {
2433
+ if (pathname === uri || uri.startsWith(pathname + '/')) {
2434
+ data.push([pathname, outgoing[pathname]]);
2435
+ }
2436
+ }
2437
+ if (data.length > 0) {
2438
+ data.sort((a, b) => b[0].length - a[0].length);
2439
+ const headers = data[0][1];
2440
+ let result = HTTP.CACHE.get(headers);
2441
+ if (!result) {
2442
+ result = (0, util_1.normalizeHeaders)(headers);
2443
+ HTTP.CACHE.set(headers, result);
2444
+ }
2445
+ return result;
2446
+ }
2447
+ }
2448
+ }
2448
2449
  set adapter(value) {
2449
2450
  if (adapter_1.constructorOf(value)) {
2450
2451
  this.#adapter = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e-mc/request",
3
- "version": "0.13.5",
3
+ "version": "0.13.7",
4
4
  "description": "Request constructor for E-mc.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -19,8 +19,8 @@
19
19
  "license": "BSD-3-Clause",
20
20
  "homepage": "https://github.com/anpham6/e-mc#readme",
21
21
  "dependencies": {
22
- "@e-mc/module": "0.13.5",
23
- "@e-mc/types": "0.13.5",
22
+ "@e-mc/module": "0.13.7",
23
+ "@e-mc/types": "0.13.7",
24
24
  "combined-stream": "^1.0.8",
25
25
  "js-yaml": "^4.1.1",
26
26
  "picomatch": "^4.0.3",
package/util.d.ts CHANGED
@@ -6,15 +6,15 @@ import type { Readable, Writable } from 'node:stream';
6
6
 
7
7
  declare namespace util {
8
8
  function parseHeader<T = unknown>(headers: IncomingHttpHeaders, name: string): T | undefined;
9
- function parseOutgoingHeaders(headers: OutgoingHttpHeaders | Headers | undefined): OutgoingHttpHeaders | undefined;
10
- function normalizeHeaders(headers: OutgoingHttpHeaders): OutgoingHttpHeaders;
9
+ function parseOutgoingHeaders(headers: OutgoingHttpHeaders | Headers | undefined, ...ignore: string[]): OutgoingHttpHeaders | undefined;
10
+ function normalizeHeaders(headers: OutgoingHttpHeaders | Headers): OutgoingHttpHeaders;
11
11
  function getBasicAuth(auth: AuthValue): string;
12
12
  function getBasicAuth(username: unknown, password?: unknown): string;
13
13
  function hasBasicAuth(value: string): boolean;
14
14
  function checkRetryable(err: unknown): boolean;
15
15
  function isRetryable(value: number, timeout?: boolean): boolean;
16
16
  function parseHttpProxy(value?: string): HttpProxySettings | undefined;
17
- function trimPath(value: string): string;
17
+ function trimPath(value: string, char?: string): string;
18
18
  function asInt(value: unknown): number;
19
19
  function asFloat(value: unknown): number;
20
20
  function fromSeconds(value: unknown): number;
package/util.js CHANGED
@@ -25,45 +25,63 @@ const node_util_1 = require("node:util");
25
25
  const types_1 = require("@e-mc/types");
26
26
  const module_1 = require("@e-mc/module");
27
27
  const host_1 = require("@e-mc/request/http/host");
28
+ function setHeader(result, key, value) {
29
+ if (key === 'set-cookie') {
30
+ (result[key] ||= []).push(value);
31
+ }
32
+ else {
33
+ result[key] = value;
34
+ }
35
+ }
28
36
  const safeInt = (value) => value >= 0 ? Math.min(value, Number.MAX_SAFE_INTEGER) : NaN;
29
37
  function parseHeader(headers, name) {
30
38
  const value = headers[name];
31
- if (!value) {
32
- return;
33
- }
34
39
  switch (name.toLowerCase()) {
35
- case 'content-disposition': {
36
- let result;
37
- for (const match of value.matchAll(/\bfilename(?:\*\s*=\s*UTF-8''([^\s;]+)|\s*=\s*(?:"([^"]+)"|([^\s;]+)))/gi)) {
38
- if (match[1]) {
39
- return decodeURIComponent(match[1]).trim();
40
+ case 'content-disposition':
41
+ if ((0, types_1.isString)(value)) {
42
+ let result;
43
+ for (const match of value.matchAll(/\bfilename(?:\*\s*=\s*UTF-8''([^\s;]+)|\s*=\s*(?:"([^"]+)"|([^\s;]+)))/gi)) {
44
+ if (match[1]) {
45
+ return decodeURIComponent(match[1]).trim();
46
+ }
47
+ result = (match[2] || match[3]).trim();
40
48
  }
41
- result = (match[2] || match[3]).trim();
49
+ return result;
42
50
  }
43
- return result;
44
- }
51
+ break;
45
52
  }
46
53
  }
47
- function parseOutgoingHeaders(headers) {
54
+ function parseOutgoingHeaders(headers, ...ignore) {
48
55
  if (!headers) {
49
56
  return;
50
57
  }
51
58
  if (headers instanceof Headers) {
52
- const result = {};
59
+ const result = Object.create(null);
53
60
  headers.forEach((value, key) => {
54
- if (key === 'set-cookie') {
55
- (result[key] ||= []).push(value);
56
- }
57
- else {
58
- result[key] = value;
61
+ if (!ignore.includes(key)) {
62
+ setHeader(result, key, value);
59
63
  }
60
64
  });
61
65
  return result;
62
66
  }
67
+ if (ignore.length > 0) {
68
+ const result = {};
69
+ for (const attr in headers) {
70
+ const name = attr.toLowerCase();
71
+ if (!ignore.includes(name)) {
72
+ result[name] = headers[attr];
73
+ }
74
+ }
75
+ return result;
76
+ }
63
77
  return { ...headers };
64
78
  }
65
79
  function normalizeHeaders(headers) {
66
80
  const result = Object.create(null);
81
+ if (headers instanceof Headers) {
82
+ headers.forEach((value, key) => setHeader(result, key, value));
83
+ return result;
84
+ }
67
85
  for (const name in headers) {
68
86
  let value = headers[name];
69
87
  switch (typeof value) {
@@ -182,9 +200,11 @@ function parseHttpProxy(value, ignoreEnv) {
182
200
  }
183
201
  }
184
202
  }
185
- function trimPath(value) {
186
- const length = value.length - 1;
187
- return value[length] === '/' ? value.substring(0, length) : value;
203
+ function trimPath(value, char = '/') {
204
+ while (value.at(-1) === char) {
205
+ value = value.slice(0, -1);
206
+ }
207
+ return value;
188
208
  }
189
209
  function asInt(value) {
190
210
  switch (typeof value) {