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
@@ -4355,6 +4355,71 @@ export declare class Rezo {
4355
4355
  * @see {@link cookieJar} - Access the underlying RezoCookieJar for more control
4356
4356
  */
4357
4357
  clearCookies(): void;
4358
+ /**
4359
+ * Convert a Rezo request configuration to a cURL command string.
4360
+ *
4361
+ * Generates a valid cURL command that can be executed in a terminal to
4362
+ * reproduce the same HTTP request. Useful for:
4363
+ * - Debugging and sharing requests
4364
+ * - Documentation and examples
4365
+ * - Testing requests outside of Node.js
4366
+ * - Exporting requests to other tools
4367
+ *
4368
+ * @param config - Request configuration object
4369
+ * @returns A cURL command string
4370
+ *
4371
+ * @example
4372
+ * ```typescript
4373
+ * const curl = Rezo.toCurl({
4374
+ * url: 'https://api.example.com/users',
4375
+ * method: 'POST',
4376
+ * headers: { 'Content-Type': 'application/json' },
4377
+ * body: { name: 'John', email: 'john@example.com' }
4378
+ * });
4379
+ * // Output: curl -X POST -H 'content-type: application/json' --data-raw '{"name":"John","email":"john@example.com"}' -L --compressed 'https://api.example.com/users'
4380
+ * ```
4381
+ */
4382
+ static toCurl(config: RezoRequestConfig | RezoRequestOptions): string;
4383
+ /**
4384
+ * Parse a cURL command string into a Rezo request configuration.
4385
+ *
4386
+ * Converts a cURL command into a configuration object that can be
4387
+ * passed directly to Rezo request methods. Useful for:
4388
+ * - Importing requests from browser DevTools
4389
+ * - Converting curl examples from API documentation
4390
+ * - Migrating scripts from curl to Rezo
4391
+ *
4392
+ * Supports common cURL options:
4393
+ * - `-X, --request` - HTTP method
4394
+ * - `-H, --header` - Request headers
4395
+ * - `-d, --data, --data-raw, --data-binary` - Request body
4396
+ * - `-u, --user` - Basic authentication
4397
+ * - `-x, --proxy` - Proxy configuration
4398
+ * - `--socks5, --socks4` - SOCKS proxy
4399
+ * - `-L, --location` - Follow redirects
4400
+ * - `--max-redirs` - Maximum redirects
4401
+ * - `--max-time` - Request timeout
4402
+ * - `-k, --insecure` - Skip TLS verification
4403
+ * - `-A, --user-agent` - User agent header
4404
+ *
4405
+ * @param curlCommand - A cURL command string
4406
+ * @returns A request configuration object
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * // From browser DevTools "Copy as cURL"
4411
+ * const config = Rezo.fromCurl(`
4412
+ * curl 'https://api.example.com/data' \\
4413
+ * -H 'Authorization: Bearer token123' \\
4414
+ * -H 'Content-Type: application/json'
4415
+ * `);
4416
+ *
4417
+ * // Use with Rezo
4418
+ * const rezo = new Rezo();
4419
+ * const response = await rezo.request(config);
4420
+ * ```
4421
+ */
4422
+ static fromCurl(curlCommand: string): RezoRequestOptions;
4358
4423
  }
4359
4424
  /**
4360
4425
  * Extended Rezo instance with Axios-compatible static helpers.
@@ -4355,6 +4355,71 @@ export declare class Rezo {
4355
4355
  * @see {@link cookieJar} - Access the underlying RezoCookieJar for more control
4356
4356
  */
4357
4357
  clearCookies(): void;
4358
+ /**
4359
+ * Convert a Rezo request configuration to a cURL command string.
4360
+ *
4361
+ * Generates a valid cURL command that can be executed in a terminal to
4362
+ * reproduce the same HTTP request. Useful for:
4363
+ * - Debugging and sharing requests
4364
+ * - Documentation and examples
4365
+ * - Testing requests outside of Node.js
4366
+ * - Exporting requests to other tools
4367
+ *
4368
+ * @param config - Request configuration object
4369
+ * @returns A cURL command string
4370
+ *
4371
+ * @example
4372
+ * ```typescript
4373
+ * const curl = Rezo.toCurl({
4374
+ * url: 'https://api.example.com/users',
4375
+ * method: 'POST',
4376
+ * headers: { 'Content-Type': 'application/json' },
4377
+ * body: { name: 'John', email: 'john@example.com' }
4378
+ * });
4379
+ * // Output: curl -X POST -H 'content-type: application/json' --data-raw '{"name":"John","email":"john@example.com"}' -L --compressed 'https://api.example.com/users'
4380
+ * ```
4381
+ */
4382
+ static toCurl(config: RezoRequestConfig | RezoRequestOptions): string;
4383
+ /**
4384
+ * Parse a cURL command string into a Rezo request configuration.
4385
+ *
4386
+ * Converts a cURL command into a configuration object that can be
4387
+ * passed directly to Rezo request methods. Useful for:
4388
+ * - Importing requests from browser DevTools
4389
+ * - Converting curl examples from API documentation
4390
+ * - Migrating scripts from curl to Rezo
4391
+ *
4392
+ * Supports common cURL options:
4393
+ * - `-X, --request` - HTTP method
4394
+ * - `-H, --header` - Request headers
4395
+ * - `-d, --data, --data-raw, --data-binary` - Request body
4396
+ * - `-u, --user` - Basic authentication
4397
+ * - `-x, --proxy` - Proxy configuration
4398
+ * - `--socks5, --socks4` - SOCKS proxy
4399
+ * - `-L, --location` - Follow redirects
4400
+ * - `--max-redirs` - Maximum redirects
4401
+ * - `--max-time` - Request timeout
4402
+ * - `-k, --insecure` - Skip TLS verification
4403
+ * - `-A, --user-agent` - User agent header
4404
+ *
4405
+ * @param curlCommand - A cURL command string
4406
+ * @returns A request configuration object
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * // From browser DevTools "Copy as cURL"
4411
+ * const config = Rezo.fromCurl(`
4412
+ * curl 'https://api.example.com/data' \\
4413
+ * -H 'Authorization: Bearer token123' \\
4414
+ * -H 'Content-Type: application/json'
4415
+ * `);
4416
+ *
4417
+ * // Use with Rezo
4418
+ * const rezo = new Rezo();
4419
+ * const response = await rezo.request(config);
4420
+ * ```
4421
+ */
4422
+ static fromCurl(curlCommand: string): RezoRequestOptions;
4358
4423
  }
4359
4424
  /**
4360
4425
  * Extended Rezo instance with Axios-compatible static helpers.
@@ -4355,6 +4355,71 @@ export declare class Rezo {
4355
4355
  * @see {@link cookieJar} - Access the underlying RezoCookieJar for more control
4356
4356
  */
4357
4357
  clearCookies(): void;
4358
+ /**
4359
+ * Convert a Rezo request configuration to a cURL command string.
4360
+ *
4361
+ * Generates a valid cURL command that can be executed in a terminal to
4362
+ * reproduce the same HTTP request. Useful for:
4363
+ * - Debugging and sharing requests
4364
+ * - Documentation and examples
4365
+ * - Testing requests outside of Node.js
4366
+ * - Exporting requests to other tools
4367
+ *
4368
+ * @param config - Request configuration object
4369
+ * @returns A cURL command string
4370
+ *
4371
+ * @example
4372
+ * ```typescript
4373
+ * const curl = Rezo.toCurl({
4374
+ * url: 'https://api.example.com/users',
4375
+ * method: 'POST',
4376
+ * headers: { 'Content-Type': 'application/json' },
4377
+ * body: { name: 'John', email: 'john@example.com' }
4378
+ * });
4379
+ * // Output: curl -X POST -H 'content-type: application/json' --data-raw '{"name":"John","email":"john@example.com"}' -L --compressed 'https://api.example.com/users'
4380
+ * ```
4381
+ */
4382
+ static toCurl(config: RezoRequestConfig | RezoRequestOptions): string;
4383
+ /**
4384
+ * Parse a cURL command string into a Rezo request configuration.
4385
+ *
4386
+ * Converts a cURL command into a configuration object that can be
4387
+ * passed directly to Rezo request methods. Useful for:
4388
+ * - Importing requests from browser DevTools
4389
+ * - Converting curl examples from API documentation
4390
+ * - Migrating scripts from curl to Rezo
4391
+ *
4392
+ * Supports common cURL options:
4393
+ * - `-X, --request` - HTTP method
4394
+ * - `-H, --header` - Request headers
4395
+ * - `-d, --data, --data-raw, --data-binary` - Request body
4396
+ * - `-u, --user` - Basic authentication
4397
+ * - `-x, --proxy` - Proxy configuration
4398
+ * - `--socks5, --socks4` - SOCKS proxy
4399
+ * - `-L, --location` - Follow redirects
4400
+ * - `--max-redirs` - Maximum redirects
4401
+ * - `--max-time` - Request timeout
4402
+ * - `-k, --insecure` - Skip TLS verification
4403
+ * - `-A, --user-agent` - User agent header
4404
+ *
4405
+ * @param curlCommand - A cURL command string
4406
+ * @returns A request configuration object
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * // From browser DevTools "Copy as cURL"
4411
+ * const config = Rezo.fromCurl(`
4412
+ * curl 'https://api.example.com/data' \\
4413
+ * -H 'Authorization: Bearer token123' \\
4414
+ * -H 'Content-Type: application/json'
4415
+ * `);
4416
+ *
4417
+ * // Use with Rezo
4418
+ * const rezo = new Rezo();
4419
+ * const response = await rezo.request(config);
4420
+ * ```
4421
+ */
4422
+ static fromCurl(curlCommand: string): RezoRequestOptions;
4358
4423
  }
4359
4424
  /**
4360
4425
  * Extended Rezo instance with Axios-compatible static helpers.
@@ -4355,6 +4355,71 @@ export declare class Rezo {
4355
4355
  * @see {@link cookieJar} - Access the underlying RezoCookieJar for more control
4356
4356
  */
4357
4357
  clearCookies(): void;
4358
+ /**
4359
+ * Convert a Rezo request configuration to a cURL command string.
4360
+ *
4361
+ * Generates a valid cURL command that can be executed in a terminal to
4362
+ * reproduce the same HTTP request. Useful for:
4363
+ * - Debugging and sharing requests
4364
+ * - Documentation and examples
4365
+ * - Testing requests outside of Node.js
4366
+ * - Exporting requests to other tools
4367
+ *
4368
+ * @param config - Request configuration object
4369
+ * @returns A cURL command string
4370
+ *
4371
+ * @example
4372
+ * ```typescript
4373
+ * const curl = Rezo.toCurl({
4374
+ * url: 'https://api.example.com/users',
4375
+ * method: 'POST',
4376
+ * headers: { 'Content-Type': 'application/json' },
4377
+ * body: { name: 'John', email: 'john@example.com' }
4378
+ * });
4379
+ * // Output: curl -X POST -H 'content-type: application/json' --data-raw '{"name":"John","email":"john@example.com"}' -L --compressed 'https://api.example.com/users'
4380
+ * ```
4381
+ */
4382
+ static toCurl(config: RezoRequestConfig | RezoRequestOptions): string;
4383
+ /**
4384
+ * Parse a cURL command string into a Rezo request configuration.
4385
+ *
4386
+ * Converts a cURL command into a configuration object that can be
4387
+ * passed directly to Rezo request methods. Useful for:
4388
+ * - Importing requests from browser DevTools
4389
+ * - Converting curl examples from API documentation
4390
+ * - Migrating scripts from curl to Rezo
4391
+ *
4392
+ * Supports common cURL options:
4393
+ * - `-X, --request` - HTTP method
4394
+ * - `-H, --header` - Request headers
4395
+ * - `-d, --data, --data-raw, --data-binary` - Request body
4396
+ * - `-u, --user` - Basic authentication
4397
+ * - `-x, --proxy` - Proxy configuration
4398
+ * - `--socks5, --socks4` - SOCKS proxy
4399
+ * - `-L, --location` - Follow redirects
4400
+ * - `--max-redirs` - Maximum redirects
4401
+ * - `--max-time` - Request timeout
4402
+ * - `-k, --insecure` - Skip TLS verification
4403
+ * - `-A, --user-agent` - User agent header
4404
+ *
4405
+ * @param curlCommand - A cURL command string
4406
+ * @returns A request configuration object
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * // From browser DevTools "Copy as cURL"
4411
+ * const config = Rezo.fromCurl(`
4412
+ * curl 'https://api.example.com/data' \\
4413
+ * -H 'Authorization: Bearer token123' \\
4414
+ * -H 'Content-Type: application/json'
4415
+ * `);
4416
+ *
4417
+ * // Use with Rezo
4418
+ * const rezo = new Rezo();
4419
+ * const response = await rezo.request(config);
4420
+ * ```
4421
+ */
4422
+ static fromCurl(curlCommand: string): RezoRequestOptions;
4358
4423
  }
4359
4424
  /**
4360
4425
  * Extended Rezo instance with Axios-compatible static helpers.
@@ -90,9 +90,24 @@ const debugLog = {
90
90
  }
91
91
  }
92
92
  },
93
- complete: (config, url) => {
93
+ redirect: (config, fromUrl, toUrl, statusCode, method) => {
94
94
  if (config.debug) {
95
- console.log(`[Rezo Debug] Request complete: ${url}`);
95
+ console.log(`[Rezo Debug] Redirect ${statusCode}: ${fromUrl}`);
96
+ console.log(`[Rezo Debug] → ${toUrl} (${method})`);
97
+ }
98
+ if (config.trackUrl) {
99
+ console.log(`[Rezo Track] ↳ ${statusCode} → ${toUrl}`);
100
+ }
101
+ },
102
+ complete: (config, url, redirectCount, duration) => {
103
+ if (config.debug) {
104
+ console.log(`[Rezo Debug] Complete: ${url}`);
105
+ if (redirectCount && redirectCount > 0) {
106
+ console.log(`[Rezo Debug] Redirects: ${redirectCount}`);
107
+ }
108
+ if (duration) {
109
+ console.log(`[Rezo Debug] Total Duration: ${duration.toFixed(2)}ms`);
110
+ }
96
111
  console.log(`[Rezo Debug] ─────────────────────────────────────
97
112
  `);
98
113
  }
@@ -435,7 +450,7 @@ async function executeRequest(options, defaultOptions, jar) {
435
450
  uploadResponse = new UploadResponse(url);
436
451
  }
437
452
  try {
438
- const res = executeFetchRequest(fetchOptions, mainConfig, options, perform, streamResponse, downloadResponse, uploadResponse);
453
+ const res = executeFetchRequest(fetchOptions, mainConfig, options, perform, streamResponse, downloadResponse, uploadResponse, jar);
439
454
  if (streamResponse) {
440
455
  return streamResponse;
441
456
  } else if (downloadResponse) {
@@ -462,7 +477,7 @@ async function executeRequest(options, defaultOptions, jar) {
462
477
  throw error;
463
478
  }
464
479
  }
465
- async function executeFetchRequest(fetchOptions, config, options, perform, streamResult, downloadResult, uploadResult) {
480
+ async function executeFetchRequest(fetchOptions, config, options, perform, streamResult, downloadResult, uploadResult, rootJar) {
466
481
  let requestCount = 0;
467
482
  const _stats = { statusOnNext: "abort" };
468
483
  let retries = 0;
@@ -479,6 +494,11 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
479
494
  const visitedUrls = new Set;
480
495
  let totalAttempts = 0;
481
496
  config.setSignal();
497
+ if (!config.requestId) {
498
+ config.requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
499
+ }
500
+ const requestUrl = fetchOptions.fullUrl ? String(fetchOptions.fullUrl) : "";
501
+ debugLog.requestStart(config, requestUrl, fetchOptions.method || "GET");
482
502
  const eventEmitter = streamResult || downloadResult || uploadResult;
483
503
  if (eventEmitter) {
484
504
  eventEmitter.emit("initiated");
@@ -530,6 +550,8 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
530
550
  continue;
531
551
  }
532
552
  if (statusOnNext === "success") {
553
+ const totalDuration = performance.now() - timing.startTime;
554
+ debugLog.complete(config, response.finalUrl || requestUrl, config.redirectCount, totalDuration);
533
555
  return response;
534
556
  }
535
557
  if (statusOnNext === "error") {
@@ -579,12 +601,13 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
579
601
  status: response.status,
580
602
  headers: response.headers,
581
603
  sameDomain: isSameDomain(fetchOptions.fullUrl, location),
582
- method: fetchOptions.method.toUpperCase()
604
+ method: fetchOptions.method.toUpperCase(),
605
+ body: config.originalBody
583
606
  }) : undefined;
584
607
  if (typeof onRedirect !== "undefined") {
585
608
  if (typeof onRedirect === "boolean" && !onRedirect) {
586
609
  throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
587
- } else if (typeof onRedirect === "object" && !onRedirect.redirect) {
610
+ } else if (typeof onRedirect === "object" && !onRedirect.redirect && !onRedirect.withoutBody && !("body" in onRedirect)) {
588
611
  throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
589
612
  }
590
613
  }
@@ -604,13 +627,78 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
604
627
  });
605
628
  perform.reset();
606
629
  config.redirectCount++;
607
- if (response.status === 301 || response.status === 302 || response.status === 303) {
608
- if (config.treat302As303 !== false || response.status === 303) {
609
- options.method = "GET";
630
+ const fromUrl = fetchOptions.fullUrl;
631
+ fetchOptions.fullUrl = location;
632
+ const normalizedRedirect = typeof onRedirect === "object" ? onRedirect.redirect || onRedirect.withoutBody || "body" in onRedirect : undefined;
633
+ if (typeof onRedirect === "object" && normalizedRedirect) {
634
+ let method;
635
+ const userMethod = onRedirect.method;
636
+ if (redirectCode === 301 || redirectCode === 302 || redirectCode === 303) {
637
+ method = userMethod || "GET";
638
+ } else {
639
+ method = userMethod || fetchOptions.method;
640
+ }
641
+ config.method = method;
642
+ options.method = method;
643
+ fetchOptions.method = method;
644
+ if (onRedirect.redirect && onRedirect.url) {
645
+ options.fullUrl = onRedirect.url;
646
+ fetchOptions.fullUrl = onRedirect.url;
647
+ }
648
+ if (onRedirect.withoutBody) {
649
+ delete options.body;
650
+ delete fetchOptions.body;
651
+ config.originalBody = undefined;
652
+ if (fetchOptions.headers instanceof RezoHeaders) {
653
+ fetchOptions.headers.delete("Content-Type");
654
+ fetchOptions.headers.delete("Content-Length");
655
+ }
656
+ } else if ("body" in onRedirect) {
657
+ options.body = onRedirect.body;
658
+ fetchOptions.body = onRedirect.body;
659
+ config.originalBody = onRedirect.body;
660
+ } else if (redirectCode === 307 || redirectCode === 308) {
661
+ const methodUpper = method.toUpperCase();
662
+ if ((methodUpper === "POST" || methodUpper === "PUT" || methodUpper === "PATCH") && config.originalBody !== undefined) {
663
+ options.body = config.originalBody;
664
+ fetchOptions.body = config.originalBody;
665
+ }
666
+ } else {
610
667
  delete options.body;
668
+ delete fetchOptions.body;
669
+ if (fetchOptions.headers instanceof RezoHeaders) {
670
+ fetchOptions.headers.delete("Content-Type");
671
+ fetchOptions.headers.delete("Content-Length");
672
+ }
611
673
  }
674
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, method);
675
+ if (onRedirect.redirect && onRedirect.setHeaders) {
676
+ if (fetchOptions.headers instanceof RezoHeaders) {
677
+ for (const [key, value] of Object.entries(onRedirect.setHeaders)) {
678
+ fetchOptions.headers.set(key, value);
679
+ }
680
+ }
681
+ }
682
+ } else if (redirectCode === 301 || redirectCode === 302 || redirectCode === 303) {
683
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, "GET");
684
+ options.method = "GET";
685
+ fetchOptions.method = "GET";
686
+ config.method = "GET";
687
+ delete options.body;
688
+ delete fetchOptions.body;
689
+ if (fetchOptions.headers instanceof RezoHeaders) {
690
+ fetchOptions.headers.delete("Content-Type");
691
+ fetchOptions.headers.delete("Content-Length");
692
+ }
693
+ } else {
694
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, fetchOptions.method);
695
+ }
696
+ const jarToSync = rootJar || config.cookieJar;
697
+ if (response.cookies?.array?.length > 0 && jarToSync) {
698
+ try {
699
+ jarToSync.setCookiesSync(response.cookies.array, fromUrl);
700
+ } catch (e) {}
612
701
  }
613
- fetchOptions.fullUrl = location;
614
702
  delete options.params;
615
703
  requestCount++;
616
704
  continue;
@@ -640,7 +728,6 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
640
728
  } else if (config.transfer.requestSize === undefined) {
641
729
  config.transfer.requestSize = 0;
642
730
  }
643
- debugLog.requestStart(config, url.href, fetchOptions.method?.toUpperCase() || "GET");
644
731
  }
645
732
  const reqHeaders = fetchOptions.headers instanceof RezoHeaders ? fetchOptions.headers.toObject() : fetchOptions.headers || {};
646
733
  const headers = toFetchHeaders(reqHeaders);
@@ -903,7 +990,6 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
903
990
  uploadResult.emit("done", uploadFinishEvent);
904
991
  uploadResult._markFinished();
905
992
  }
906
- debugLog.complete(config, url.href);
907
993
  return finalResponse;
908
994
  } catch (error) {
909
995
  _stats.statusOnNext = "error";
@@ -90,9 +90,24 @@ const debugLog = {
90
90
  }
91
91
  }
92
92
  },
93
- complete: (config, url) => {
93
+ redirect: (config, fromUrl, toUrl, statusCode, method) => {
94
94
  if (config.debug) {
95
- console.log(`[Rezo Debug] Request complete: ${url}`);
95
+ console.log(`[Rezo Debug] Redirect ${statusCode}: ${fromUrl}`);
96
+ console.log(`[Rezo Debug] → ${toUrl} (${method})`);
97
+ }
98
+ if (config.trackUrl) {
99
+ console.log(`[Rezo Track] ↳ ${statusCode} → ${toUrl}`);
100
+ }
101
+ },
102
+ complete: (config, url, redirectCount, duration) => {
103
+ if (config.debug) {
104
+ console.log(`[Rezo Debug] Complete: ${url}`);
105
+ if (redirectCount && redirectCount > 0) {
106
+ console.log(`[Rezo Debug] Redirects: ${redirectCount}`);
107
+ }
108
+ if (duration) {
109
+ console.log(`[Rezo Debug] Total Duration: ${duration.toFixed(2)}ms`);
110
+ }
96
111
  console.log(`[Rezo Debug] ─────────────────────────────────────
97
112
  `);
98
113
  }
@@ -435,7 +450,7 @@ export async function executeRequest(options, defaultOptions, jar) {
435
450
  uploadResponse = new UploadResponse(url);
436
451
  }
437
452
  try {
438
- const res = executeFetchRequest(fetchOptions, mainConfig, options, perform, streamResponse, downloadResponse, uploadResponse);
453
+ const res = executeFetchRequest(fetchOptions, mainConfig, options, perform, streamResponse, downloadResponse, uploadResponse, jar);
439
454
  if (streamResponse) {
440
455
  return streamResponse;
441
456
  } else if (downloadResponse) {
@@ -462,7 +477,7 @@ export async function executeRequest(options, defaultOptions, jar) {
462
477
  throw error;
463
478
  }
464
479
  }
465
- async function executeFetchRequest(fetchOptions, config, options, perform, streamResult, downloadResult, uploadResult) {
480
+ async function executeFetchRequest(fetchOptions, config, options, perform, streamResult, downloadResult, uploadResult, rootJar) {
466
481
  let requestCount = 0;
467
482
  const _stats = { statusOnNext: "abort" };
468
483
  let retries = 0;
@@ -479,6 +494,11 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
479
494
  const visitedUrls = new Set;
480
495
  let totalAttempts = 0;
481
496
  config.setSignal();
497
+ if (!config.requestId) {
498
+ config.requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
499
+ }
500
+ const requestUrl = fetchOptions.fullUrl ? String(fetchOptions.fullUrl) : "";
501
+ debugLog.requestStart(config, requestUrl, fetchOptions.method || "GET");
482
502
  const eventEmitter = streamResult || downloadResult || uploadResult;
483
503
  if (eventEmitter) {
484
504
  eventEmitter.emit("initiated");
@@ -530,6 +550,8 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
530
550
  continue;
531
551
  }
532
552
  if (statusOnNext === "success") {
553
+ const totalDuration = performance.now() - timing.startTime;
554
+ debugLog.complete(config, response.finalUrl || requestUrl, config.redirectCount, totalDuration);
533
555
  return response;
534
556
  }
535
557
  if (statusOnNext === "error") {
@@ -579,12 +601,13 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
579
601
  status: response.status,
580
602
  headers: response.headers,
581
603
  sameDomain: isSameDomain(fetchOptions.fullUrl, location),
582
- method: fetchOptions.method.toUpperCase()
604
+ method: fetchOptions.method.toUpperCase(),
605
+ body: config.originalBody
583
606
  }) : undefined;
584
607
  if (typeof onRedirect !== "undefined") {
585
608
  if (typeof onRedirect === "boolean" && !onRedirect) {
586
609
  throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
587
- } else if (typeof onRedirect === "object" && !onRedirect.redirect) {
610
+ } else if (typeof onRedirect === "object" && !onRedirect.redirect && !onRedirect.withoutBody && !("body" in onRedirect)) {
588
611
  throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
589
612
  }
590
613
  }
@@ -604,13 +627,78 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
604
627
  });
605
628
  perform.reset();
606
629
  config.redirectCount++;
607
- if (response.status === 301 || response.status === 302 || response.status === 303) {
608
- if (config.treat302As303 !== false || response.status === 303) {
609
- options.method = "GET";
630
+ const fromUrl = fetchOptions.fullUrl;
631
+ fetchOptions.fullUrl = location;
632
+ const normalizedRedirect = typeof onRedirect === "object" ? onRedirect.redirect || onRedirect.withoutBody || "body" in onRedirect : undefined;
633
+ if (typeof onRedirect === "object" && normalizedRedirect) {
634
+ let method;
635
+ const userMethod = onRedirect.method;
636
+ if (redirectCode === 301 || redirectCode === 302 || redirectCode === 303) {
637
+ method = userMethod || "GET";
638
+ } else {
639
+ method = userMethod || fetchOptions.method;
640
+ }
641
+ config.method = method;
642
+ options.method = method;
643
+ fetchOptions.method = method;
644
+ if (onRedirect.redirect && onRedirect.url) {
645
+ options.fullUrl = onRedirect.url;
646
+ fetchOptions.fullUrl = onRedirect.url;
647
+ }
648
+ if (onRedirect.withoutBody) {
649
+ delete options.body;
650
+ delete fetchOptions.body;
651
+ config.originalBody = undefined;
652
+ if (fetchOptions.headers instanceof RezoHeaders) {
653
+ fetchOptions.headers.delete("Content-Type");
654
+ fetchOptions.headers.delete("Content-Length");
655
+ }
656
+ } else if ("body" in onRedirect) {
657
+ options.body = onRedirect.body;
658
+ fetchOptions.body = onRedirect.body;
659
+ config.originalBody = onRedirect.body;
660
+ } else if (redirectCode === 307 || redirectCode === 308) {
661
+ const methodUpper = method.toUpperCase();
662
+ if ((methodUpper === "POST" || methodUpper === "PUT" || methodUpper === "PATCH") && config.originalBody !== undefined) {
663
+ options.body = config.originalBody;
664
+ fetchOptions.body = config.originalBody;
665
+ }
666
+ } else {
610
667
  delete options.body;
668
+ delete fetchOptions.body;
669
+ if (fetchOptions.headers instanceof RezoHeaders) {
670
+ fetchOptions.headers.delete("Content-Type");
671
+ fetchOptions.headers.delete("Content-Length");
672
+ }
611
673
  }
674
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, method);
675
+ if (onRedirect.redirect && onRedirect.setHeaders) {
676
+ if (fetchOptions.headers instanceof RezoHeaders) {
677
+ for (const [key, value] of Object.entries(onRedirect.setHeaders)) {
678
+ fetchOptions.headers.set(key, value);
679
+ }
680
+ }
681
+ }
682
+ } else if (redirectCode === 301 || redirectCode === 302 || redirectCode === 303) {
683
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, "GET");
684
+ options.method = "GET";
685
+ fetchOptions.method = "GET";
686
+ config.method = "GET";
687
+ delete options.body;
688
+ delete fetchOptions.body;
689
+ if (fetchOptions.headers instanceof RezoHeaders) {
690
+ fetchOptions.headers.delete("Content-Type");
691
+ fetchOptions.headers.delete("Content-Length");
692
+ }
693
+ } else {
694
+ debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, fetchOptions.method);
695
+ }
696
+ const jarToSync = rootJar || config.cookieJar;
697
+ if (response.cookies?.array?.length > 0 && jarToSync) {
698
+ try {
699
+ jarToSync.setCookiesSync(response.cookies.array, fromUrl);
700
+ } catch (e) {}
612
701
  }
613
- fetchOptions.fullUrl = location;
614
702
  delete options.params;
615
703
  requestCount++;
616
704
  continue;
@@ -640,7 +728,6 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
640
728
  } else if (config.transfer.requestSize === undefined) {
641
729
  config.transfer.requestSize = 0;
642
730
  }
643
- debugLog.requestStart(config, url.href, fetchOptions.method?.toUpperCase() || "GET");
644
731
  }
645
732
  const reqHeaders = fetchOptions.headers instanceof RezoHeaders ? fetchOptions.headers.toObject() : fetchOptions.headers || {};
646
733
  const headers = toFetchHeaders(reqHeaders);
@@ -903,7 +990,6 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
903
990
  uploadResult.emit("done", uploadFinishEvent);
904
991
  uploadResult._markFinished();
905
992
  }
906
- debugLog.complete(config, url.href);
907
993
  return finalResponse;
908
994
  } catch (error) {
909
995
  _stats.statusOnNext = "error";