rezo 1.0.41 → 1.0.43

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.
Files changed (68) hide show
  1. package/dist/adapters/curl.cjs +143 -32
  2. package/dist/adapters/curl.js +143 -32
  3. package/dist/adapters/entries/curl.d.ts +65 -0
  4. package/dist/adapters/entries/fetch.d.ts +65 -0
  5. package/dist/adapters/entries/http.d.ts +65 -0
  6. package/dist/adapters/entries/http2.d.ts +65 -0
  7. package/dist/adapters/entries/react-native.d.ts +65 -0
  8. package/dist/adapters/entries/xhr.d.ts +65 -0
  9. package/dist/adapters/fetch.cjs +98 -12
  10. package/dist/adapters/fetch.js +98 -12
  11. package/dist/adapters/http.cjs +26 -14
  12. package/dist/adapters/http.js +26 -14
  13. package/dist/adapters/http2.cjs +756 -227
  14. package/dist/adapters/http2.js +756 -227
  15. package/dist/adapters/index.cjs +6 -6
  16. package/dist/adapters/xhr.cjs +94 -2
  17. package/dist/adapters/xhr.js +94 -2
  18. package/dist/cache/dns-cache.cjs +5 -3
  19. package/dist/cache/dns-cache.js +5 -3
  20. package/dist/cache/file-cacher.cjs +7 -1
  21. package/dist/cache/file-cacher.js +7 -1
  22. package/dist/cache/index.cjs +15 -13
  23. package/dist/cache/index.js +1 -0
  24. package/dist/cache/navigation-history.cjs +298 -0
  25. package/dist/cache/navigation-history.js +296 -0
  26. package/dist/cache/url-store.cjs +7 -1
  27. package/dist/cache/url-store.js +7 -1
  28. package/dist/core/rezo.cjs +7 -0
  29. package/dist/core/rezo.js +7 -0
  30. package/dist/crawler.d.ts +196 -11
  31. package/dist/entries/crawler.cjs +5 -5
  32. package/dist/index.cjs +27 -24
  33. package/dist/index.d.ts +73 -0
  34. package/dist/index.js +1 -0
  35. package/dist/internal/agents/base.cjs +113 -0
  36. package/dist/internal/agents/base.js +110 -0
  37. package/dist/internal/agents/http-proxy.cjs +89 -0
  38. package/dist/internal/agents/http-proxy.js +86 -0
  39. package/dist/internal/agents/https-proxy.cjs +176 -0
  40. package/dist/internal/agents/https-proxy.js +173 -0
  41. package/dist/internal/agents/index.cjs +10 -0
  42. package/dist/internal/agents/index.js +5 -0
  43. package/dist/internal/agents/socks-client.cjs +571 -0
  44. package/dist/internal/agents/socks-client.js +567 -0
  45. package/dist/internal/agents/socks-proxy.cjs +75 -0
  46. package/dist/internal/agents/socks-proxy.js +72 -0
  47. package/dist/platform/browser.d.ts +65 -0
  48. package/dist/platform/bun.d.ts +65 -0
  49. package/dist/platform/deno.d.ts +65 -0
  50. package/dist/platform/node.d.ts +65 -0
  51. package/dist/platform/react-native.d.ts +65 -0
  52. package/dist/platform/worker.d.ts +65 -0
  53. package/dist/plugin/crawler-options.cjs +1 -1
  54. package/dist/plugin/crawler-options.js +1 -1
  55. package/dist/plugin/crawler.cjs +192 -1
  56. package/dist/plugin/crawler.js +192 -1
  57. package/dist/plugin/index.cjs +36 -36
  58. package/dist/proxy/index.cjs +18 -16
  59. package/dist/proxy/index.js +17 -12
  60. package/dist/queue/index.cjs +8 -8
  61. package/dist/responses/buildError.cjs +11 -2
  62. package/dist/responses/buildError.js +11 -2
  63. package/dist/responses/universal/index.cjs +11 -11
  64. package/dist/utils/agent-pool.cjs +1 -17
  65. package/dist/utils/agent-pool.js +1 -17
  66. package/dist/utils/curl.cjs +317 -0
  67. package/dist/utils/curl.js +314 -0
  68. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import * as http2 from "node:http2";
2
+ import * as tls from "node:tls";
2
3
  import * as zlib from "node:zlib";
3
4
  import { URL } from "node:url";
4
5
  import { Readable } from "node:stream";
@@ -14,9 +15,105 @@ import { DownloadResponse } from '../responses/download.js';
14
15
  import { UploadResponse } from '../responses/upload.js';
15
16
  import { CompressionUtil } from '../utils/compression.js';
16
17
  import { isSameDomain, RezoPerformance } from '../utils/tools.js';
18
+ import { SocksClient } from '../internal/agents/socks-client.js';
19
+ import * as net from "node:net";
17
20
  import { ResponseCache } from '../cache/response-cache.js';
18
21
  let zstdDecompressSync = null;
19
22
  let zstdChecked = false;
23
+ const debugLog = {
24
+ requestStart: (config, url, method) => {
25
+ if (config.debug) {
26
+ console.log(`
27
+ [Rezo Debug] ─────────────────────────────────────`);
28
+ console.log(`[Rezo Debug] ${method} ${url}`);
29
+ console.log(`[Rezo Debug] Request ID: ${config.requestId}`);
30
+ if (config.originalRequest?.headers) {
31
+ const headers = config.originalRequest.headers instanceof RezoHeaders ? config.originalRequest.headers.toObject() : config.originalRequest.headers;
32
+ console.log(`[Rezo Debug] Request Headers:`, JSON.stringify(headers, null, 2));
33
+ }
34
+ if (config.proxy && typeof config.proxy === "object") {
35
+ console.log(`[Rezo Debug] Proxy: ${config.proxy.protocol}://${config.proxy.host}:${config.proxy.port}`);
36
+ } else if (config.proxy && typeof config.proxy === "string") {
37
+ console.log(`[Rezo Debug] Proxy: ${config.proxy}`);
38
+ }
39
+ }
40
+ if (config.trackUrl) {
41
+ console.log(`[Rezo Track] → ${method} ${url}`);
42
+ }
43
+ },
44
+ redirect: (config, fromUrl, toUrl, statusCode, method) => {
45
+ if (config.debug) {
46
+ console.log(`[Rezo Debug] Redirect ${statusCode}: ${fromUrl}`);
47
+ console.log(`[Rezo Debug] → ${toUrl} (${method})`);
48
+ }
49
+ if (config.trackUrl) {
50
+ console.log(`[Rezo Track] ↳ ${statusCode} → ${toUrl}`);
51
+ }
52
+ },
53
+ retry: (config, attempt, maxRetries, statusCode, delay) => {
54
+ if (config.debug) {
55
+ console.log(`[Rezo Debug] Retry ${attempt}/${maxRetries} after status ${statusCode}${delay > 0 ? ` (waiting ${delay}ms)` : ""}`);
56
+ }
57
+ if (config.trackUrl) {
58
+ console.log(`[Rezo Track] ⟳ Retry ${attempt}/${maxRetries} (status ${statusCode})`);
59
+ }
60
+ },
61
+ maxRetries: (config, maxRetries) => {
62
+ if (config.debug) {
63
+ console.log(`[Rezo Debug] Max retries (${maxRetries}) reached, throwing error`);
64
+ }
65
+ if (config.trackUrl) {
66
+ console.log(`[Rezo Track] ✗ Max retries reached`);
67
+ }
68
+ },
69
+ response: (config, status, statusText, duration) => {
70
+ if (config.debug) {
71
+ console.log(`[Rezo Debug] Response: ${status} ${statusText} (${duration.toFixed(2)}ms)`);
72
+ }
73
+ if (config.trackUrl) {
74
+ console.log(`[Rezo Track] ✓ ${status} ${statusText}`);
75
+ }
76
+ },
77
+ responseHeaders: (config, headers) => {
78
+ if (config.debug) {
79
+ console.log(`[Rezo Debug] Response Headers:`, JSON.stringify(headers, null, 2));
80
+ }
81
+ },
82
+ cookies: (config, cookieCount) => {
83
+ if (config.debug && cookieCount > 0) {
84
+ console.log(`[Rezo Debug] Cookies received: ${cookieCount}`);
85
+ }
86
+ },
87
+ timing: (config, timing) => {
88
+ if (config.debug) {
89
+ const parts = [];
90
+ if (timing.dns)
91
+ parts.push(`DNS: ${timing.dns.toFixed(2)}ms`);
92
+ if (timing.connect)
93
+ parts.push(`Connect: ${timing.connect.toFixed(2)}ms`);
94
+ if (timing.tls)
95
+ parts.push(`TLS: ${timing.tls.toFixed(2)}ms`);
96
+ if (timing.ttfb)
97
+ parts.push(`TTFB: ${timing.ttfb.toFixed(2)}ms`);
98
+ if (timing.total)
99
+ parts.push(`Total: ${timing.total.toFixed(2)}ms`);
100
+ if (parts.length > 0) {
101
+ console.log(`[Rezo Debug] Timing: ${parts.join(" | ")}`);
102
+ }
103
+ }
104
+ },
105
+ complete: (config, finalUrl, redirectCount, duration) => {
106
+ if (config.debug) {
107
+ console.log(`[Rezo Debug] Complete: ${finalUrl}`);
108
+ if (redirectCount > 0) {
109
+ console.log(`[Rezo Debug] Redirects: ${redirectCount}`);
110
+ }
111
+ console.log(`[Rezo Debug] Total Duration: ${duration.toFixed(2)}ms`);
112
+ console.log(`[Rezo Debug] ─────────────────────────────────────
113
+ `);
114
+ }
115
+ }
116
+ };
20
117
  async function decompressBuffer(buffer, contentEncoding) {
21
118
  const encoding = contentEncoding.toLowerCase();
22
119
  switch (encoding) {
@@ -100,40 +197,67 @@ class Http2SessionPool {
100
197
  this.cleanupInterval.unref();
101
198
  }
102
199
  }
103
- getSessionKey(url, options) {
104
- return `${url.protocol}//${url.host}`;
200
+ getSessionKey(url, options, proxy) {
201
+ const proxyKey = proxy ? typeof proxy === "string" ? proxy : `${proxy.protocol}://${proxy.host}:${proxy.port}` : "";
202
+ return `${url.protocol}//${url.host}${proxyKey ? `@${proxyKey}` : ""}`;
105
203
  }
106
- async getSession(url, options, timeout) {
107
- const key = this.getSessionKey(url, options);
204
+ isSessionHealthy(session, entry) {
205
+ if (session.closed || session.destroyed)
206
+ return false;
207
+ if (entry.goawayReceived)
208
+ return false;
209
+ const socket = session.socket;
210
+ if (socket && (socket.destroyed || socket.closed || !socket.writable))
211
+ return false;
212
+ return true;
213
+ }
214
+ async getSession(url, options, timeout, forceNew = false, proxy) {
215
+ const key = this.getSessionKey(url, options, proxy);
108
216
  const existing = this.sessions.get(key);
109
- if (existing && !existing.session.closed && !existing.session.destroyed) {
217
+ if (!forceNew && existing && this.isSessionHealthy(existing.session, existing)) {
110
218
  existing.lastUsed = Date.now();
111
219
  existing.refCount++;
112
220
  return existing.session;
113
221
  }
114
- const session = await this.createSession(url, options, timeout);
115
- this.sessions.set(key, {
222
+ if (existing && !this.isSessionHealthy(existing.session, existing)) {
223
+ try {
224
+ existing.session.close();
225
+ } catch {}
226
+ this.sessions.delete(key);
227
+ }
228
+ const session = await this.createSession(url, options, timeout, proxy);
229
+ const entry = {
116
230
  session,
117
231
  lastUsed: Date.now(),
118
- refCount: 1
119
- });
232
+ refCount: 1,
233
+ goawayReceived: false,
234
+ proxy
235
+ };
236
+ this.sessions.set(key, entry);
120
237
  session.on("close", () => {
121
238
  this.sessions.delete(key);
122
239
  });
123
240
  session.on("error", () => {
124
241
  this.sessions.delete(key);
125
242
  });
243
+ session.on("goaway", () => {
244
+ entry.goawayReceived = true;
245
+ });
126
246
  return session;
127
247
  }
128
- createSession(url, options, timeout) {
248
+ async createSession(url, options, timeout, proxy) {
249
+ const authority = `${url.protocol}//${url.host}`;
250
+ const sessionOptions = {
251
+ ...options,
252
+ rejectUnauthorized: options?.rejectUnauthorized !== false,
253
+ ALPNProtocols: ["h2", "http/1.1"],
254
+ timeout
255
+ };
256
+ if (proxy) {
257
+ const tunnelSocket = await this.createProxyTunnel(url, proxy, timeout, options?.rejectUnauthorized);
258
+ sessionOptions.createConnection = () => tunnelSocket;
259
+ }
129
260
  return new Promise((resolve, reject) => {
130
- const authority = `${url.protocol}//${url.host}`;
131
- const sessionOptions = {
132
- ...options,
133
- rejectUnauthorized: options?.rejectUnauthorized !== false,
134
- ALPNProtocols: ["h2", "http/1.1"],
135
- timeout
136
- };
137
261
  const session = http2.connect(authority, sessionOptions);
138
262
  let settled = false;
139
263
  const timeoutId = timeout ? setTimeout(() => {
@@ -164,8 +288,186 @@ class Http2SessionPool {
164
288
  });
165
289
  });
166
290
  }
167
- releaseSession(url) {
168
- const key = this.getSessionKey(url);
291
+ async createProxyTunnel(url, proxy, timeout, rejectUnauthorized) {
292
+ return new Promise((resolve, reject) => {
293
+ let proxyUrl;
294
+ let proxyAuth;
295
+ if (typeof proxy === "string") {
296
+ proxyUrl = new URL(proxy);
297
+ if (proxyUrl.username || proxyUrl.password) {
298
+ proxyAuth = Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString("base64");
299
+ }
300
+ } else {
301
+ const protocol = proxy.protocol || "http";
302
+ let proxyUrlStr = `${protocol}://${proxy.host}:${proxy.port}`;
303
+ if (proxy.auth) {
304
+ const encodedUser = encodeURIComponent(proxy.auth.username);
305
+ const encodedPass = encodeURIComponent(proxy.auth.password);
306
+ proxyUrlStr = `${protocol}://${encodedUser}:${encodedPass}@${proxy.host}:${proxy.port}`;
307
+ proxyAuth = Buffer.from(`${proxy.auth.username}:${proxy.auth.password}`).toString("base64");
308
+ }
309
+ proxyUrl = new URL(proxyUrlStr);
310
+ }
311
+ const targetHost = url.hostname;
312
+ const targetPort = url.port || (url.protocol === "https:" ? "443" : "80");
313
+ if (proxyUrl.protocol.startsWith("socks")) {
314
+ const socksType = proxyUrl.protocol === "socks5:" || proxyUrl.protocol === "socks5h:" ? 5 : 4;
315
+ const socksOpts = {
316
+ proxy: {
317
+ host: proxyUrl.hostname,
318
+ port: parseInt(proxyUrl.port || "1080", 10),
319
+ type: socksType,
320
+ userId: proxyUrl.username ? decodeURIComponent(proxyUrl.username) : undefined,
321
+ password: proxyUrl.password ? decodeURIComponent(proxyUrl.password) : undefined
322
+ },
323
+ destination: {
324
+ host: targetHost,
325
+ port: parseInt(targetPort, 10)
326
+ },
327
+ command: "connect",
328
+ timeout
329
+ };
330
+ SocksClient.createConnection(socksOpts).then(({ socket }) => {
331
+ if (url.protocol === "https:") {
332
+ const tlsSocket = tls.connect({
333
+ socket,
334
+ host: targetHost,
335
+ servername: targetHost,
336
+ rejectUnauthorized: rejectUnauthorized !== false,
337
+ ALPNProtocols: ["h2", "http/1.1"]
338
+ });
339
+ const tlsTimeoutId = timeout ? setTimeout(() => {
340
+ tlsSocket.destroy();
341
+ reject(new Error(`TLS handshake timeout after ${timeout}ms`));
342
+ }, timeout) : null;
343
+ tlsSocket.on("secureConnect", () => {
344
+ if (tlsTimeoutId)
345
+ clearTimeout(tlsTimeoutId);
346
+ const alpn = tlsSocket.alpnProtocol;
347
+ if (alpn && alpn !== "h2") {
348
+ tlsSocket.destroy();
349
+ reject(new Error(`Server does not support HTTP/2 (negotiated: ${alpn})`));
350
+ return;
351
+ }
352
+ resolve(tlsSocket);
353
+ });
354
+ tlsSocket.on("error", (err) => {
355
+ if (tlsTimeoutId)
356
+ clearTimeout(tlsTimeoutId);
357
+ reject(new Error(`TLS handshake failed: ${err.message}`));
358
+ });
359
+ } else {
360
+ resolve(socket);
361
+ }
362
+ }).catch((err) => {
363
+ reject(new Error(`SOCKS proxy connection failed: ${err.message}`));
364
+ });
365
+ return;
366
+ }
367
+ const proxyHost = proxyUrl.hostname;
368
+ const proxyPort = parseInt(proxyUrl.port || (proxyUrl.protocol === "https:" ? "443" : "80"), 10);
369
+ let proxySocket;
370
+ const connectToProxy = () => {
371
+ if (proxyUrl.protocol === "https:") {
372
+ proxySocket = tls.connect({
373
+ host: proxyHost,
374
+ port: proxyPort,
375
+ rejectUnauthorized: rejectUnauthorized !== false
376
+ });
377
+ } else {
378
+ proxySocket = net.connect({
379
+ host: proxyHost,
380
+ port: proxyPort
381
+ });
382
+ }
383
+ let settled = false;
384
+ const timeoutId = timeout ? setTimeout(() => {
385
+ if (!settled) {
386
+ settled = true;
387
+ proxySocket.destroy();
388
+ reject(new Error(`Proxy connection timeout after ${timeout}ms`));
389
+ }
390
+ }, timeout) : null;
391
+ proxySocket.on("error", (err) => {
392
+ if (!settled) {
393
+ settled = true;
394
+ if (timeoutId)
395
+ clearTimeout(timeoutId);
396
+ reject(new Error(`Proxy connection error: ${err.message}`));
397
+ }
398
+ });
399
+ proxySocket.on("connect", () => {
400
+ const connectRequest = [
401
+ `CONNECT ${targetHost}:${targetPort} HTTP/1.1`,
402
+ `Host: ${targetHost}:${targetPort}`,
403
+ proxyAuth ? `Proxy-Authorization: Basic ${proxyAuth}` : "",
404
+ "",
405
+ ""
406
+ ].filter(Boolean).join(`\r
407
+ `);
408
+ proxySocket.write(connectRequest);
409
+ });
410
+ let responseBuffer = "";
411
+ proxySocket.on("data", function onData(data) {
412
+ if (settled)
413
+ return;
414
+ responseBuffer += data.toString();
415
+ const headerEnd = responseBuffer.indexOf(`\r
416
+ \r
417
+ `);
418
+ if (headerEnd !== -1) {
419
+ settled = true;
420
+ if (timeoutId)
421
+ clearTimeout(timeoutId);
422
+ proxySocket.removeListener("data", onData);
423
+ const statusLine = responseBuffer.split(`\r
424
+ `)[0];
425
+ const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d{3})/);
426
+ const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 0;
427
+ if (statusCode === 200) {
428
+ if (url.protocol === "https:") {
429
+ const tlsSocket = tls.connect({
430
+ socket: proxySocket,
431
+ host: targetHost,
432
+ servername: targetHost,
433
+ rejectUnauthorized: rejectUnauthorized !== false,
434
+ ALPNProtocols: ["h2", "http/1.1"]
435
+ });
436
+ const tlsTimeoutId = timeout ? setTimeout(() => {
437
+ tlsSocket.destroy();
438
+ reject(new Error(`TLS handshake timeout after ${timeout}ms`));
439
+ }, timeout) : null;
440
+ tlsSocket.on("secureConnect", () => {
441
+ if (tlsTimeoutId)
442
+ clearTimeout(tlsTimeoutId);
443
+ const alpn = tlsSocket.alpnProtocol;
444
+ if (alpn && alpn !== "h2") {
445
+ tlsSocket.destroy();
446
+ reject(new Error(`Server does not support HTTP/2 (negotiated: ${alpn})`));
447
+ return;
448
+ }
449
+ resolve(tlsSocket);
450
+ });
451
+ tlsSocket.on("error", (err) => {
452
+ if (tlsTimeoutId)
453
+ clearTimeout(tlsTimeoutId);
454
+ reject(new Error(`TLS handshake failed: ${err.message}`));
455
+ });
456
+ } else {
457
+ resolve(proxySocket);
458
+ }
459
+ } else {
460
+ proxySocket.destroy();
461
+ reject(new Error(`Proxy CONNECT failed with status ${statusCode}: ${statusLine}`));
462
+ }
463
+ }
464
+ });
465
+ };
466
+ connectToProxy();
467
+ });
468
+ }
469
+ releaseSession(url, proxy) {
470
+ const key = this.getSessionKey(url, undefined, proxy);
169
471
  const entry = this.sessions.get(key);
170
472
  if (entry) {
171
473
  entry.refCount = Math.max(0, entry.refCount - 1);
@@ -178,8 +480,8 @@ class Http2SessionPool {
178
480
  }
179
481
  }
180
482
  }
181
- closeSession(url) {
182
- const key = this.getSessionKey(url);
483
+ closeSession(url, proxy) {
484
+ const key = this.getSessionKey(url, undefined, proxy);
183
485
  const entry = this.sessions.get(key);
184
486
  if (entry) {
185
487
  entry.session.close();
@@ -308,7 +610,7 @@ function sanitizeConfig(config) {
308
610
  const { data: _data, ...sanitized } = config;
309
611
  return sanitized;
310
612
  }
311
- async function updateCookies(config, headers, url) {
613
+ async function updateCookies(config, headers, url, rootJar) {
312
614
  const setCookieHeaders = headers["set-cookie"];
313
615
  if (!setCookieHeaders)
314
616
  return;
@@ -352,8 +654,9 @@ async function updateCookies(config, headers, url) {
352
654
  const acceptedCookieStrings = acceptedCookies.map((c) => c.toSetCookieString());
353
655
  const jar = new RezoCookieJar;
354
656
  jar.setCookiesSync(acceptedCookieStrings, url);
355
- if (!config.disableCookieJar && config.cookieJar) {
356
- config.cookieJar.setCookiesSync(acceptedCookieStrings, url);
657
+ const jarToSync = rootJar || config.cookieJar;
658
+ if (!config.disableCookieJar && jarToSync) {
659
+ jarToSync.setCookiesSync(acceptedCookieStrings, url);
357
660
  }
358
661
  const cookies = jar.cookies();
359
662
  cookies.setCookiesString = cookieHeaderArray;
@@ -525,7 +828,7 @@ export async function executeRequest(options, defaultOptions, jar) {
525
828
  }
526
829
  }
527
830
  try {
528
- const res = executeHttp2Request(fetchOptions, mainConfig, options, perform, fs, streamResponse, downloadResponse, uploadResponse);
831
+ const res = executeHttp2Request(fetchOptions, mainConfig, options, perform, fs, streamResponse, downloadResponse, uploadResponse, jar);
529
832
  if (streamResponse) {
530
833
  return streamResponse;
531
834
  } else if (downloadResponse) {
@@ -558,7 +861,7 @@ export async function executeRequest(options, defaultOptions, jar) {
558
861
  throw error;
559
862
  }
560
863
  }
561
- async function executeHttp2Request(fetchOptions, config, options, perform, fs, streamResult, downloadResult, uploadResult) {
864
+ async function executeHttp2Request(fetchOptions, config, options, perform, fs, streamResult, downloadResult, uploadResult, rootJar) {
562
865
  let requestCount = 0;
563
866
  const _stats = { statusOnNext: "abort" };
564
867
  let responseStatusCode;
@@ -578,6 +881,11 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
578
881
  config.setSignal();
579
882
  const timeoutClearInstance = config.timeoutClearInstanse;
580
883
  delete config.timeoutClearInstanse;
884
+ if (!config.requestId) {
885
+ config.requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
886
+ }
887
+ const requestUrl = fetchOptions.fullUrl ? String(fetchOptions.fullUrl) : "";
888
+ debugLog.requestStart(config, requestUrl, fetchOptions.method || "GET");
581
889
  const eventEmitter = streamResult || downloadResult || uploadResult;
582
890
  if (eventEmitter) {
583
891
  eventEmitter.emit("initiated");
@@ -590,7 +898,7 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
590
898
  throw error;
591
899
  }
592
900
  try {
593
- const response = await executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool);
901
+ const response = await executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool, rootJar);
594
902
  const statusOnNext = _stats.statusOnNext;
595
903
  if (response instanceof RezoError) {
596
904
  const fileName = config.fileName;
@@ -619,16 +927,12 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
619
927
  throw response;
620
928
  }
621
929
  if (maxRetries <= retries) {
622
- if (config.debug) {
623
- console.log(`Max retries (${maxRetries}) reached`);
624
- }
930
+ debugLog.maxRetries(config, maxRetries);
625
931
  throw response;
626
932
  }
627
933
  retries++;
628
934
  const currentDelay = incrementDelay ? retryDelay * retries : retryDelay;
629
- if (config.debug) {
630
- console.log(`Retrying... ${retryDelay > 0 ? "in " + currentDelay + "ms" : ""}`);
631
- }
935
+ debugLog.retry(config, retries, maxRetries, responseStatusCode || 0, currentDelay);
632
936
  if (config.hooks?.beforeRetry && config.hooks.beforeRetry.length > 0) {
633
937
  for (const hook of config.hooks.beforeRetry) {
634
938
  await hook(config, response, retries);
@@ -643,6 +947,16 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
643
947
  continue;
644
948
  }
645
949
  if (statusOnNext === "success") {
950
+ const totalDuration = performance.now() - timing.startTime;
951
+ debugLog.response(config, response.status, response.statusText, totalDuration);
952
+ if (response.headers) {
953
+ const headersObj = response.headers instanceof RezoHeaders ? response.headers.toObject() : response.headers;
954
+ debugLog.responseHeaders(config, headersObj);
955
+ }
956
+ if (response.cookies?.array) {
957
+ debugLog.cookies(config, response.cookies.array.length);
958
+ }
959
+ debugLog.complete(config, response.finalUrl || requestUrl, config.redirectCount, totalDuration);
646
960
  return response;
647
961
  }
648
962
  if (statusOnNext === "error") {
@@ -686,18 +1000,20 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
686
1000
  visitedUrls.add(normalizedRedirectUrl);
687
1001
  }
688
1002
  const redirectCode = response.status;
1003
+ const fromUrl = fetchOptions.fullUrl;
689
1004
  const redirectCallback = config.beforeRedirect || config.onRedirect;
690
1005
  const onRedirect = redirectCallback ? redirectCallback({
691
1006
  url: new URL(location),
692
1007
  status: response.status,
693
1008
  headers: response.headers,
694
1009
  sameDomain: isSameDomain(fetchOptions.fullUrl, location),
695
- method: fetchOptions.method.toUpperCase()
1010
+ method: fetchOptions.method.toUpperCase(),
1011
+ body: config.originalBody
696
1012
  }) : undefined;
697
1013
  if (typeof onRedirect !== "undefined") {
698
1014
  if (typeof onRedirect === "boolean" && !onRedirect) {
699
1015
  throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
700
- } else if (typeof onRedirect === "object" && !onRedirect.redirect) {
1016
+ } else if (typeof onRedirect === "object" && !onRedirect.redirect && !onRedirect.withoutBody && !("body" in onRedirect)) {
701
1017
  throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
702
1018
  }
703
1019
  }
@@ -717,14 +1033,88 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
717
1033
  });
718
1034
  perform.reset();
719
1035
  config.redirectCount++;
720
- if (response.status === 301 || response.status === 302 || response.status === 303) {
721
- if (config.treat302As303 !== false || response.status === 303) {
722
- options.method = "GET";
1036
+ fetchOptions.fullUrl = location;
1037
+ delete options.params;
1038
+ const normalizedRedirect = typeof onRedirect === "object" ? onRedirect.redirect || onRedirect.withoutBody || "body" in onRedirect : undefined;
1039
+ if (typeof onRedirect === "object" && normalizedRedirect) {
1040
+ let method;
1041
+ const userMethod = onRedirect.method;
1042
+ if (redirectCode === 301 || redirectCode === 302 || redirectCode === 303) {
1043
+ method = userMethod || "GET";
1044
+ } else {
1045
+ method = userMethod || fetchOptions.method;
1046
+ }
1047
+ config.method = method;
1048
+ options.method = method;
1049
+ fetchOptions.method = method;
1050
+ if (onRedirect.redirect && onRedirect.url) {
1051
+ options.fullUrl = onRedirect.url;
1052
+ fetchOptions.fullUrl = onRedirect.url;
1053
+ }
1054
+ if (onRedirect.withoutBody) {
1055
+ delete options.body;
1056
+ delete fetchOptions.body;
1057
+ config.originalBody = undefined;
1058
+ if (fetchOptions.headers instanceof RezoHeaders) {
1059
+ fetchOptions.headers.delete("Content-Type");
1060
+ fetchOptions.headers.delete("Content-Length");
1061
+ }
1062
+ } else if ("body" in onRedirect) {
1063
+ options.body = onRedirect.body;
1064
+ fetchOptions.body = onRedirect.body;
1065
+ config.originalBody = onRedirect.body;
1066
+ } else if (redirectCode === 307 || redirectCode === 308) {
1067
+ const methodUpper = method.toUpperCase();
1068
+ if ((methodUpper === "POST" || methodUpper === "PUT" || methodUpper === "PATCH") && config.originalBody !== undefined) {
1069
+ options.body = config.originalBody;
1070
+ fetchOptions.body = config.originalBody;
1071
+ }
1072
+ } else {
723
1073
  delete options.body;
1074
+ delete fetchOptions.body;
1075
+ if (fetchOptions.headers instanceof RezoHeaders) {
1076
+ fetchOptions.headers.delete("Content-Type");
1077
+ fetchOptions.headers.delete("Content-Length");
1078
+ }
724
1079
  }
1080
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, method);
1081
+ } else if (response.status === 301 || response.status === 302 || response.status === 303) {
1082
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, "GET");
1083
+ options.method = "GET";
1084
+ fetchOptions.method = "GET";
1085
+ config.method = "GET";
1086
+ delete options.body;
1087
+ delete fetchOptions.body;
1088
+ if (fetchOptions.headers instanceof RezoHeaders) {
1089
+ fetchOptions.headers.delete("Content-Type");
1090
+ fetchOptions.headers.delete("Content-Length");
1091
+ }
1092
+ } else {
1093
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, fetchOptions.method);
1094
+ }
1095
+ const jarToSync = rootJar || config.cookieJar;
1096
+ if (response.cookies?.setCookiesString?.length > 0 && jarToSync) {
1097
+ try {
1098
+ jarToSync.setCookiesSync(response.cookies.setCookiesString, fromUrl);
1099
+ } catch (e) {}
1100
+ }
1101
+ if (jarToSync && !config.disableCookieJar) {
1102
+ try {
1103
+ const cookieString = jarToSync.getCookieStringSync(fetchOptions.fullUrl);
1104
+ if (cookieString) {
1105
+ if (fetchOptions.headers instanceof RezoHeaders) {
1106
+ fetchOptions.headers.set("cookie", cookieString);
1107
+ } else if (fetchOptions.headers) {
1108
+ fetchOptions.headers["cookie"] = cookieString;
1109
+ } else {
1110
+ fetchOptions.headers = new RezoHeaders({ cookie: cookieString });
1111
+ }
1112
+ if (config.debug) {
1113
+ console.log(`[Rezo Debug] HTTP/2: Updated Cookie header for redirect: ${cookieString.substring(0, 100)}...`);
1114
+ }
1115
+ }
1116
+ } catch (e) {}
725
1117
  }
726
- fetchOptions.fullUrl = location;
727
- delete options.params;
728
1118
  requestCount++;
729
1119
  continue;
730
1120
  }
@@ -737,7 +1127,7 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
737
1127
  }
738
1128
  }
739
1129
  }
740
- async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool) {
1130
+ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool, rootJar) {
741
1131
  return new Promise(async (resolve) => {
742
1132
  try {
743
1133
  const { fullUrl, body } = fetchOptions;
@@ -764,6 +1154,11 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
764
1154
  if (!headers["accept-encoding"]) {
765
1155
  headers["accept-encoding"] = "gzip, deflate, br";
766
1156
  }
1157
+ if (config.debug && headers["cookie"]) {
1158
+ console.log(`[Rezo Debug] HTTP/2: Sending Cookie header: ${String(headers["cookie"]).substring(0, 100)}...`);
1159
+ } else if (config.debug) {
1160
+ console.log(`[Rezo Debug] HTTP/2: No Cookie header in request`);
1161
+ }
767
1162
  if (body instanceof RezoFormData) {
768
1163
  headers["content-type"] = `multipart/form-data; boundary=${body.getBoundary()}`;
769
1164
  } else if (body instanceof FormData) {
@@ -797,30 +1192,136 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
797
1192
  sessionOptions.pfx = securityContext.pfx;
798
1193
  if (securityContext?.passphrase)
799
1194
  sessionOptions.passphrase = securityContext.passphrase;
1195
+ const forceNewSession = requestCount > 0;
800
1196
  let session;
1197
+ if (config.debug) {
1198
+ console.log(`[Rezo Debug] HTTP/2: Acquiring session for ${url.host}${forceNewSession ? " (forcing new for redirect)" : ""}${fetchOptions.proxy ? " (via proxy)" : ""}...`);
1199
+ }
801
1200
  try {
802
- session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined);
1201
+ session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined, forceNewSession, fetchOptions.proxy);
1202
+ if (config.debug) {
1203
+ console.log(`[Rezo Debug] HTTP/2: Session acquired successfully`);
1204
+ }
803
1205
  } catch (err) {
1206
+ if (config.debug) {
1207
+ console.log(`[Rezo Debug] HTTP/2: Session failed:`, err.message);
1208
+ }
804
1209
  const error = buildSmartError(config, fetchOptions, err);
805
1210
  _stats.statusOnNext = "error";
806
1211
  resolve(error);
807
1212
  return;
808
1213
  }
809
- const req = session.request(headers);
810
- if (config.timeout) {
811
- req.setTimeout(config.timeout, () => {
812
- req.close(http2.constants.NGHTTP2_CANCEL);
813
- const error = buildSmartError(config, fetchOptions, new Error(`Request timeout after ${config.timeout}ms`));
814
- _stats.statusOnNext = "error";
815
- resolve(error);
816
- });
817
- }
818
1214
  let chunks = [];
819
1215
  let contentLengthCounter = 0;
820
1216
  let responseHeaders = {};
821
1217
  let status = 0;
822
1218
  let statusText = "";
1219
+ let resolved = false;
1220
+ let isRedirect = false;
1221
+ let timeoutId = null;
1222
+ const sessionErrorHandler = (err) => {
1223
+ if (config.debug) {
1224
+ console.log(`[Rezo Debug] HTTP/2: Session error:`, err.message);
1225
+ }
1226
+ if (!resolved) {
1227
+ resolved = true;
1228
+ if (timeoutId)
1229
+ clearTimeout(timeoutId);
1230
+ const error = buildSmartError(config, fetchOptions, err);
1231
+ _stats.statusOnNext = "error";
1232
+ resolve(error);
1233
+ }
1234
+ };
1235
+ session.on("error", sessionErrorHandler);
1236
+ session.on("goaway", (errorCode, lastStreamID) => {
1237
+ if (config.debug) {
1238
+ console.log(`[Rezo Debug] HTTP/2: Session GOAWAY received (errorCode: ${errorCode}, lastStreamID: ${lastStreamID})`);
1239
+ }
1240
+ });
1241
+ if (config.debug) {
1242
+ console.log(`[Rezo Debug] HTTP/2: Creating request stream...`);
1243
+ }
1244
+ let req;
1245
+ try {
1246
+ req = session.request(headers);
1247
+ } catch (err) {
1248
+ if (config.debug) {
1249
+ console.log(`[Rezo Debug] HTTP/2: Failed to create request stream:`, err.message);
1250
+ }
1251
+ session.removeListener("error", sessionErrorHandler);
1252
+ const error = buildSmartError(config, fetchOptions, err);
1253
+ _stats.statusOnNext = "error";
1254
+ resolve(error);
1255
+ return;
1256
+ }
1257
+ if (config.debug) {
1258
+ console.log(`[Rezo Debug] HTTP/2: Request stream created`);
1259
+ }
1260
+ const requestTimeout = config.timeout || 30000;
1261
+ timeoutId = setTimeout(() => {
1262
+ if (!resolved) {
1263
+ resolved = true;
1264
+ if (config.debug) {
1265
+ console.log(`[Rezo Debug] HTTP/2: Request timeout after ${requestTimeout}ms (no response received)`);
1266
+ }
1267
+ req.close(http2.constants.NGHTTP2_CANCEL);
1268
+ const error = buildSmartError(config, fetchOptions, new Error(`Request timeout after ${requestTimeout}ms`));
1269
+ _stats.statusOnNext = "error";
1270
+ resolve(error);
1271
+ }
1272
+ }, requestTimeout);
1273
+ const sessionSocket = session.socket;
1274
+ if (sessionSocket && typeof sessionSocket.ref === "function") {
1275
+ sessionSocket.ref();
1276
+ }
1277
+ req.on("close", () => {
1278
+ if (config.debug && !resolved) {
1279
+ console.log(`[Rezo Debug] HTTP/2: Stream closed (status: ${status}, resolved: ${resolved})`);
1280
+ }
1281
+ if (!resolved && status === 0) {
1282
+ resolved = true;
1283
+ clearTimeout(timeoutId);
1284
+ if (config.debug) {
1285
+ console.log(`[Rezo Debug] HTTP/2: Stream closed without response - retrying with new session`);
1286
+ }
1287
+ const error = buildSmartError(config, fetchOptions, new Error("HTTP/2 stream closed without response"));
1288
+ _stats.statusOnNext = "error";
1289
+ resolve(error);
1290
+ }
1291
+ });
1292
+ req.on("aborted", () => {
1293
+ if (config.debug) {
1294
+ console.log(`[Rezo Debug] HTTP/2: Stream aborted`);
1295
+ }
1296
+ if (!resolved) {
1297
+ resolved = true;
1298
+ clearTimeout(timeoutId);
1299
+ const error = buildSmartError(config, fetchOptions, new Error("HTTP/2 stream aborted"));
1300
+ _stats.statusOnNext = "error";
1301
+ resolve(error);
1302
+ }
1303
+ });
1304
+ req.on("error", (err) => {
1305
+ if (config.debug) {
1306
+ console.log(`[Rezo Debug] HTTP/2: Stream error:`, err.message);
1307
+ }
1308
+ if (!resolved) {
1309
+ resolved = true;
1310
+ clearTimeout(timeoutId);
1311
+ const error = buildSmartError(config, fetchOptions, err);
1312
+ _stats.statusOnNext = "error";
1313
+ resolve(error);
1314
+ }
1315
+ });
1316
+ req.on("frameError", (type, code, id) => {
1317
+ if (config.debug) {
1318
+ console.log(`[Rezo Debug] HTTP/2: Frame error - type: ${type}, code: ${code}, id: ${id}`);
1319
+ }
1320
+ });
823
1321
  req.on("response", (headers) => {
1322
+ if (config.debug) {
1323
+ console.log(`[Rezo Debug] HTTP/2: Response received, status: ${headers[":status"]}`);
1324
+ }
824
1325
  responseHeaders = headers;
825
1326
  status = Number(headers[http2.constants.HTTP2_HEADER_STATUS]) || 200;
826
1327
  statusText = getStatusText(status);
@@ -829,7 +1330,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
829
1330
  config.timing.responseStart = timing.firstByteTime;
830
1331
  }
831
1332
  const location = headers["location"];
832
- const isRedirect = status >= 300 && status < 400 && location;
1333
+ isRedirect = status >= 300 && status < 400 && !!location;
833
1334
  if (isRedirect) {
834
1335
  _stats.statusOnNext = "redirect";
835
1336
  const redirectUrlObj = new URL(location, url);
@@ -841,7 +1342,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
841
1342
  config.network.httpVersion = "h2";
842
1343
  (async () => {
843
1344
  try {
844
- await updateCookies(config, headers, url.href);
1345
+ await updateCookies(config, headers, url.href, rootJar);
845
1346
  } catch (err) {
846
1347
  if (config.debug) {
847
1348
  console.log("[Rezo Debug] Cookie hook error:", err);
@@ -874,6 +1375,9 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
874
1375
  }
875
1376
  });
876
1377
  req.on("data", (chunk) => {
1378
+ if (config.debug) {
1379
+ console.log(`[Rezo Debug] HTTP/2: Received data chunk: ${chunk.length} bytes (total: ${contentLengthCounter + chunk.length})`);
1380
+ }
877
1381
  chunks.push(chunk);
878
1382
  contentLengthCounter += chunk.length;
879
1383
  if (streamResult) {
@@ -895,209 +1399,221 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
895
1399
  }
896
1400
  });
897
1401
  req.on("end", async () => {
898
- updateTiming(config, timing, contentLengthCounter);
899
- if (!config.transfer) {
900
- config.transfer = { requestSize: 0, responseSize: 0, headerSize: 0, bodySize: 0 };
901
- }
902
- if (config.transfer.requestSize === undefined) {
903
- config.transfer.requestSize = 0;
904
- }
905
- if (config.transfer.requestSize === 0 && body) {
906
- if (typeof body === "string") {
907
- config.transfer.requestSize = Buffer.byteLength(body, "utf8");
908
- } else if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
909
- config.transfer.requestSize = body.length;
910
- } else if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
911
- config.transfer.requestSize = Buffer.byteLength(body.toString(), "utf8");
912
- } else if (body instanceof RezoFormData) {
913
- config.transfer.requestSize = await body.getLength() || 0;
914
- } else if (typeof body === "object") {
915
- config.transfer.requestSize = Buffer.byteLength(JSON.stringify(body), "utf8");
916
- }
917
- }
918
- (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
919
- if (_stats.statusOnNext === "redirect") {
920
- const partialResponse = {
921
- data: "",
922
- status,
923
- statusText,
924
- headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
925
- cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
926
- config,
927
- contentType: responseHeaders["content-type"],
928
- contentLength: contentLengthCounter,
929
- finalUrl: url.href,
930
- urls: buildUrlTree(config, url.href)
931
- };
932
- resolve(partialResponse);
1402
+ if (resolved)
933
1403
  return;
1404
+ if (config.debug) {
1405
+ console.log(`[Rezo Debug] HTTP/2: Stream 'end' event fired (status: ${status}, chunks: ${chunks.length}, bytes: ${contentLengthCounter})`);
934
1406
  }
935
- let responseBody = Buffer.concat(chunks);
936
- const contentEncoding = responseHeaders["content-encoding"];
937
- if (contentEncoding && contentLengthCounter > 0 && CompressionUtil.shouldDecompress(contentEncoding, config)) {
938
- try {
939
- const decompressed = await decompressBuffer(responseBody, contentEncoding);
940
- responseBody = decompressed;
941
- } catch (err) {
942
- const error = buildDecompressionError({
943
- statusCode: status,
944
- headers: sanitizeHttp2Headers(responseHeaders),
1407
+ resolved = true;
1408
+ clearTimeout(timeoutId);
1409
+ try {
1410
+ updateTiming(config, timing, contentLengthCounter);
1411
+ if (!config.transfer) {
1412
+ config.transfer = { requestSize: 0, responseSize: 0, headerSize: 0, bodySize: 0 };
1413
+ }
1414
+ if (config.transfer.requestSize === undefined) {
1415
+ config.transfer.requestSize = 0;
1416
+ }
1417
+ if (config.transfer.requestSize === 0 && body) {
1418
+ if (typeof body === "string") {
1419
+ config.transfer.requestSize = Buffer.byteLength(body, "utf8");
1420
+ } else if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
1421
+ config.transfer.requestSize = body.length;
1422
+ } else if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
1423
+ config.transfer.requestSize = Buffer.byteLength(body.toString(), "utf8");
1424
+ } else if (body instanceof RezoFormData) {
1425
+ config.transfer.requestSize = await body.getLength() || 0;
1426
+ } else if (typeof body === "object") {
1427
+ config.transfer.requestSize = Buffer.byteLength(JSON.stringify(body), "utf8");
1428
+ }
1429
+ }
1430
+ (sessionPool || Http2SessionPool.getInstance()).releaseSession(url, fetchOptions.proxy);
1431
+ if (isRedirect) {
1432
+ _stats.statusOnNext = "redirect";
1433
+ const partialResponse = {
1434
+ data: "",
1435
+ status,
1436
+ statusText,
1437
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1438
+ cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
1439
+ config,
945
1440
  contentType: responseHeaders["content-type"],
946
- contentLength: String(contentLengthCounter),
947
- cookies: config.responseCookies?.setCookiesString || [],
948
- statusText: err.message,
949
- url: url.href,
950
- body: responseBody,
1441
+ contentLength: contentLengthCounter,
951
1442
  finalUrl: url.href,
952
- config,
953
- request: fetchOptions
954
- });
955
- _stats.statusOnNext = "error";
956
- resolve(error);
1443
+ urls: buildUrlTree(config, url.href)
1444
+ };
1445
+ resolve(partialResponse);
957
1446
  return;
958
1447
  }
959
- }
960
- let data;
961
- const contentType = responseHeaders["content-type"] || "";
962
- const responseType = config.responseType || fetchOptions.responseType || "auto";
963
- if (responseType === "buffer" || responseType === "arrayBuffer") {
964
- data = responseBody;
965
- } else if (responseType === "text") {
966
- data = responseBody.toString("utf-8");
967
- } else if (responseType === "json" || responseType === "auto" && contentType.includes("application/json")) {
968
- try {
969
- data = JSON.parse(responseBody.toString("utf-8"));
970
- } catch {
971
- data = responseBody.toString("utf-8");
1448
+ let responseBody = Buffer.concat(chunks);
1449
+ const contentEncoding = responseHeaders["content-encoding"];
1450
+ if (contentEncoding && contentLengthCounter > 0 && CompressionUtil.shouldDecompress(contentEncoding, config)) {
1451
+ try {
1452
+ const decompressed = await decompressBuffer(responseBody, contentEncoding);
1453
+ responseBody = decompressed;
1454
+ } catch (err) {
1455
+ const error = buildDecompressionError({
1456
+ statusCode: status,
1457
+ headers: sanitizeHttp2Headers(responseHeaders),
1458
+ contentType: responseHeaders["content-type"],
1459
+ contentLength: String(contentLengthCounter),
1460
+ cookies: config.responseCookies?.setCookiesString || [],
1461
+ statusText: err.message,
1462
+ url: url.href,
1463
+ body: responseBody,
1464
+ finalUrl: url.href,
1465
+ config,
1466
+ request: fetchOptions
1467
+ });
1468
+ _stats.statusOnNext = "error";
1469
+ resolve(error);
1470
+ return;
1471
+ }
972
1472
  }
973
- } else {
974
- if (contentType.includes("application/json")) {
1473
+ let data;
1474
+ const contentType = responseHeaders["content-type"] || "";
1475
+ const responseType = config.responseType || fetchOptions.responseType || "auto";
1476
+ if (responseType === "buffer" || responseType === "arrayBuffer") {
1477
+ data = responseBody;
1478
+ } else if (responseType === "text") {
1479
+ data = responseBody.toString("utf-8");
1480
+ } else if (responseType === "json" || responseType === "auto" && contentType.includes("application/json")) {
975
1481
  try {
976
1482
  data = JSON.parse(responseBody.toString("utf-8"));
977
1483
  } catch {
978
1484
  data = responseBody.toString("utf-8");
979
1485
  }
980
1486
  } else {
981
- data = responseBody.toString("utf-8");
1487
+ if (contentType.includes("application/json")) {
1488
+ try {
1489
+ data = JSON.parse(responseBody.toString("utf-8"));
1490
+ } catch {
1491
+ data = responseBody.toString("utf-8");
1492
+ }
1493
+ } else {
1494
+ data = responseBody.toString("utf-8");
1495
+ }
982
1496
  }
983
- }
984
- config.status = status;
985
- config.statusText = statusText;
986
- _stats.statusOnNext = status >= 400 ? "error" : "success";
987
- const responseCookies = config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] };
988
- const mergedCookies = mergeRequestAndResponseCookies(config, responseCookies, url.href);
989
- const finalResponse = {
990
- data,
991
- status,
992
- statusText,
993
- headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
994
- cookies: mergedCookies,
995
- config,
996
- contentType,
997
- contentLength: contentLengthCounter,
998
- finalUrl: url.href,
999
- urls: buildUrlTree(config, url.href)
1000
- };
1001
- if (downloadResult && fs && config.fileName) {
1002
- try {
1003
- fs.writeFileSync(config.fileName, responseBody);
1004
- const downloadFinishEvent = {
1497
+ config.status = status;
1498
+ config.statusText = statusText;
1499
+ _stats.statusOnNext = status >= 400 ? "error" : "success";
1500
+ const responseCookies = config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] };
1501
+ const mergedCookies = mergeRequestAndResponseCookies(config, responseCookies, url.href);
1502
+ const finalResponse = {
1503
+ data,
1504
+ status,
1505
+ statusText,
1506
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1507
+ cookies: mergedCookies,
1508
+ config,
1509
+ contentType,
1510
+ contentLength: contentLengthCounter,
1511
+ finalUrl: url.href,
1512
+ urls: buildUrlTree(config, url.href)
1513
+ };
1514
+ if (downloadResult && fs && config.fileName) {
1515
+ try {
1516
+ fs.writeFileSync(config.fileName, responseBody);
1517
+ const downloadFinishEvent = {
1518
+ status,
1519
+ statusText,
1520
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1521
+ contentType,
1522
+ contentLength: responseBody.length,
1523
+ finalUrl: url.href,
1524
+ cookies: mergedCookies,
1525
+ urls: buildUrlTree(config, url.href),
1526
+ fileName: config.fileName,
1527
+ fileSize: responseBody.length,
1528
+ timing: {
1529
+ ...getTimingDurations(config),
1530
+ download: getTimingDurations(config).download || 0
1531
+ },
1532
+ averageSpeed: getTimingDurations(config).download ? responseBody.length / getTimingDurations(config).download * 1000 : 0,
1533
+ config: sanitizeConfig(config)
1534
+ };
1535
+ downloadResult.emit("finish", downloadFinishEvent);
1536
+ downloadResult.emit("done", downloadFinishEvent);
1537
+ downloadResult._markFinished();
1538
+ } catch (err) {
1539
+ const error = buildDownloadError({
1540
+ statusCode: status,
1541
+ headers: sanitizeHttp2Headers(responseHeaders),
1542
+ contentType,
1543
+ contentLength: String(contentLengthCounter),
1544
+ cookies: config.responseCookies?.setCookiesString || [],
1545
+ statusText: err.message,
1546
+ url: url.href,
1547
+ body: responseBody,
1548
+ finalUrl: url.href,
1549
+ config,
1550
+ request: fetchOptions
1551
+ });
1552
+ downloadResult.emit("error", error);
1553
+ resolve(error);
1554
+ return;
1555
+ }
1556
+ }
1557
+ if (streamResult) {
1558
+ const streamFinishEvent = {
1005
1559
  status,
1006
1560
  statusText,
1007
1561
  headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1008
1562
  contentType,
1009
- contentLength: responseBody.length,
1563
+ contentLength: contentLengthCounter,
1564
+ finalUrl: url.href,
1565
+ cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
1566
+ urls: buildUrlTree(config, url.href),
1567
+ timing: getTimingDurations(config),
1568
+ config: sanitizeConfig(config)
1569
+ };
1570
+ streamResult.emit("finish", streamFinishEvent);
1571
+ streamResult.emit("done", streamFinishEvent);
1572
+ streamResult.emit("end");
1573
+ streamResult._markFinished();
1574
+ }
1575
+ if (uploadResult) {
1576
+ const uploadFinishEvent = {
1577
+ response: {
1578
+ status,
1579
+ statusText,
1580
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1581
+ data,
1582
+ contentType,
1583
+ contentLength: contentLengthCounter
1584
+ },
1010
1585
  finalUrl: url.href,
1011
- cookies: mergedCookies,
1586
+ cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
1012
1587
  urls: buildUrlTree(config, url.href),
1013
- fileName: config.fileName,
1014
- fileSize: responseBody.length,
1588
+ uploadSize: config.transfer.requestSize || 0,
1015
1589
  timing: {
1016
1590
  ...getTimingDurations(config),
1017
- download: getTimingDurations(config).download || 0
1591
+ upload: getTimingDurations(config).firstByte || 0,
1592
+ waiting: getTimingDurations(config).download > 0 && getTimingDurations(config).firstByte > 0 ? getTimingDurations(config).download - getTimingDurations(config).firstByte : 0
1018
1593
  },
1019
- averageSpeed: getTimingDurations(config).download ? responseBody.length / getTimingDurations(config).download * 1000 : 0,
1594
+ averageUploadSpeed: getTimingDurations(config).firstByte && config.transfer.requestSize ? config.transfer.requestSize / getTimingDurations(config).firstByte * 1000 : 0,
1595
+ averageDownloadSpeed: getTimingDurations(config).download ? contentLengthCounter / getTimingDurations(config).download * 1000 : 0,
1020
1596
  config: sanitizeConfig(config)
1021
1597
  };
1022
- downloadResult.emit("finish", downloadFinishEvent);
1023
- downloadResult.emit("done", downloadFinishEvent);
1024
- downloadResult._markFinished();
1025
- } catch (err) {
1026
- const error = buildDownloadError({
1027
- statusCode: status,
1028
- headers: sanitizeHttp2Headers(responseHeaders),
1029
- contentType,
1030
- contentLength: String(contentLengthCounter),
1031
- cookies: config.responseCookies?.setCookiesString || [],
1032
- statusText: err.message,
1033
- url: url.href,
1034
- body: responseBody,
1035
- finalUrl: url.href,
1036
- config,
1037
- request: fetchOptions
1038
- });
1039
- downloadResult.emit("error", error);
1040
- resolve(error);
1041
- return;
1598
+ uploadResult.emit("finish", uploadFinishEvent);
1599
+ uploadResult.emit("done", uploadFinishEvent);
1600
+ uploadResult._markFinished();
1042
1601
  }
1602
+ resolve(finalResponse);
1603
+ } catch (endError) {
1604
+ if (config.debug) {
1605
+ console.log(`[Rezo Debug] HTTP/2: Error in 'end' handler:`, endError.message);
1606
+ }
1607
+ (sessionPool || Http2SessionPool.getInstance()).releaseSession(url, fetchOptions.proxy);
1608
+ const error = buildSmartError(config, fetchOptions, endError);
1609
+ _stats.statusOnNext = "error";
1610
+ resolve(error);
1043
1611
  }
1044
- if (streamResult) {
1045
- const streamFinishEvent = {
1046
- status,
1047
- statusText,
1048
- headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1049
- contentType,
1050
- contentLength: contentLengthCounter,
1051
- finalUrl: url.href,
1052
- cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
1053
- urls: buildUrlTree(config, url.href),
1054
- timing: getTimingDurations(config),
1055
- config: sanitizeConfig(config)
1056
- };
1057
- streamResult.emit("finish", streamFinishEvent);
1058
- streamResult.emit("done", streamFinishEvent);
1059
- streamResult.emit("end");
1060
- streamResult._markFinished();
1061
- }
1062
- if (uploadResult) {
1063
- const uploadFinishEvent = {
1064
- response: {
1065
- status,
1066
- statusText,
1067
- headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1068
- data,
1069
- contentType,
1070
- contentLength: contentLengthCounter
1071
- },
1072
- finalUrl: url.href,
1073
- cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
1074
- urls: buildUrlTree(config, url.href),
1075
- uploadSize: config.transfer.requestSize || 0,
1076
- timing: {
1077
- ...getTimingDurations(config),
1078
- upload: getTimingDurations(config).firstByte || 0,
1079
- waiting: getTimingDurations(config).download > 0 && getTimingDurations(config).firstByte > 0 ? getTimingDurations(config).download - getTimingDurations(config).firstByte : 0
1080
- },
1081
- averageUploadSpeed: getTimingDurations(config).firstByte && config.transfer.requestSize ? config.transfer.requestSize / getTimingDurations(config).firstByte * 1000 : 0,
1082
- averageDownloadSpeed: getTimingDurations(config).download ? contentLengthCounter / getTimingDurations(config).download * 1000 : 0,
1083
- config: sanitizeConfig(config)
1084
- };
1085
- uploadResult.emit("finish", uploadFinishEvent);
1086
- uploadResult.emit("done", uploadFinishEvent);
1087
- uploadResult._markFinished();
1088
- }
1089
- resolve(finalResponse);
1090
- });
1091
- req.on("error", (err) => {
1092
- _stats.statusOnNext = "error";
1093
- (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
1094
- const error = buildSmartError(config, fetchOptions, err);
1095
- if (eventEmitter) {
1096
- eventEmitter.emit("error", error);
1097
- }
1098
- resolve(error);
1099
1612
  });
1100
1613
  if (body) {
1614
+ if (config.debug) {
1615
+ console.log(`[Rezo Debug] HTTP/2: Writing request body (type: ${body?.constructor?.name || typeof body})...`);
1616
+ }
1101
1617
  if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
1102
1618
  req.write(body.toString());
1103
1619
  } else if (body instanceof FormData || body instanceof RezoFormData) {
@@ -1107,13 +1623,26 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
1107
1623
  } else if (typeof body === "object" && !Buffer.isBuffer(body) && !(body instanceof Uint8Array) && !(body instanceof Readable)) {
1108
1624
  req.write(JSON.stringify(body));
1109
1625
  } else if (body instanceof Readable) {
1626
+ if (config.debug) {
1627
+ console.log(`[Rezo Debug] HTTP/2: Piping stream body...`);
1628
+ }
1110
1629
  body.pipe(req);
1111
1630
  return;
1112
1631
  } else {
1113
1632
  req.write(body);
1114
1633
  }
1634
+ if (config.debug) {
1635
+ console.log(`[Rezo Debug] HTTP/2: Body written, calling req.end()...`);
1636
+ }
1637
+ } else {
1638
+ if (config.debug) {
1639
+ console.log(`[Rezo Debug] HTTP/2: No body, calling req.end()...`);
1640
+ }
1115
1641
  }
1116
1642
  req.end();
1643
+ if (config.debug) {
1644
+ console.log(`[Rezo Debug] HTTP/2: req.end() called, waiting for response...`);
1645
+ }
1117
1646
  } catch (error) {
1118
1647
  _stats.statusOnNext = "error";
1119
1648
  const rezoError = buildSmartError(config, fetchOptions, error);