rezo 1.0.12 → 1.0.14

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 (44) hide show
  1. package/dist/adapters/curl.cjs +89 -22
  2. package/dist/adapters/curl.js +89 -22
  3. package/dist/adapters/entries/curl.d.ts +8 -0
  4. package/dist/adapters/entries/fetch.d.ts +8 -0
  5. package/dist/adapters/entries/http.d.ts +8 -0
  6. package/dist/adapters/entries/http2.d.ts +8 -0
  7. package/dist/adapters/entries/react-native.d.ts +8 -0
  8. package/dist/adapters/entries/xhr.d.ts +8 -0
  9. package/dist/adapters/fetch.cjs +128 -58
  10. package/dist/adapters/fetch.js +128 -58
  11. package/dist/adapters/http.cjs +43 -13
  12. package/dist/adapters/http.js +43 -13
  13. package/dist/adapters/http2.cjs +136 -58
  14. package/dist/adapters/http2.js +136 -58
  15. package/dist/adapters/index.cjs +6 -6
  16. package/dist/cache/index.cjs +13 -13
  17. package/dist/core/rezo.cjs +27 -6
  18. package/dist/core/rezo.js +27 -6
  19. package/dist/crawler.d.ts +8 -0
  20. package/dist/entries/crawler.cjs +5 -5
  21. package/dist/index.cjs +24 -24
  22. package/dist/index.d.ts +8 -0
  23. package/dist/platform/browser.d.ts +8 -0
  24. package/dist/platform/bun.d.ts +8 -0
  25. package/dist/platform/deno.d.ts +8 -0
  26. package/dist/platform/node.d.ts +8 -0
  27. package/dist/platform/react-native.d.ts +8 -0
  28. package/dist/platform/worker.d.ts +8 -0
  29. package/dist/plugin/index.cjs +36 -36
  30. package/dist/proxy/index.cjs +2 -2
  31. package/dist/queue/index.cjs +8 -8
  32. package/dist/responses/buildError.cjs +5 -1
  33. package/dist/responses/buildError.js +5 -1
  34. package/dist/responses/buildResponse.cjs +2 -0
  35. package/dist/responses/buildResponse.js +2 -0
  36. package/dist/utils/compression.cjs +6 -6
  37. package/dist/utils/compression.js +6 -6
  38. package/dist/utils/form-data.cjs +64 -7
  39. package/dist/utils/form-data.js +64 -7
  40. package/dist/utils/headers.cjs +17 -0
  41. package/dist/utils/headers.js +17 -1
  42. package/dist/utils/http-config.cjs +39 -4
  43. package/dist/utils/http-config.js +39 -4
  44. package/package.json +1 -1
@@ -402,6 +402,29 @@ Redirecting to: ${fetchOptions.fullUrl} using GET method`);
402
402
  options = __.options;
403
403
  continue;
404
404
  }
405
+ if (statusOnNext === "error") {
406
+ const httpError = builErrorFromResponse(`Request failed with status code ${response.status}`, response, config, fetchOptions);
407
+ if (config.retry && statusCodes?.includes(response.status)) {
408
+ if (maxRetries > retries) {
409
+ retries++;
410
+ config.retryAttempts++;
411
+ config.errors.push({
412
+ attempt: config.retryAttempts,
413
+ error: httpError,
414
+ duration: perform.now()
415
+ });
416
+ perform.reset();
417
+ if (config.debug) {
418
+ console.log(`Request failed with status code ${response.status}, retrying...${retryDelay > 0 ? " in " + (incrementDelay ? retryDelay * retries : retryDelay) + "ms" : ""}`);
419
+ }
420
+ if (retryDelay > 0) {
421
+ await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
422
+ }
423
+ continue;
424
+ }
425
+ }
426
+ throw httpError;
427
+ }
405
428
  delete config.beforeRedirect;
406
429
  config.setSignal = () => {};
407
430
  return response;
@@ -821,10 +844,23 @@ async function request(config, fetchOptions, requestCount, timing, _stats, respo
821
844
  }
822
845
  });
823
846
  });
847
+ req.on("error", (error) => {
848
+ _stats.statusOnNext = "error";
849
+ updateTiming(config, timing, "", 0);
850
+ const e = buildSmartError(config, fetchOptions, error);
851
+ const eventEmitter = streamResult || downloadResult || uploadResult;
852
+ if (eventEmitter) {
853
+ eventEmitter.emit("error", e);
854
+ }
855
+ resolve(e);
856
+ return;
857
+ });
858
+ let bodyPiped = false;
824
859
  if (body) {
825
860
  if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
826
861
  req.write(body.toString());
827
862
  } else if (body instanceof FormData || body instanceof RezoFormData) {
863
+ bodyPiped = true;
828
864
  if (body instanceof RezoFormData) {
829
865
  req.setHeader("Content-Type", `multipart/form-data; boundary=${body.getBoundary()}`);
830
866
  body.pipe(req);
@@ -833,24 +869,18 @@ async function request(config, fetchOptions, requestCount, timing, _stats, respo
833
869
  req.setHeader("Content-Type", `multipart/form-data; boundary=${form.getBoundary()}`);
834
870
  form.pipe(req);
835
871
  }
836
- } else if (typeof body === "object" && !(body instanceof Buffer) && !(body instanceof Uint8Array) && !(body instanceof Readable)) {
872
+ } else if (body instanceof Readable) {
873
+ bodyPiped = true;
874
+ body.pipe(req);
875
+ } else if (typeof body === "object" && !(body instanceof Buffer) && !(body instanceof Uint8Array)) {
837
876
  req.write(JSON.stringify(body));
838
877
  } else {
839
878
  req.write(body);
840
879
  }
841
880
  }
842
- req.end();
843
- req.on("error", (error) => {
844
- _stats.statusOnNext = "error";
845
- updateTiming(config, timing, "", 0);
846
- const e = buildSmartError(config, fetchOptions, error);
847
- const eventEmitter = streamResult || downloadResult || uploadResult;
848
- if (eventEmitter) {
849
- eventEmitter.emit("error", e);
850
- }
851
- resolve(e);
852
- return;
853
- });
881
+ if (!bodyPiped) {
882
+ req.end();
883
+ }
854
884
  } catch (error) {
855
885
  _stats.statusOnNext = "error";
856
886
  updateTiming(config, timing, "", 0);
@@ -4,14 +4,15 @@ const { URL } = require("node:url");
4
4
  const { Readable } = require("node:stream");
5
5
  const { RezoError } = require('../errors/rezo-error.cjs');
6
6
  const { buildSmartError, buildDecompressionError, builErrorFromResponse, buildDownloadError } = require('../responses/buildError.cjs');
7
- const { Cookie } = require('../utils/cookies.cjs');
7
+ const { RezoCookieJar } = require('../utils/cookies.cjs');
8
8
  const RezoFormData = require('../utils/form-data.cjs');
9
9
  const { getDefaultConfig, prepareHTTPOptions } = require('../utils/http-config.cjs');
10
- const { RezoHeaders } = require('../utils/headers.cjs');
10
+ const { RezoHeaders, sanitizeHttp2Headers } = require('../utils/headers.cjs');
11
11
  const { RezoURLSearchParams } = require('../utils/data-operations.cjs');
12
12
  const { StreamResponse } = require('../responses/stream.cjs');
13
13
  const { DownloadResponse } = require('../responses/download.cjs');
14
14
  const { UploadResponse } = require('../responses/upload.cjs');
15
+ const { CompressionUtil } = require('../utils/compression.cjs');
15
16
  const { isSameDomain, RezoPerformance } = require('../utils/tools.cjs');
16
17
  const { ResponseCache } = require('../cache/response-cache.cjs');
17
18
  let zstdDecompressSync = null;
@@ -142,15 +143,14 @@ class Http2SessionPool {
142
143
  reject(new Error(`HTTP/2 connection timeout after ${timeout}ms`));
143
144
  }
144
145
  }, timeout) : null;
146
+ if (timeoutId && typeof timeoutId === "object" && "unref" in timeoutId) {
147
+ timeoutId.unref();
148
+ }
145
149
  session.on("connect", () => {
146
150
  if (!settled) {
147
151
  settled = true;
148
152
  if (timeoutId)
149
153
  clearTimeout(timeoutId);
150
- const socket = session.socket;
151
- if (socket && typeof socket.unref === "function") {
152
- socket.unref();
153
- }
154
154
  resolve(session);
155
155
  }
156
156
  });
@@ -170,6 +170,12 @@ class Http2SessionPool {
170
170
  if (entry) {
171
171
  entry.refCount = Math.max(0, entry.refCount - 1);
172
172
  entry.lastUsed = Date.now();
173
+ if (entry.refCount === 0) {
174
+ const socket = entry.session.socket;
175
+ if (socket && typeof socket.unref === "function") {
176
+ socket.unref();
177
+ }
178
+ }
173
179
  }
174
180
  }
175
181
  closeSession(url) {
@@ -275,38 +281,66 @@ function updateCookies(config, headers, url) {
275
281
  if (!setCookieHeaders)
276
282
  return;
277
283
  const cookieHeaderArray = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
278
- if (!config.responseCookies) {
279
- config.responseCookies = {
280
- array: [],
281
- serialized: [],
282
- netscape: "",
283
- string: "",
284
- setCookiesString: []
285
- };
284
+ if (cookieHeaderArray.length === 0)
285
+ return;
286
+ const jar = new RezoCookieJar;
287
+ jar.setCookiesSync(cookieHeaderArray, url);
288
+ if (config.enableCookieJar && config.cookieJar) {
289
+ config.cookieJar.setCookiesSync(cookieHeaderArray, url);
286
290
  }
287
- for (const cookieStr of cookieHeaderArray) {
288
- config.responseCookies.setCookiesString.push(cookieStr);
289
- const parts = cookieStr.split(";");
290
- const [nameValue] = parts;
291
- const [name, ...valueParts] = nameValue.split("=");
292
- const value = valueParts.join("=");
293
- if (name && value !== undefined) {
294
- const cookie = new Cookie({
295
- key: name.trim(),
296
- value: value.trim(),
297
- domain: new URL(url).hostname,
298
- path: "/",
299
- httpOnly: cookieStr.toLowerCase().includes("httponly"),
300
- secure: cookieStr.toLowerCase().includes("secure"),
301
- sameSite: "lax"
302
- });
303
- config.responseCookies.array.push(cookie);
291
+ const cookies = jar.cookies();
292
+ cookies.setCookiesString = cookieHeaderArray;
293
+ if (config.useCookies) {
294
+ const existingArray = config.responseCookies?.array || [];
295
+ for (const cookie of cookies.array) {
296
+ const existingIndex = existingArray.findIndex((c) => c.key === cookie.key && c.domain === cookie.domain);
297
+ if (existingIndex >= 0) {
298
+ existingArray[existingIndex] = cookie;
299
+ } else {
300
+ existingArray.push(cookie);
301
+ }
302
+ }
303
+ const mergedJar = new RezoCookieJar(existingArray, url);
304
+ config.responseCookies = mergedJar.cookies();
305
+ config.responseCookies.setCookiesString = cookieHeaderArray;
306
+ } else {
307
+ config.responseCookies = cookies;
308
+ }
309
+ }
310
+ function mergeRequestAndResponseCookies(config, responseCookies, url) {
311
+ const mergedCookiesArray = [];
312
+ const cookieKeyDomainMap = new Map;
313
+ if (config.requestCookies && config.requestCookies.length > 0) {
314
+ for (const cookie of config.requestCookies) {
315
+ const key = `${cookie.key}|${cookie.domain || ""}`;
316
+ mergedCookiesArray.push(cookie);
317
+ cookieKeyDomainMap.set(key, mergedCookiesArray.length - 1);
304
318
  }
305
319
  }
306
- config.responseCookies.string = config.responseCookies.array.map((c) => `${c.key}=${c.value}`).join("; ");
307
- config.responseCookies.serialized = config.responseCookies.array.map((c) => c.toJSON());
308
- config.responseCookies.netscape = config.responseCookies.array.map((c) => c.toNetscapeFormat()).join(`
309
- `);
320
+ for (const cookie of responseCookies.array) {
321
+ const key = `${cookie.key}|${cookie.domain || ""}`;
322
+ const existingIndex = cookieKeyDomainMap.get(key);
323
+ if (existingIndex !== undefined) {
324
+ mergedCookiesArray[existingIndex] = cookie;
325
+ } else {
326
+ mergedCookiesArray.push(cookie);
327
+ cookieKeyDomainMap.set(key, mergedCookiesArray.length - 1);
328
+ }
329
+ }
330
+ if (mergedCookiesArray.length > 0) {
331
+ const mergedJar = new RezoCookieJar(mergedCookiesArray, url);
332
+ return mergedJar.cookies();
333
+ }
334
+ return {
335
+ array: [],
336
+ serialized: [],
337
+ netscape: `# Netscape HTTP Cookie File
338
+ # This file was generated by Rezo HTTP client
339
+ # Based on uniqhtt cookie implementation
340
+ `,
341
+ string: "",
342
+ setCookiesString: []
343
+ };
310
344
  }
311
345
  async function executeRequest(options, defaultOptions, jar) {
312
346
  if (!options.responseType) {
@@ -484,6 +518,8 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
484
518
  if (fileName && fs && fs.existsSync(fileName)) {
485
519
  fs.unlinkSync(fileName);
486
520
  }
521
+ if (!config.errors)
522
+ config.errors = [];
487
523
  config.errors.push({
488
524
  attempt: config.retryAttempts + 1,
489
525
  error: response,
@@ -524,6 +560,29 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
524
560
  if (statusOnNext === "success") {
525
561
  return response;
526
562
  }
563
+ if (statusOnNext === "error") {
564
+ const httpError = builErrorFromResponse(`Request failed with status code ${response.status}`, response, config, fetchOptions);
565
+ if (config.retry && statusCodes?.includes(response.status)) {
566
+ if (maxRetries > retries) {
567
+ retries++;
568
+ config.retryAttempts++;
569
+ config.errors.push({
570
+ attempt: config.retryAttempts,
571
+ error: httpError,
572
+ duration: perform.now()
573
+ });
574
+ perform.reset();
575
+ if (config.debug) {
576
+ console.log(`Request failed with status code ${response.status}, retrying...${retryDelay > 0 ? " in " + (incrementDelay ? retryDelay * retries : retryDelay) + "ms" : ""}`);
577
+ }
578
+ if (retryDelay > 0) {
579
+ await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
580
+ }
581
+ continue;
582
+ }
583
+ }
584
+ throw httpError;
585
+ }
527
586
  if (statusOnNext === "redirect") {
528
587
  const location = _stats.redirectUrl;
529
588
  if (!location) {
@@ -620,6 +679,13 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
620
679
  if (!headers["accept-encoding"]) {
621
680
  headers["accept-encoding"] = "gzip, deflate, br";
622
681
  }
682
+ if (body instanceof RezoFormData) {
683
+ headers["content-type"] = `multipart/form-data; boundary=${body.getBoundary()}`;
684
+ } else if (body instanceof FormData) {
685
+ const tempForm = await RezoFormData.fromNativeFormData(body);
686
+ headers["content-type"] = `multipart/form-data; boundary=${tempForm.getBoundary()}`;
687
+ fetchOptions._convertedFormData = tempForm;
688
+ }
623
689
  const eventEmitter = streamResult || downloadResult || uploadResult;
624
690
  if (eventEmitter && requestCount === 0) {
625
691
  const startEvent = {
@@ -735,6 +801,25 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
735
801
  config.timing.endTimestamp = Date.now();
736
802
  config.timing.durationMs = performance.now() - timing.startTime;
737
803
  config.timing.transferMs = timing.firstByteTime ? performance.now() - timing.firstByteTime : config.timing.durationMs;
804
+ if (!config.transfer) {
805
+ config.transfer = { requestSize: 0, responseSize: 0, headerSize: 0, bodySize: 0 };
806
+ }
807
+ if (config.transfer.requestSize === undefined) {
808
+ config.transfer.requestSize = 0;
809
+ }
810
+ if (config.transfer.requestSize === 0 && body) {
811
+ if (typeof body === "string") {
812
+ config.transfer.requestSize = Buffer.byteLength(body, "utf8");
813
+ } else if (body instanceof Buffer || body instanceof Uint8Array) {
814
+ config.transfer.requestSize = body.length;
815
+ } else if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
816
+ config.transfer.requestSize = Buffer.byteLength(body.toString(), "utf8");
817
+ } else if (body instanceof RezoFormData) {
818
+ config.transfer.requestSize = body.getLengthSync();
819
+ } else if (typeof body === "object") {
820
+ config.transfer.requestSize = Buffer.byteLength(JSON.stringify(body), "utf8");
821
+ }
822
+ }
738
823
  config.transfer.bodySize = contentLengthCounter;
739
824
  config.transfer.responseSize = contentLengthCounter;
740
825
  (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
@@ -743,7 +828,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
743
828
  data: "",
744
829
  status,
745
830
  statusText,
746
- headers: new RezoHeaders(responseHeaders),
831
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
747
832
  cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
748
833
  config,
749
834
  contentType: responseHeaders["content-type"],
@@ -756,14 +841,14 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
756
841
  }
757
842
  let responseBody = Buffer.concat(chunks);
758
843
  const contentEncoding = responseHeaders["content-encoding"];
759
- if (contentEncoding && contentLengthCounter > 0) {
844
+ if (contentEncoding && contentLengthCounter > 0 && CompressionUtil.shouldDecompress(contentEncoding, config)) {
760
845
  try {
761
846
  const decompressed = await decompressBuffer(responseBody, contentEncoding);
762
847
  responseBody = decompressed;
763
848
  } catch (err) {
764
849
  const error = buildDecompressionError({
765
850
  statusCode: status,
766
- headers: responseHeaders,
851
+ headers: sanitizeHttp2Headers(responseHeaders),
767
852
  contentType: responseHeaders["content-type"],
768
853
  contentLength: String(contentLengthCounter),
769
854
  cookies: config.responseCookies?.setCookiesString || [],
@@ -803,24 +888,17 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
803
888
  data = responseBody.toString("utf-8");
804
889
  }
805
890
  }
806
- if (status >= 400) {
807
- const error = builErrorFromResponse(`HTTP Error ${status}: ${statusText}`, {
808
- status,
809
- statusText,
810
- headers: new RezoHeaders(responseHeaders),
811
- data
812
- }, config, fetchOptions);
813
- _stats.statusOnNext = "error";
814
- resolve(error);
815
- return;
816
- }
817
- _stats.statusOnNext = "success";
891
+ config.status = status;
892
+ config.statusText = statusText;
893
+ _stats.statusOnNext = status >= 400 ? "error" : "success";
894
+ const responseCookies = config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] };
895
+ const mergedCookies = mergeRequestAndResponseCookies(config, responseCookies, url.href);
818
896
  const finalResponse = {
819
897
  data,
820
898
  status,
821
899
  statusText,
822
- headers: new RezoHeaders(responseHeaders),
823
- cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
900
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
901
+ cookies: mergedCookies,
824
902
  config,
825
903
  contentType,
826
904
  contentLength: contentLengthCounter,
@@ -833,11 +911,11 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
833
911
  const downloadFinishEvent = {
834
912
  status,
835
913
  statusText,
836
- headers: new RezoHeaders(responseHeaders),
914
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
837
915
  contentType,
838
916
  contentLength: responseBody.length,
839
917
  finalUrl: url.href,
840
- cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
918
+ cookies: mergedCookies,
841
919
  urls: buildUrlTree(config, url.href),
842
920
  fileName: config.fileName,
843
921
  fileSize: responseBody.length,
@@ -858,7 +936,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
858
936
  } catch (err) {
859
937
  const error = buildDownloadError({
860
938
  statusCode: status,
861
- headers: responseHeaders,
939
+ headers: sanitizeHttp2Headers(responseHeaders),
862
940
  contentType,
863
941
  contentLength: String(contentLengthCounter),
864
942
  cookies: config.responseCookies?.setCookiesString || [],
@@ -878,7 +956,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
878
956
  const streamFinishEvent = {
879
957
  status,
880
958
  statusText,
881
- headers: new RezoHeaders(responseHeaders),
959
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
882
960
  contentType,
883
961
  contentLength: contentLengthCounter,
884
962
  finalUrl: url.href,
@@ -904,7 +982,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
904
982
  response: {
905
983
  status,
906
984
  statusText,
907
- headers: new RezoHeaders(responseHeaders),
985
+ headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
908
986
  data,
909
987
  contentType,
910
988
  contentLength: contentLengthCounter
@@ -949,7 +1027,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
949
1027
  body.pipe(req);
950
1028
  return;
951
1029
  } else {
952
- const form = await RezoFormData.fromNativeFormData(body);
1030
+ const form = fetchOptions._convertedFormData || await RezoFormData.fromNativeFormData(body);
953
1031
  form.pipe(req);
954
1032
  return;
955
1033
  }