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
@@ -1,7 +1,7 @@
1
1
  const { URL } = require("node:url");
2
2
  const { RezoError } = require('../errors/rezo-error.cjs');
3
3
  const { buildSmartError, builErrorFromResponse, buildDownloadError } = require('../responses/buildError.cjs');
4
- const { Cookie } = require('../utils/cookies.cjs');
4
+ const { RezoCookieJar } = require('../utils/cookies.cjs');
5
5
  const RezoFormData = require('../utils/form-data.cjs');
6
6
  const { getDefaultConfig, prepareHTTPOptions } = require('../utils/http-config.cjs');
7
7
  const { RezoHeaders } = require('../utils/headers.cjs');
@@ -105,39 +105,69 @@ function sanitizeConfig(config) {
105
105
  delete sanitized.data;
106
106
  return sanitized;
107
107
  }
108
- function parseCookiesFromHeaders(headers, url) {
109
- const cookies = {
108
+ function parseCookiesFromHeaders(headers, url, config) {
109
+ let setCookieHeaders = [];
110
+ if (typeof headers.getSetCookie === "function") {
111
+ setCookieHeaders = headers.getSetCookie() || [];
112
+ } else {
113
+ const setCookieRaw = headers.get("set-cookie");
114
+ if (setCookieRaw) {
115
+ const splitPattern = /,(?=\s*[A-Za-z0-9_-]+=)/;
116
+ setCookieHeaders = setCookieRaw.split(splitPattern).map((s) => s.trim()).filter(Boolean);
117
+ }
118
+ }
119
+ if (setCookieHeaders.length === 0) {
120
+ return {
121
+ array: [],
122
+ serialized: [],
123
+ netscape: "",
124
+ string: "",
125
+ setCookiesString: []
126
+ };
127
+ }
128
+ const jar = new RezoCookieJar;
129
+ jar.setCookiesSync(setCookieHeaders, url);
130
+ if (config?.enableCookieJar && config?.cookieJar) {
131
+ config.cookieJar.setCookiesSync(setCookieHeaders, url);
132
+ }
133
+ const cookies = jar.cookies();
134
+ cookies.setCookiesString = setCookieHeaders;
135
+ return cookies;
136
+ }
137
+ function mergeRequestAndResponseCookies(config, responseCookies, url) {
138
+ const mergedCookiesArray = [];
139
+ const cookieKeyDomainMap = new Map;
140
+ if (config.requestCookies && config.requestCookies.length > 0) {
141
+ for (const cookie of config.requestCookies) {
142
+ const key = `${cookie.key}|${cookie.domain || ""}`;
143
+ mergedCookiesArray.push(cookie);
144
+ cookieKeyDomainMap.set(key, mergedCookiesArray.length - 1);
145
+ }
146
+ }
147
+ for (const cookie of responseCookies.array) {
148
+ const key = `${cookie.key}|${cookie.domain || ""}`;
149
+ const existingIndex = cookieKeyDomainMap.get(key);
150
+ if (existingIndex !== undefined) {
151
+ mergedCookiesArray[existingIndex] = cookie;
152
+ } else {
153
+ mergedCookiesArray.push(cookie);
154
+ cookieKeyDomainMap.set(key, mergedCookiesArray.length - 1);
155
+ }
156
+ }
157
+ if (mergedCookiesArray.length > 0) {
158
+ const mergedJar = new RezoCookieJar(mergedCookiesArray, url);
159
+ return mergedJar.cookies();
160
+ }
161
+ return {
110
162
  array: [],
111
163
  serialized: [],
112
- netscape: "",
164
+ netscape: `# Netscape HTTP Cookie File
165
+ # This file was generated by Rezo HTTP client
166
+ # Based on uniqhtt cookie implementation
167
+ `,
113
168
  string: "",
114
169
  setCookiesString: []
115
170
  };
116
- const setCookieHeaders = headers.getSetCookie?.() || [];
117
- for (const cookieStr of setCookieHeaders) {
118
- cookies.setCookiesString.push(cookieStr);
119
- const parts = cookieStr.split(";");
120
- const [nameValue] = parts;
121
- const [name, ...valueParts] = nameValue.split("=");
122
- const value = valueParts.join("=");
123
- if (name && value !== undefined) {
124
- const cookie = new Cookie({
125
- key: name.trim(),
126
- value: value.trim(),
127
- domain: new URL(url).hostname,
128
- path: "/",
129
- httpOnly: cookieStr.toLowerCase().includes("httponly"),
130
- secure: cookieStr.toLowerCase().includes("secure"),
131
- sameSite: "lax"
132
- });
133
- cookies.array.push(cookie);
134
- }
135
- }
136
- cookies.string = cookies.array.map((c) => `${c.key}=${c.value}`).join("; ");
137
- cookies.serialized = cookies.array.map((c) => c.toJSON());
138
- cookies.netscape = cookies.array.map((c) => c.toNetscapeFormat()).join(`
139
- `);
140
- return cookies;
141
171
  }
142
172
  function toFetchHeaders(headers) {
143
173
  const fetchHeaders = new Headers;
@@ -181,7 +211,7 @@ async function prepareFetchBody(body) {
181
211
  return new Uint8Array(buffer);
182
212
  }
183
213
  }
184
- const nativeForm = body.toNativeFormData();
214
+ const nativeForm = await body.toNativeFormData();
185
215
  if (nativeForm) {
186
216
  return nativeForm;
187
217
  }
@@ -306,6 +336,8 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
306
336
  const response = await executeSingleFetchRequest(config, fetchOptions, requestCount, timing, _stats, streamResult, downloadResult, uploadResult);
307
337
  const statusOnNext = _stats.statusOnNext;
308
338
  if (response instanceof RezoError) {
339
+ if (!config.errors)
340
+ config.errors = [];
309
341
  config.errors.push({
310
342
  attempt: config.retryAttempts + 1,
311
343
  error: response,
@@ -316,30 +348,50 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
316
348
  throw response;
317
349
  }
318
350
  if (config.retry) {
351
+ let shouldRetry = false;
319
352
  if (config.retry.condition) {
320
353
  const isPassed = await config.retry.condition(response);
321
- if (typeof isPassed === "boolean" && isPassed === false) {
322
- throw response;
323
- }
354
+ shouldRetry = isPassed === true;
324
355
  } else {
325
- if (statusCodes && !statusCodes.includes(response.status || 0)) {
326
- throw response;
327
- }
328
- if (maxRetries <= retries) {
329
- throw response;
330
- }
331
- retries++;
332
- if (retryDelay > 0) {
333
- await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
334
- }
356
+ shouldRetry = statusCodes ? statusCodes.includes(response.status || 0) : true;
357
+ }
358
+ if (!shouldRetry || retries >= maxRetries) {
359
+ throw response;
335
360
  }
361
+ retries++;
336
362
  config.retryAttempts++;
363
+ if (retryDelay > 0) {
364
+ await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
365
+ }
337
366
  }
338
367
  continue;
339
368
  }
340
369
  if (statusOnNext === "success") {
341
370
  return response;
342
371
  }
372
+ if (statusOnNext === "error") {
373
+ const httpError = builErrorFromResponse(`Request failed with status code ${response.status}`, response, config, fetchOptions);
374
+ if (config.retry && statusCodes?.includes(response.status)) {
375
+ if (maxRetries > retries) {
376
+ retries++;
377
+ config.retryAttempts++;
378
+ config.errors.push({
379
+ attempt: config.retryAttempts,
380
+ error: httpError,
381
+ duration: perform.now()
382
+ });
383
+ perform.reset();
384
+ if (config.debug) {
385
+ console.log(`Request failed with status code ${response.status}, retrying...${retryDelay > 0 ? " in " + (incrementDelay ? retryDelay * retries : retryDelay) + "ms" : ""}`);
386
+ }
387
+ if (retryDelay > 0) {
388
+ await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
389
+ }
390
+ continue;
391
+ }
392
+ }
393
+ throw httpError;
394
+ }
343
395
  if (statusOnNext === "redirect") {
344
396
  const location = _stats.redirectUrl;
345
397
  if (!location) {
@@ -418,7 +470,13 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
418
470
  config.isSecure = isSecure;
419
471
  config.finalUrl = url.href;
420
472
  config.network.protocol = isSecure ? "https" : "http";
473
+ config.network.httpVersion = undefined;
421
474
  config.timing.startTimestamp = timing.startTimestamp;
475
+ if (!config.transfer) {
476
+ config.transfer = { requestSize: 0, responseSize: 0, headerSize: 0, bodySize: 0 };
477
+ } else if (config.transfer.requestSize === undefined) {
478
+ config.transfer.requestSize = 0;
479
+ }
422
480
  }
423
481
  const reqHeaders = fetchOptions.headers instanceof RezoHeaders ? fetchOptions.headers.toObject() : fetchOptions.headers || {};
424
482
  const headers = toFetchHeaders(reqHeaders);
@@ -448,7 +506,26 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
448
506
  clearTimeout(timeoutId);
449
507
  });
450
508
  }
509
+ if (body instanceof RezoFormData) {
510
+ const contentType = body.getContentType();
511
+ if (contentType && !headers.has("content-type")) {
512
+ headers.set("content-type", contentType);
513
+ }
514
+ }
451
515
  const preparedBody = await prepareFetchBody(body);
516
+ if (config.transfer && body) {
517
+ if (typeof body === "string") {
518
+ config.transfer.requestSize = body.length;
519
+ } else if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
520
+ config.transfer.requestSize = body.byteLength || body.length;
521
+ } else if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
522
+ config.transfer.requestSize = body.toString().length;
523
+ } else if (body instanceof RezoFormData) {
524
+ config.transfer.requestSize = body.getLengthSync();
525
+ } else if (typeof body === "object" && !(body instanceof Blob) && !(body instanceof ReadableStream)) {
526
+ config.transfer.requestSize = JSON.stringify(body).length;
527
+ }
528
+ }
452
529
  const fetchInit = {
453
530
  method: fetchOptions.method.toUpperCase(),
454
531
  headers,
@@ -482,7 +559,7 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
482
559
  const responseHeaders = fromFetchHeaders(response.headers);
483
560
  const contentType = response.headers.get("content-type") || "";
484
561
  const contentLength = response.headers.get("content-length");
485
- const cookies = parseCookiesFromHeaders(response.headers, url.href);
562
+ const cookies = parseCookiesFromHeaders(response.headers, url.href, config);
486
563
  config.responseCookies = cookies;
487
564
  const location = response.headers.get("location");
488
565
  const isRedirect = status >= 300 && status < 400 && location;
@@ -564,23 +641,16 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
564
641
  const bodySize = bodyBuffer?.byteLength || (typeof responseData === "string" ? responseData.length : 0);
565
642
  config.transfer.bodySize = bodySize;
566
643
  config.transfer.responseSize = bodySize;
567
- if (status >= 400) {
568
- const error = builErrorFromResponse(`HTTP Error ${status}: ${statusText}`, {
569
- status,
570
- statusText,
571
- headers: responseHeaders,
572
- data: responseData
573
- }, config, fetchOptions);
574
- _stats.statusOnNext = "error";
575
- return error;
576
- }
577
- _stats.statusOnNext = "success";
644
+ config.status = status;
645
+ config.statusText = statusText;
646
+ _stats.statusOnNext = status >= 400 ? "error" : "success";
647
+ const mergedCookies = mergeRequestAndResponseCookies(config, cookies, url.href);
578
648
  const finalResponse = {
579
649
  data: responseData,
580
650
  status,
581
651
  statusText,
582
652
  headers: responseHeaders,
583
- cookies,
653
+ cookies: mergedCookies,
584
654
  config,
585
655
  contentType,
586
656
  contentLength: bodySize,
@@ -599,7 +669,7 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
599
669
  contentType,
600
670
  contentLength: buffer.length,
601
671
  finalUrl: url.href,
602
- cookies,
672
+ cookies: mergedCookies,
603
673
  urls: buildUrlTree(config, url.href),
604
674
  fileName: config.fileName,
605
675
  fileSize: buffer.length,
@@ -646,7 +716,7 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
646
716
  contentLength: bodySize
647
717
  },
648
718
  finalUrl: url.href,
649
- cookies,
719
+ cookies: mergedCookies,
650
720
  urls: buildUrlTree(config, url.href),
651
721
  uploadSize: config.transfer.requestSize || 0,
652
722
  timing: {
@@ -1,7 +1,7 @@
1
1
  import { URL } from "node:url";
2
2
  import { RezoError } from '../errors/rezo-error.js';
3
3
  import { buildSmartError, builErrorFromResponse, buildDownloadError } from '../responses/buildError.js';
4
- import { Cookie } from '../utils/cookies.js';
4
+ import { RezoCookieJar } from '../utils/cookies.js';
5
5
  import RezoFormData from '../utils/form-data.js';
6
6
  import { getDefaultConfig, prepareHTTPOptions } from '../utils/http-config.js';
7
7
  import { RezoHeaders } from '../utils/headers.js';
@@ -105,39 +105,69 @@ function sanitizeConfig(config) {
105
105
  delete sanitized.data;
106
106
  return sanitized;
107
107
  }
108
- function parseCookiesFromHeaders(headers, url) {
109
- const cookies = {
108
+ function parseCookiesFromHeaders(headers, url, config) {
109
+ let setCookieHeaders = [];
110
+ if (typeof headers.getSetCookie === "function") {
111
+ setCookieHeaders = headers.getSetCookie() || [];
112
+ } else {
113
+ const setCookieRaw = headers.get("set-cookie");
114
+ if (setCookieRaw) {
115
+ const splitPattern = /,(?=\s*[A-Za-z0-9_-]+=)/;
116
+ setCookieHeaders = setCookieRaw.split(splitPattern).map((s) => s.trim()).filter(Boolean);
117
+ }
118
+ }
119
+ if (setCookieHeaders.length === 0) {
120
+ return {
121
+ array: [],
122
+ serialized: [],
123
+ netscape: "",
124
+ string: "",
125
+ setCookiesString: []
126
+ };
127
+ }
128
+ const jar = new RezoCookieJar;
129
+ jar.setCookiesSync(setCookieHeaders, url);
130
+ if (config?.enableCookieJar && config?.cookieJar) {
131
+ config.cookieJar.setCookiesSync(setCookieHeaders, url);
132
+ }
133
+ const cookies = jar.cookies();
134
+ cookies.setCookiesString = setCookieHeaders;
135
+ return cookies;
136
+ }
137
+ function mergeRequestAndResponseCookies(config, responseCookies, url) {
138
+ const mergedCookiesArray = [];
139
+ const cookieKeyDomainMap = new Map;
140
+ if (config.requestCookies && config.requestCookies.length > 0) {
141
+ for (const cookie of config.requestCookies) {
142
+ const key = `${cookie.key}|${cookie.domain || ""}`;
143
+ mergedCookiesArray.push(cookie);
144
+ cookieKeyDomainMap.set(key, mergedCookiesArray.length - 1);
145
+ }
146
+ }
147
+ for (const cookie of responseCookies.array) {
148
+ const key = `${cookie.key}|${cookie.domain || ""}`;
149
+ const existingIndex = cookieKeyDomainMap.get(key);
150
+ if (existingIndex !== undefined) {
151
+ mergedCookiesArray[existingIndex] = cookie;
152
+ } else {
153
+ mergedCookiesArray.push(cookie);
154
+ cookieKeyDomainMap.set(key, mergedCookiesArray.length - 1);
155
+ }
156
+ }
157
+ if (mergedCookiesArray.length > 0) {
158
+ const mergedJar = new RezoCookieJar(mergedCookiesArray, url);
159
+ return mergedJar.cookies();
160
+ }
161
+ return {
110
162
  array: [],
111
163
  serialized: [],
112
- netscape: "",
164
+ netscape: `# Netscape HTTP Cookie File
165
+ # This file was generated by Rezo HTTP client
166
+ # Based on uniqhtt cookie implementation
167
+ `,
113
168
  string: "",
114
169
  setCookiesString: []
115
170
  };
116
- const setCookieHeaders = headers.getSetCookie?.() || [];
117
- for (const cookieStr of setCookieHeaders) {
118
- cookies.setCookiesString.push(cookieStr);
119
- const parts = cookieStr.split(";");
120
- const [nameValue] = parts;
121
- const [name, ...valueParts] = nameValue.split("=");
122
- const value = valueParts.join("=");
123
- if (name && value !== undefined) {
124
- const cookie = new Cookie({
125
- key: name.trim(),
126
- value: value.trim(),
127
- domain: new URL(url).hostname,
128
- path: "/",
129
- httpOnly: cookieStr.toLowerCase().includes("httponly"),
130
- secure: cookieStr.toLowerCase().includes("secure"),
131
- sameSite: "lax"
132
- });
133
- cookies.array.push(cookie);
134
- }
135
- }
136
- cookies.string = cookies.array.map((c) => `${c.key}=${c.value}`).join("; ");
137
- cookies.serialized = cookies.array.map((c) => c.toJSON());
138
- cookies.netscape = cookies.array.map((c) => c.toNetscapeFormat()).join(`
139
- `);
140
- return cookies;
141
171
  }
142
172
  function toFetchHeaders(headers) {
143
173
  const fetchHeaders = new Headers;
@@ -181,7 +211,7 @@ async function prepareFetchBody(body) {
181
211
  return new Uint8Array(buffer);
182
212
  }
183
213
  }
184
- const nativeForm = body.toNativeFormData();
214
+ const nativeForm = await body.toNativeFormData();
185
215
  if (nativeForm) {
186
216
  return nativeForm;
187
217
  }
@@ -306,6 +336,8 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
306
336
  const response = await executeSingleFetchRequest(config, fetchOptions, requestCount, timing, _stats, streamResult, downloadResult, uploadResult);
307
337
  const statusOnNext = _stats.statusOnNext;
308
338
  if (response instanceof RezoError) {
339
+ if (!config.errors)
340
+ config.errors = [];
309
341
  config.errors.push({
310
342
  attempt: config.retryAttempts + 1,
311
343
  error: response,
@@ -316,30 +348,50 @@ async function executeFetchRequest(fetchOptions, config, options, perform, strea
316
348
  throw response;
317
349
  }
318
350
  if (config.retry) {
351
+ let shouldRetry = false;
319
352
  if (config.retry.condition) {
320
353
  const isPassed = await config.retry.condition(response);
321
- if (typeof isPassed === "boolean" && isPassed === false) {
322
- throw response;
323
- }
354
+ shouldRetry = isPassed === true;
324
355
  } else {
325
- if (statusCodes && !statusCodes.includes(response.status || 0)) {
326
- throw response;
327
- }
328
- if (maxRetries <= retries) {
329
- throw response;
330
- }
331
- retries++;
332
- if (retryDelay > 0) {
333
- await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
334
- }
356
+ shouldRetry = statusCodes ? statusCodes.includes(response.status || 0) : true;
357
+ }
358
+ if (!shouldRetry || retries >= maxRetries) {
359
+ throw response;
335
360
  }
361
+ retries++;
336
362
  config.retryAttempts++;
363
+ if (retryDelay > 0) {
364
+ await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
365
+ }
337
366
  }
338
367
  continue;
339
368
  }
340
369
  if (statusOnNext === "success") {
341
370
  return response;
342
371
  }
372
+ if (statusOnNext === "error") {
373
+ const httpError = builErrorFromResponse(`Request failed with status code ${response.status}`, response, config, fetchOptions);
374
+ if (config.retry && statusCodes?.includes(response.status)) {
375
+ if (maxRetries > retries) {
376
+ retries++;
377
+ config.retryAttempts++;
378
+ config.errors.push({
379
+ attempt: config.retryAttempts,
380
+ error: httpError,
381
+ duration: perform.now()
382
+ });
383
+ perform.reset();
384
+ if (config.debug) {
385
+ console.log(`Request failed with status code ${response.status}, retrying...${retryDelay > 0 ? " in " + (incrementDelay ? retryDelay * retries : retryDelay) + "ms" : ""}`);
386
+ }
387
+ if (retryDelay > 0) {
388
+ await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
389
+ }
390
+ continue;
391
+ }
392
+ }
393
+ throw httpError;
394
+ }
343
395
  if (statusOnNext === "redirect") {
344
396
  const location = _stats.redirectUrl;
345
397
  if (!location) {
@@ -418,7 +470,13 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
418
470
  config.isSecure = isSecure;
419
471
  config.finalUrl = url.href;
420
472
  config.network.protocol = isSecure ? "https" : "http";
473
+ config.network.httpVersion = undefined;
421
474
  config.timing.startTimestamp = timing.startTimestamp;
475
+ if (!config.transfer) {
476
+ config.transfer = { requestSize: 0, responseSize: 0, headerSize: 0, bodySize: 0 };
477
+ } else if (config.transfer.requestSize === undefined) {
478
+ config.transfer.requestSize = 0;
479
+ }
422
480
  }
423
481
  const reqHeaders = fetchOptions.headers instanceof RezoHeaders ? fetchOptions.headers.toObject() : fetchOptions.headers || {};
424
482
  const headers = toFetchHeaders(reqHeaders);
@@ -448,7 +506,26 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
448
506
  clearTimeout(timeoutId);
449
507
  });
450
508
  }
509
+ if (body instanceof RezoFormData) {
510
+ const contentType = body.getContentType();
511
+ if (contentType && !headers.has("content-type")) {
512
+ headers.set("content-type", contentType);
513
+ }
514
+ }
451
515
  const preparedBody = await prepareFetchBody(body);
516
+ if (config.transfer && body) {
517
+ if (typeof body === "string") {
518
+ config.transfer.requestSize = body.length;
519
+ } else if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
520
+ config.transfer.requestSize = body.byteLength || body.length;
521
+ } else if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
522
+ config.transfer.requestSize = body.toString().length;
523
+ } else if (body instanceof RezoFormData) {
524
+ config.transfer.requestSize = body.getLengthSync();
525
+ } else if (typeof body === "object" && !(body instanceof Blob) && !(body instanceof ReadableStream)) {
526
+ config.transfer.requestSize = JSON.stringify(body).length;
527
+ }
528
+ }
452
529
  const fetchInit = {
453
530
  method: fetchOptions.method.toUpperCase(),
454
531
  headers,
@@ -482,7 +559,7 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
482
559
  const responseHeaders = fromFetchHeaders(response.headers);
483
560
  const contentType = response.headers.get("content-type") || "";
484
561
  const contentLength = response.headers.get("content-length");
485
- const cookies = parseCookiesFromHeaders(response.headers, url.href);
562
+ const cookies = parseCookiesFromHeaders(response.headers, url.href, config);
486
563
  config.responseCookies = cookies;
487
564
  const location = response.headers.get("location");
488
565
  const isRedirect = status >= 300 && status < 400 && location;
@@ -564,23 +641,16 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
564
641
  const bodySize = bodyBuffer?.byteLength || (typeof responseData === "string" ? responseData.length : 0);
565
642
  config.transfer.bodySize = bodySize;
566
643
  config.transfer.responseSize = bodySize;
567
- if (status >= 400) {
568
- const error = builErrorFromResponse(`HTTP Error ${status}: ${statusText}`, {
569
- status,
570
- statusText,
571
- headers: responseHeaders,
572
- data: responseData
573
- }, config, fetchOptions);
574
- _stats.statusOnNext = "error";
575
- return error;
576
- }
577
- _stats.statusOnNext = "success";
644
+ config.status = status;
645
+ config.statusText = statusText;
646
+ _stats.statusOnNext = status >= 400 ? "error" : "success";
647
+ const mergedCookies = mergeRequestAndResponseCookies(config, cookies, url.href);
578
648
  const finalResponse = {
579
649
  data: responseData,
580
650
  status,
581
651
  statusText,
582
652
  headers: responseHeaders,
583
- cookies,
653
+ cookies: mergedCookies,
584
654
  config,
585
655
  contentType,
586
656
  contentLength: bodySize,
@@ -599,7 +669,7 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
599
669
  contentType,
600
670
  contentLength: buffer.length,
601
671
  finalUrl: url.href,
602
- cookies,
672
+ cookies: mergedCookies,
603
673
  urls: buildUrlTree(config, url.href),
604
674
  fileName: config.fileName,
605
675
  fileSize: buffer.length,
@@ -646,7 +716,7 @@ async function executeSingleFetchRequest(config, fetchOptions, requestCount, tim
646
716
  contentLength: bodySize
647
717
  },
648
718
  finalUrl: url.href,
649
- cookies,
719
+ cookies: mergedCookies,
650
720
  urls: buildUrlTree(config, url.href),
651
721
  uploadSize: config.transfer.requestSize || 0,
652
722
  timing: {
@@ -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);