rezo 1.0.40 → 1.0.42

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.
@@ -17,6 +17,100 @@ const { isSameDomain, RezoPerformance } = require('../utils/tools.cjs');
17
17
  const { ResponseCache } = require('../cache/response-cache.cjs');
18
18
  let zstdDecompressSync = null;
19
19
  let zstdChecked = false;
20
+ const debugLog = {
21
+ requestStart: (config, url, method) => {
22
+ if (config.debug) {
23
+ console.log(`
24
+ [Rezo Debug] ─────────────────────────────────────`);
25
+ console.log(`[Rezo Debug] ${method} ${url}`);
26
+ console.log(`[Rezo Debug] Request ID: ${config.requestId}`);
27
+ if (config.originalRequest?.headers) {
28
+ const headers = config.originalRequest.headers instanceof RezoHeaders ? config.originalRequest.headers.toObject() : config.originalRequest.headers;
29
+ console.log(`[Rezo Debug] Request Headers:`, JSON.stringify(headers, null, 2));
30
+ }
31
+ if (config.proxy && typeof config.proxy === "object") {
32
+ console.log(`[Rezo Debug] Proxy: ${config.proxy.protocol}://${config.proxy.host}:${config.proxy.port}`);
33
+ } else if (config.proxy && typeof config.proxy === "string") {
34
+ console.log(`[Rezo Debug] Proxy: ${config.proxy}`);
35
+ }
36
+ }
37
+ if (config.trackUrl) {
38
+ console.log(`[Rezo Track] → ${method} ${url}`);
39
+ }
40
+ },
41
+ redirect: (config, fromUrl, toUrl, statusCode, method) => {
42
+ if (config.debug) {
43
+ console.log(`[Rezo Debug] Redirect ${statusCode}: ${fromUrl}`);
44
+ console.log(`[Rezo Debug] → ${toUrl} (${method})`);
45
+ }
46
+ if (config.trackUrl) {
47
+ console.log(`[Rezo Track] ↳ ${statusCode} → ${toUrl}`);
48
+ }
49
+ },
50
+ retry: (config, attempt, maxRetries, statusCode, delay) => {
51
+ if (config.debug) {
52
+ console.log(`[Rezo Debug] Retry ${attempt}/${maxRetries} after status ${statusCode}${delay > 0 ? ` (waiting ${delay}ms)` : ""}`);
53
+ }
54
+ if (config.trackUrl) {
55
+ console.log(`[Rezo Track] ⟳ Retry ${attempt}/${maxRetries} (status ${statusCode})`);
56
+ }
57
+ },
58
+ maxRetries: (config, maxRetries) => {
59
+ if (config.debug) {
60
+ console.log(`[Rezo Debug] Max retries (${maxRetries}) reached, throwing error`);
61
+ }
62
+ if (config.trackUrl) {
63
+ console.log(`[Rezo Track] ✗ Max retries reached`);
64
+ }
65
+ },
66
+ response: (config, status, statusText, duration) => {
67
+ if (config.debug) {
68
+ console.log(`[Rezo Debug] Response: ${status} ${statusText} (${duration.toFixed(2)}ms)`);
69
+ }
70
+ if (config.trackUrl) {
71
+ console.log(`[Rezo Track] ✓ ${status} ${statusText}`);
72
+ }
73
+ },
74
+ responseHeaders: (config, headers) => {
75
+ if (config.debug) {
76
+ console.log(`[Rezo Debug] Response Headers:`, JSON.stringify(headers, null, 2));
77
+ }
78
+ },
79
+ cookies: (config, cookieCount) => {
80
+ if (config.debug && cookieCount > 0) {
81
+ console.log(`[Rezo Debug] Cookies received: ${cookieCount}`);
82
+ }
83
+ },
84
+ timing: (config, timing) => {
85
+ if (config.debug) {
86
+ const parts = [];
87
+ if (timing.dns)
88
+ parts.push(`DNS: ${timing.dns.toFixed(2)}ms`);
89
+ if (timing.connect)
90
+ parts.push(`Connect: ${timing.connect.toFixed(2)}ms`);
91
+ if (timing.tls)
92
+ parts.push(`TLS: ${timing.tls.toFixed(2)}ms`);
93
+ if (timing.ttfb)
94
+ parts.push(`TTFB: ${timing.ttfb.toFixed(2)}ms`);
95
+ if (timing.total)
96
+ parts.push(`Total: ${timing.total.toFixed(2)}ms`);
97
+ if (parts.length > 0) {
98
+ console.log(`[Rezo Debug] Timing: ${parts.join(" | ")}`);
99
+ }
100
+ }
101
+ },
102
+ complete: (config, finalUrl, redirectCount, duration) => {
103
+ if (config.debug) {
104
+ console.log(`[Rezo Debug] Complete: ${finalUrl}`);
105
+ if (redirectCount > 0) {
106
+ console.log(`[Rezo Debug] Redirects: ${redirectCount}`);
107
+ }
108
+ console.log(`[Rezo Debug] Total Duration: ${duration.toFixed(2)}ms`);
109
+ console.log(`[Rezo Debug] ─────────────────────────────────────
110
+ `);
111
+ }
112
+ }
113
+ };
20
114
  async function decompressBuffer(buffer, contentEncoding) {
21
115
  const encoding = contentEncoding.toLowerCase();
22
116
  switch (encoding) {
@@ -103,26 +197,47 @@ class Http2SessionPool {
103
197
  getSessionKey(url, options) {
104
198
  return `${url.protocol}//${url.host}`;
105
199
  }
106
- async getSession(url, options, timeout) {
200
+ isSessionHealthy(session, entry) {
201
+ if (session.closed || session.destroyed)
202
+ return false;
203
+ if (entry.goawayReceived)
204
+ return false;
205
+ const socket = session.socket;
206
+ if (socket && (socket.destroyed || socket.closed || !socket.writable))
207
+ return false;
208
+ return true;
209
+ }
210
+ async getSession(url, options, timeout, forceNew = false) {
107
211
  const key = this.getSessionKey(url, options);
108
212
  const existing = this.sessions.get(key);
109
- if (existing && !existing.session.closed && !existing.session.destroyed) {
213
+ if (!forceNew && existing && this.isSessionHealthy(existing.session, existing)) {
110
214
  existing.lastUsed = Date.now();
111
215
  existing.refCount++;
112
216
  return existing.session;
113
217
  }
218
+ if (existing && !this.isSessionHealthy(existing.session, existing)) {
219
+ try {
220
+ existing.session.close();
221
+ } catch {}
222
+ this.sessions.delete(key);
223
+ }
114
224
  const session = await this.createSession(url, options, timeout);
115
- this.sessions.set(key, {
225
+ const entry = {
116
226
  session,
117
227
  lastUsed: Date.now(),
118
- refCount: 1
119
- });
228
+ refCount: 1,
229
+ goawayReceived: false
230
+ };
231
+ this.sessions.set(key, entry);
120
232
  session.on("close", () => {
121
233
  this.sessions.delete(key);
122
234
  });
123
235
  session.on("error", () => {
124
236
  this.sessions.delete(key);
125
237
  });
238
+ session.on("goaway", () => {
239
+ entry.goawayReceived = true;
240
+ });
126
241
  return session;
127
242
  }
128
243
  createSession(url, options, timeout) {
@@ -308,7 +423,7 @@ function sanitizeConfig(config) {
308
423
  const { data: _data, ...sanitized } = config;
309
424
  return sanitized;
310
425
  }
311
- async function updateCookies(config, headers, url) {
426
+ async function updateCookies(config, headers, url, rootJar) {
312
427
  const setCookieHeaders = headers["set-cookie"];
313
428
  if (!setCookieHeaders)
314
429
  return;
@@ -352,8 +467,9 @@ async function updateCookies(config, headers, url) {
352
467
  const acceptedCookieStrings = acceptedCookies.map((c) => c.toSetCookieString());
353
468
  const jar = new RezoCookieJar;
354
469
  jar.setCookiesSync(acceptedCookieStrings, url);
355
- if (!config.disableCookieJar && config.cookieJar) {
356
- config.cookieJar.setCookiesSync(acceptedCookieStrings, url);
470
+ const jarToSync = rootJar || config.cookieJar;
471
+ if (!config.disableCookieJar && jarToSync) {
472
+ jarToSync.setCookiesSync(acceptedCookieStrings, url);
357
473
  }
358
474
  const cookies = jar.cookies();
359
475
  cookies.setCookiesString = cookieHeaderArray;
@@ -525,7 +641,7 @@ async function executeRequest(options, defaultOptions, jar) {
525
641
  }
526
642
  }
527
643
  try {
528
- const res = executeHttp2Request(fetchOptions, mainConfig, options, perform, fs, streamResponse, downloadResponse, uploadResponse);
644
+ const res = executeHttp2Request(fetchOptions, mainConfig, options, perform, fs, streamResponse, downloadResponse, uploadResponse, jar);
529
645
  if (streamResponse) {
530
646
  return streamResponse;
531
647
  } else if (downloadResponse) {
@@ -558,7 +674,7 @@ async function executeRequest(options, defaultOptions, jar) {
558
674
  throw error;
559
675
  }
560
676
  }
561
- async function executeHttp2Request(fetchOptions, config, options, perform, fs, streamResult, downloadResult, uploadResult) {
677
+ async function executeHttp2Request(fetchOptions, config, options, perform, fs, streamResult, downloadResult, uploadResult, rootJar) {
562
678
  let requestCount = 0;
563
679
  const _stats = { statusOnNext: "abort" };
564
680
  let responseStatusCode;
@@ -578,6 +694,11 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
578
694
  config.setSignal();
579
695
  const timeoutClearInstance = config.timeoutClearInstanse;
580
696
  delete config.timeoutClearInstanse;
697
+ if (!config.requestId) {
698
+ config.requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
699
+ }
700
+ const requestUrl = fetchOptions.fullUrl ? String(fetchOptions.fullUrl) : "";
701
+ debugLog.requestStart(config, requestUrl, fetchOptions.method || "GET");
581
702
  const eventEmitter = streamResult || downloadResult || uploadResult;
582
703
  if (eventEmitter) {
583
704
  eventEmitter.emit("initiated");
@@ -590,7 +711,7 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
590
711
  throw error;
591
712
  }
592
713
  try {
593
- const response = await executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool);
714
+ const response = await executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool, rootJar);
594
715
  const statusOnNext = _stats.statusOnNext;
595
716
  if (response instanceof RezoError) {
596
717
  const fileName = config.fileName;
@@ -619,16 +740,12 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
619
740
  throw response;
620
741
  }
621
742
  if (maxRetries <= retries) {
622
- if (config.debug) {
623
- console.log(`Max retries (${maxRetries}) reached`);
624
- }
743
+ debugLog.maxRetries(config, maxRetries);
625
744
  throw response;
626
745
  }
627
746
  retries++;
628
747
  const currentDelay = incrementDelay ? retryDelay * retries : retryDelay;
629
- if (config.debug) {
630
- console.log(`Retrying... ${retryDelay > 0 ? "in " + currentDelay + "ms" : ""}`);
631
- }
748
+ debugLog.retry(config, retries, maxRetries, responseStatusCode || 0, currentDelay);
632
749
  if (config.hooks?.beforeRetry && config.hooks.beforeRetry.length > 0) {
633
750
  for (const hook of config.hooks.beforeRetry) {
634
751
  await hook(config, response, retries);
@@ -643,6 +760,16 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
643
760
  continue;
644
761
  }
645
762
  if (statusOnNext === "success") {
763
+ const totalDuration = performance.now() - timing.startTime;
764
+ debugLog.response(config, response.status, response.statusText, totalDuration);
765
+ if (response.headers) {
766
+ const headersObj = response.headers instanceof RezoHeaders ? response.headers.toObject() : response.headers;
767
+ debugLog.responseHeaders(config, headersObj);
768
+ }
769
+ if (response.cookies?.array) {
770
+ debugLog.cookies(config, response.cookies.array.length);
771
+ }
772
+ debugLog.complete(config, response.finalUrl || requestUrl, config.redirectCount, totalDuration);
646
773
  return response;
647
774
  }
648
775
  if (statusOnNext === "error") {
@@ -686,18 +813,20 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
686
813
  visitedUrls.add(normalizedRedirectUrl);
687
814
  }
688
815
  const redirectCode = response.status;
816
+ const fromUrl = fetchOptions.fullUrl;
689
817
  const redirectCallback = config.beforeRedirect || config.onRedirect;
690
818
  const onRedirect = redirectCallback ? redirectCallback({
691
819
  url: new URL(location),
692
820
  status: response.status,
693
821
  headers: response.headers,
694
822
  sameDomain: isSameDomain(fetchOptions.fullUrl, location),
695
- method: fetchOptions.method.toUpperCase()
823
+ method: fetchOptions.method.toUpperCase(),
824
+ body: config.originalBody
696
825
  }) : undefined;
697
826
  if (typeof onRedirect !== "undefined") {
698
827
  if (typeof onRedirect === "boolean" && !onRedirect) {
699
828
  throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
700
- } else if (typeof onRedirect === "object" && !onRedirect.redirect) {
829
+ } else if (typeof onRedirect === "object" && !onRedirect.redirect && !onRedirect.withoutBody && !("body" in onRedirect)) {
701
830
  throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
702
831
  }
703
832
  }
@@ -717,14 +846,88 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
717
846
  });
718
847
  perform.reset();
719
848
  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";
849
+ fetchOptions.fullUrl = location;
850
+ delete options.params;
851
+ const normalizedRedirect = typeof onRedirect === "object" ? onRedirect.redirect || onRedirect.withoutBody || "body" in onRedirect : undefined;
852
+ if (typeof onRedirect === "object" && normalizedRedirect) {
853
+ let method;
854
+ const userMethod = onRedirect.method;
855
+ if (redirectCode === 301 || redirectCode === 302 || redirectCode === 303) {
856
+ method = userMethod || "GET";
857
+ } else {
858
+ method = userMethod || fetchOptions.method;
859
+ }
860
+ config.method = method;
861
+ options.method = method;
862
+ fetchOptions.method = method;
863
+ if (onRedirect.redirect && onRedirect.url) {
864
+ options.fullUrl = onRedirect.url;
865
+ fetchOptions.fullUrl = onRedirect.url;
866
+ }
867
+ if (onRedirect.withoutBody) {
723
868
  delete options.body;
869
+ delete fetchOptions.body;
870
+ config.originalBody = undefined;
871
+ if (fetchOptions.headers instanceof RezoHeaders) {
872
+ fetchOptions.headers.delete("Content-Type");
873
+ fetchOptions.headers.delete("Content-Length");
874
+ }
875
+ } else if ("body" in onRedirect) {
876
+ options.body = onRedirect.body;
877
+ fetchOptions.body = onRedirect.body;
878
+ config.originalBody = onRedirect.body;
879
+ } else if (redirectCode === 307 || redirectCode === 308) {
880
+ const methodUpper = method.toUpperCase();
881
+ if ((methodUpper === "POST" || methodUpper === "PUT" || methodUpper === "PATCH") && config.originalBody !== undefined) {
882
+ options.body = config.originalBody;
883
+ fetchOptions.body = config.originalBody;
884
+ }
885
+ } else {
886
+ delete options.body;
887
+ delete fetchOptions.body;
888
+ if (fetchOptions.headers instanceof RezoHeaders) {
889
+ fetchOptions.headers.delete("Content-Type");
890
+ fetchOptions.headers.delete("Content-Length");
891
+ }
724
892
  }
893
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, method);
894
+ } else if (response.status === 301 || response.status === 302 || response.status === 303) {
895
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, "GET");
896
+ options.method = "GET";
897
+ fetchOptions.method = "GET";
898
+ config.method = "GET";
899
+ delete options.body;
900
+ delete fetchOptions.body;
901
+ if (fetchOptions.headers instanceof RezoHeaders) {
902
+ fetchOptions.headers.delete("Content-Type");
903
+ fetchOptions.headers.delete("Content-Length");
904
+ }
905
+ } else {
906
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, fetchOptions.method);
907
+ }
908
+ const jarToSync = rootJar || config.cookieJar;
909
+ if (response.cookies?.setCookiesString?.length > 0 && jarToSync) {
910
+ try {
911
+ jarToSync.setCookiesSync(response.cookies.setCookiesString, fromUrl);
912
+ } catch (e) {}
913
+ }
914
+ if (jarToSync && !config.disableCookieJar) {
915
+ try {
916
+ const cookieString = jarToSync.getCookieStringSync(fetchOptions.fullUrl);
917
+ if (cookieString) {
918
+ if (fetchOptions.headers instanceof RezoHeaders) {
919
+ fetchOptions.headers.set("cookie", cookieString);
920
+ } else if (fetchOptions.headers) {
921
+ fetchOptions.headers["cookie"] = cookieString;
922
+ } else {
923
+ fetchOptions.headers = new RezoHeaders({ cookie: cookieString });
924
+ }
925
+ if (config.debug) {
926
+ console.log(`[Rezo Debug] HTTP/2: Updated Cookie header for redirect: ${cookieString.substring(0, 100)}...`);
927
+ }
928
+ }
929
+ } catch (e) {}
725
930
  }
726
- fetchOptions.fullUrl = location;
727
- delete options.params;
728
931
  requestCount++;
729
932
  continue;
730
933
  }
@@ -737,7 +940,7 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
737
940
  }
738
941
  }
739
942
  }
740
- async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool) {
943
+ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool, rootJar) {
741
944
  return new Promise(async (resolve) => {
742
945
  try {
743
946
  const { fullUrl, body } = fetchOptions;
@@ -764,6 +967,11 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
764
967
  if (!headers["accept-encoding"]) {
765
968
  headers["accept-encoding"] = "gzip, deflate, br";
766
969
  }
970
+ if (config.debug && headers["cookie"]) {
971
+ console.log(`[Rezo Debug] HTTP/2: Sending Cookie header: ${String(headers["cookie"]).substring(0, 100)}...`);
972
+ } else if (config.debug) {
973
+ console.log(`[Rezo Debug] HTTP/2: No Cookie header in request`);
974
+ }
767
975
  if (body instanceof RezoFormData) {
768
976
  headers["content-type"] = `multipart/form-data; boundary=${body.getBoundary()}`;
769
977
  } else if (body instanceof FormData) {
@@ -797,30 +1005,136 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
797
1005
  sessionOptions.pfx = securityContext.pfx;
798
1006
  if (securityContext?.passphrase)
799
1007
  sessionOptions.passphrase = securityContext.passphrase;
1008
+ const forceNewSession = requestCount > 0;
800
1009
  let session;
1010
+ if (config.debug) {
1011
+ console.log(`[Rezo Debug] HTTP/2: Acquiring session for ${url.host}${forceNewSession ? " (forcing new for redirect)" : ""}...`);
1012
+ }
801
1013
  try {
802
- session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined);
1014
+ session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined, forceNewSession);
1015
+ if (config.debug) {
1016
+ console.log(`[Rezo Debug] HTTP/2: Session acquired successfully`);
1017
+ }
803
1018
  } catch (err) {
1019
+ if (config.debug) {
1020
+ console.log(`[Rezo Debug] HTTP/2: Session failed:`, err.message);
1021
+ }
804
1022
  const error = buildSmartError(config, fetchOptions, err);
805
1023
  _stats.statusOnNext = "error";
806
1024
  resolve(error);
807
1025
  return;
808
1026
  }
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
1027
  let chunks = [];
819
1028
  let contentLengthCounter = 0;
820
1029
  let responseHeaders = {};
821
1030
  let status = 0;
822
1031
  let statusText = "";
1032
+ let resolved = false;
1033
+ let isRedirect = false;
1034
+ let timeoutId = null;
1035
+ const sessionErrorHandler = (err) => {
1036
+ if (config.debug) {
1037
+ console.log(`[Rezo Debug] HTTP/2: Session error:`, err.message);
1038
+ }
1039
+ if (!resolved) {
1040
+ resolved = true;
1041
+ if (timeoutId)
1042
+ clearTimeout(timeoutId);
1043
+ const error = buildSmartError(config, fetchOptions, err);
1044
+ _stats.statusOnNext = "error";
1045
+ resolve(error);
1046
+ }
1047
+ };
1048
+ session.on("error", sessionErrorHandler);
1049
+ session.on("goaway", (errorCode, lastStreamID) => {
1050
+ if (config.debug) {
1051
+ console.log(`[Rezo Debug] HTTP/2: Session GOAWAY received (errorCode: ${errorCode}, lastStreamID: ${lastStreamID})`);
1052
+ }
1053
+ });
1054
+ if (config.debug) {
1055
+ console.log(`[Rezo Debug] HTTP/2: Creating request stream...`);
1056
+ }
1057
+ let req;
1058
+ try {
1059
+ req = session.request(headers);
1060
+ } catch (err) {
1061
+ if (config.debug) {
1062
+ console.log(`[Rezo Debug] HTTP/2: Failed to create request stream:`, err.message);
1063
+ }
1064
+ session.removeListener("error", sessionErrorHandler);
1065
+ const error = buildSmartError(config, fetchOptions, err);
1066
+ _stats.statusOnNext = "error";
1067
+ resolve(error);
1068
+ return;
1069
+ }
1070
+ if (config.debug) {
1071
+ console.log(`[Rezo Debug] HTTP/2: Request stream created`);
1072
+ }
1073
+ const requestTimeout = config.timeout || 30000;
1074
+ timeoutId = setTimeout(() => {
1075
+ if (!resolved) {
1076
+ resolved = true;
1077
+ if (config.debug) {
1078
+ console.log(`[Rezo Debug] HTTP/2: Request timeout after ${requestTimeout}ms (no response received)`);
1079
+ }
1080
+ req.close(http2.constants.NGHTTP2_CANCEL);
1081
+ const error = buildSmartError(config, fetchOptions, new Error(`Request timeout after ${requestTimeout}ms`));
1082
+ _stats.statusOnNext = "error";
1083
+ resolve(error);
1084
+ }
1085
+ }, requestTimeout);
1086
+ const sessionSocket = session.socket;
1087
+ if (sessionSocket && typeof sessionSocket.ref === "function") {
1088
+ sessionSocket.ref();
1089
+ }
1090
+ req.on("close", () => {
1091
+ if (config.debug && !resolved) {
1092
+ console.log(`[Rezo Debug] HTTP/2: Stream closed (status: ${status}, resolved: ${resolved})`);
1093
+ }
1094
+ if (!resolved && status === 0) {
1095
+ resolved = true;
1096
+ clearTimeout(timeoutId);
1097
+ if (config.debug) {
1098
+ console.log(`[Rezo Debug] HTTP/2: Stream closed without response - retrying with new session`);
1099
+ }
1100
+ const error = buildSmartError(config, fetchOptions, new Error("HTTP/2 stream closed without response"));
1101
+ _stats.statusOnNext = "error";
1102
+ resolve(error);
1103
+ }
1104
+ });
1105
+ req.on("aborted", () => {
1106
+ if (config.debug) {
1107
+ console.log(`[Rezo Debug] HTTP/2: Stream aborted`);
1108
+ }
1109
+ if (!resolved) {
1110
+ resolved = true;
1111
+ clearTimeout(timeoutId);
1112
+ const error = buildSmartError(config, fetchOptions, new Error("HTTP/2 stream aborted"));
1113
+ _stats.statusOnNext = "error";
1114
+ resolve(error);
1115
+ }
1116
+ });
1117
+ req.on("error", (err) => {
1118
+ if (config.debug) {
1119
+ console.log(`[Rezo Debug] HTTP/2: Stream error:`, err.message);
1120
+ }
1121
+ if (!resolved) {
1122
+ resolved = true;
1123
+ clearTimeout(timeoutId);
1124
+ const error = buildSmartError(config, fetchOptions, err);
1125
+ _stats.statusOnNext = "error";
1126
+ resolve(error);
1127
+ }
1128
+ });
1129
+ req.on("frameError", (type, code, id) => {
1130
+ if (config.debug) {
1131
+ console.log(`[Rezo Debug] HTTP/2: Frame error - type: ${type}, code: ${code}, id: ${id}`);
1132
+ }
1133
+ });
823
1134
  req.on("response", (headers) => {
1135
+ if (config.debug) {
1136
+ console.log(`[Rezo Debug] HTTP/2: Response received, status: ${headers[":status"]}`);
1137
+ }
824
1138
  responseHeaders = headers;
825
1139
  status = Number(headers[http2.constants.HTTP2_HEADER_STATUS]) || 200;
826
1140
  statusText = getStatusText(status);
@@ -829,7 +1143,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
829
1143
  config.timing.responseStart = timing.firstByteTime;
830
1144
  }
831
1145
  const location = headers["location"];
832
- const isRedirect = status >= 300 && status < 400 && location;
1146
+ isRedirect = status >= 300 && status < 400 && !!location;
833
1147
  if (isRedirect) {
834
1148
  _stats.statusOnNext = "redirect";
835
1149
  const redirectUrlObj = new URL(location, url);
@@ -841,7 +1155,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
841
1155
  config.network.httpVersion = "h2";
842
1156
  (async () => {
843
1157
  try {
844
- await updateCookies(config, headers, url.href);
1158
+ await updateCookies(config, headers, url.href, rootJar);
845
1159
  } catch (err) {
846
1160
  if (config.debug) {
847
1161
  console.log("[Rezo Debug] Cookie hook error:", err);
@@ -874,6 +1188,9 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
874
1188
  }
875
1189
  });
876
1190
  req.on("data", (chunk) => {
1191
+ if (config.debug) {
1192
+ console.log(`[Rezo Debug] HTTP/2: Received data chunk: ${chunk.length} bytes (total: ${contentLengthCounter + chunk.length})`);
1193
+ }
877
1194
  chunks.push(chunk);
878
1195
  contentLengthCounter += chunk.length;
879
1196
  if (streamResult) {
@@ -895,209 +1212,221 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
895
1212
  }
896
1213
  });
897
1214
  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);
1215
+ if (resolved)
933
1216
  return;
1217
+ if (config.debug) {
1218
+ console.log(`[Rezo Debug] HTTP/2: Stream 'end' event fired (status: ${status}, chunks: ${chunks.length}, bytes: ${contentLengthCounter})`);
934
1219
  }
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),
1220
+ resolved = true;
1221
+ clearTimeout(timeoutId);
1222
+ try {
1223
+ updateTiming(config, timing, contentLengthCounter);
1224
+ if (!config.transfer) {
1225
+ config.transfer = { requestSize: 0, responseSize: 0, headerSize: 0, bodySize: 0 };
1226
+ }
1227
+ if (config.transfer.requestSize === undefined) {
1228
+ config.transfer.requestSize = 0;
1229
+ }
1230
+ if (config.transfer.requestSize === 0 && body) {
1231
+ if (typeof body === "string") {
1232
+ config.transfer.requestSize = Buffer.byteLength(body, "utf8");
1233
+ } else if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
1234
+ config.transfer.requestSize = body.length;
1235
+ } else if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
1236
+ config.transfer.requestSize = Buffer.byteLength(body.toString(), "utf8");
1237
+ } else if (body instanceof RezoFormData) {
1238
+ config.transfer.requestSize = await body.getLength() || 0;
1239
+ } else if (typeof body === "object") {
1240
+ config.transfer.requestSize = Buffer.byteLength(JSON.stringify(body), "utf8");
1241
+ }
1242
+ }
1243
+ (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
1244
+ if (isRedirect) {
1245
+ _stats.statusOnNext = "redirect";
1246
+ const partialResponse = {
1247
+ data: "",
1248
+ status,
1249
+ statusText,
1250
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1251
+ cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
1252
+ config,
945
1253
  contentType: responseHeaders["content-type"],
946
- contentLength: String(contentLengthCounter),
947
- cookies: config.responseCookies?.setCookiesString || [],
948
- statusText: err.message,
949
- url: url.href,
950
- body: responseBody,
1254
+ contentLength: contentLengthCounter,
951
1255
  finalUrl: url.href,
952
- config,
953
- request: fetchOptions
954
- });
955
- _stats.statusOnNext = "error";
956
- resolve(error);
1256
+ urls: buildUrlTree(config, url.href)
1257
+ };
1258
+ resolve(partialResponse);
957
1259
  return;
958
1260
  }
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");
1261
+ let responseBody = Buffer.concat(chunks);
1262
+ const contentEncoding = responseHeaders["content-encoding"];
1263
+ if (contentEncoding && contentLengthCounter > 0 && CompressionUtil.shouldDecompress(contentEncoding, config)) {
1264
+ try {
1265
+ const decompressed = await decompressBuffer(responseBody, contentEncoding);
1266
+ responseBody = decompressed;
1267
+ } catch (err) {
1268
+ const error = buildDecompressionError({
1269
+ statusCode: status,
1270
+ headers: sanitizeHttp2Headers(responseHeaders),
1271
+ contentType: responseHeaders["content-type"],
1272
+ contentLength: String(contentLengthCounter),
1273
+ cookies: config.responseCookies?.setCookiesString || [],
1274
+ statusText: err.message,
1275
+ url: url.href,
1276
+ body: responseBody,
1277
+ finalUrl: url.href,
1278
+ config,
1279
+ request: fetchOptions
1280
+ });
1281
+ _stats.statusOnNext = "error";
1282
+ resolve(error);
1283
+ return;
1284
+ }
972
1285
  }
973
- } else {
974
- if (contentType.includes("application/json")) {
1286
+ let data;
1287
+ const contentType = responseHeaders["content-type"] || "";
1288
+ const responseType = config.responseType || fetchOptions.responseType || "auto";
1289
+ if (responseType === "buffer" || responseType === "arrayBuffer") {
1290
+ data = responseBody;
1291
+ } else if (responseType === "text") {
1292
+ data = responseBody.toString("utf-8");
1293
+ } else if (responseType === "json" || responseType === "auto" && contentType.includes("application/json")) {
975
1294
  try {
976
1295
  data = JSON.parse(responseBody.toString("utf-8"));
977
1296
  } catch {
978
1297
  data = responseBody.toString("utf-8");
979
1298
  }
980
1299
  } else {
981
- data = responseBody.toString("utf-8");
1300
+ if (contentType.includes("application/json")) {
1301
+ try {
1302
+ data = JSON.parse(responseBody.toString("utf-8"));
1303
+ } catch {
1304
+ data = responseBody.toString("utf-8");
1305
+ }
1306
+ } else {
1307
+ data = responseBody.toString("utf-8");
1308
+ }
982
1309
  }
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 = {
1310
+ config.status = status;
1311
+ config.statusText = statusText;
1312
+ _stats.statusOnNext = status >= 400 ? "error" : "success";
1313
+ const responseCookies = config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] };
1314
+ const mergedCookies = mergeRequestAndResponseCookies(config, responseCookies, url.href);
1315
+ const finalResponse = {
1316
+ data,
1317
+ status,
1318
+ statusText,
1319
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1320
+ cookies: mergedCookies,
1321
+ config,
1322
+ contentType,
1323
+ contentLength: contentLengthCounter,
1324
+ finalUrl: url.href,
1325
+ urls: buildUrlTree(config, url.href)
1326
+ };
1327
+ if (downloadResult && fs && config.fileName) {
1328
+ try {
1329
+ fs.writeFileSync(config.fileName, responseBody);
1330
+ const downloadFinishEvent = {
1331
+ status,
1332
+ statusText,
1333
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1334
+ contentType,
1335
+ contentLength: responseBody.length,
1336
+ finalUrl: url.href,
1337
+ cookies: mergedCookies,
1338
+ urls: buildUrlTree(config, url.href),
1339
+ fileName: config.fileName,
1340
+ fileSize: responseBody.length,
1341
+ timing: {
1342
+ ...getTimingDurations(config),
1343
+ download: getTimingDurations(config).download || 0
1344
+ },
1345
+ averageSpeed: getTimingDurations(config).download ? responseBody.length / getTimingDurations(config).download * 1000 : 0,
1346
+ config: sanitizeConfig(config)
1347
+ };
1348
+ downloadResult.emit("finish", downloadFinishEvent);
1349
+ downloadResult.emit("done", downloadFinishEvent);
1350
+ downloadResult._markFinished();
1351
+ } catch (err) {
1352
+ const error = buildDownloadError({
1353
+ statusCode: status,
1354
+ headers: sanitizeHttp2Headers(responseHeaders),
1355
+ contentType,
1356
+ contentLength: String(contentLengthCounter),
1357
+ cookies: config.responseCookies?.setCookiesString || [],
1358
+ statusText: err.message,
1359
+ url: url.href,
1360
+ body: responseBody,
1361
+ finalUrl: url.href,
1362
+ config,
1363
+ request: fetchOptions
1364
+ });
1365
+ downloadResult.emit("error", error);
1366
+ resolve(error);
1367
+ return;
1368
+ }
1369
+ }
1370
+ if (streamResult) {
1371
+ const streamFinishEvent = {
1005
1372
  status,
1006
1373
  statusText,
1007
1374
  headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1008
1375
  contentType,
1009
- contentLength: responseBody.length,
1376
+ contentLength: contentLengthCounter,
1377
+ finalUrl: url.href,
1378
+ cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
1379
+ urls: buildUrlTree(config, url.href),
1380
+ timing: getTimingDurations(config),
1381
+ config: sanitizeConfig(config)
1382
+ };
1383
+ streamResult.emit("finish", streamFinishEvent);
1384
+ streamResult.emit("done", streamFinishEvent);
1385
+ streamResult.emit("end");
1386
+ streamResult._markFinished();
1387
+ }
1388
+ if (uploadResult) {
1389
+ const uploadFinishEvent = {
1390
+ response: {
1391
+ status,
1392
+ statusText,
1393
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
1394
+ data,
1395
+ contentType,
1396
+ contentLength: contentLengthCounter
1397
+ },
1010
1398
  finalUrl: url.href,
1011
- cookies: mergedCookies,
1399
+ cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
1012
1400
  urls: buildUrlTree(config, url.href),
1013
- fileName: config.fileName,
1014
- fileSize: responseBody.length,
1401
+ uploadSize: config.transfer.requestSize || 0,
1015
1402
  timing: {
1016
1403
  ...getTimingDurations(config),
1017
- download: getTimingDurations(config).download || 0
1404
+ upload: getTimingDurations(config).firstByte || 0,
1405
+ waiting: getTimingDurations(config).download > 0 && getTimingDurations(config).firstByte > 0 ? getTimingDurations(config).download - getTimingDurations(config).firstByte : 0
1018
1406
  },
1019
- averageSpeed: getTimingDurations(config).download ? responseBody.length / getTimingDurations(config).download * 1000 : 0,
1407
+ averageUploadSpeed: getTimingDurations(config).firstByte && config.transfer.requestSize ? config.transfer.requestSize / getTimingDurations(config).firstByte * 1000 : 0,
1408
+ averageDownloadSpeed: getTimingDurations(config).download ? contentLengthCounter / getTimingDurations(config).download * 1000 : 0,
1020
1409
  config: sanitizeConfig(config)
1021
1410
  };
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;
1411
+ uploadResult.emit("finish", uploadFinishEvent);
1412
+ uploadResult.emit("done", uploadFinishEvent);
1413
+ uploadResult._markFinished();
1042
1414
  }
1415
+ resolve(finalResponse);
1416
+ } catch (endError) {
1417
+ if (config.debug) {
1418
+ console.log(`[Rezo Debug] HTTP/2: Error in 'end' handler:`, endError.message);
1419
+ }
1420
+ (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
1421
+ const error = buildSmartError(config, fetchOptions, endError);
1422
+ _stats.statusOnNext = "error";
1423
+ resolve(error);
1043
1424
  }
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
1425
  });
1100
1426
  if (body) {
1427
+ if (config.debug) {
1428
+ console.log(`[Rezo Debug] HTTP/2: Writing request body (type: ${body?.constructor?.name || typeof body})...`);
1429
+ }
1101
1430
  if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
1102
1431
  req.write(body.toString());
1103
1432
  } else if (body instanceof FormData || body instanceof RezoFormData) {
@@ -1107,13 +1436,26 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
1107
1436
  } else if (typeof body === "object" && !Buffer.isBuffer(body) && !(body instanceof Uint8Array) && !(body instanceof Readable)) {
1108
1437
  req.write(JSON.stringify(body));
1109
1438
  } else if (body instanceof Readable) {
1439
+ if (config.debug) {
1440
+ console.log(`[Rezo Debug] HTTP/2: Piping stream body...`);
1441
+ }
1110
1442
  body.pipe(req);
1111
1443
  return;
1112
1444
  } else {
1113
1445
  req.write(body);
1114
1446
  }
1447
+ if (config.debug) {
1448
+ console.log(`[Rezo Debug] HTTP/2: Body written, calling req.end()...`);
1449
+ }
1450
+ } else {
1451
+ if (config.debug) {
1452
+ console.log(`[Rezo Debug] HTTP/2: No body, calling req.end()...`);
1453
+ }
1115
1454
  }
1116
1455
  req.end();
1456
+ if (config.debug) {
1457
+ console.log(`[Rezo Debug] HTTP/2: req.end() called, waiting for response...`);
1458
+ }
1117
1459
  } catch (error) {
1118
1460
  _stats.statusOnNext = "error";
1119
1461
  const rezoError = buildSmartError(config, fetchOptions, error);