create-request 1.5.4 → 1.6.1

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.
@@ -3,98 +3,99 @@
3
3
  /**
4
4
  * Enum for HTTP methods
5
5
  */
6
- exports.HttpMethod = void 0;
7
- (function (HttpMethod) {
8
- HttpMethod["GET"] = "GET";
9
- HttpMethod["PUT"] = "PUT";
10
- HttpMethod["POST"] = "POST";
11
- HttpMethod["HEAD"] = "HEAD";
12
- HttpMethod["PATCH"] = "PATCH";
13
- HttpMethod["DELETE"] = "DELETE";
14
- HttpMethod["OPTIONS"] = "OPTIONS";
15
- })(exports.HttpMethod || (exports.HttpMethod = {}));
6
+ const HttpMethod = {
7
+ GET: "GET",
8
+ PUT: "PUT",
9
+ POST: "POST",
10
+ HEAD: "HEAD",
11
+ PATCH: "PATCH",
12
+ DELETE: "DELETE",
13
+ OPTIONS: "OPTIONS",
14
+ };
16
15
  /**
17
16
  * Enum for request priorities
18
17
  */
19
- exports.RequestPriority = void 0;
20
- (function (RequestPriority) {
21
- RequestPriority["LOW"] = "low";
22
- RequestPriority["HIGH"] = "high";
23
- RequestPriority["AUTO"] = "auto";
24
- })(exports.RequestPriority || (exports.RequestPriority = {}));
18
+ const RequestPriority = {
19
+ LOW: "low",
20
+ HIGH: "high",
21
+ AUTO: "auto",
22
+ };
25
23
  /**
26
24
  * Enum for credentials policies
27
25
  */
28
- exports.CredentialsPolicy = void 0;
29
- (function (CredentialsPolicy) {
30
- CredentialsPolicy["OMIT"] = "omit";
31
- CredentialsPolicy["INCLUDE"] = "include";
32
- CredentialsPolicy["SAME_ORIGIN"] = "same-origin";
33
- })(exports.CredentialsPolicy || (exports.CredentialsPolicy = {}));
26
+ const CredentialsPolicy = {
27
+ OMIT: "omit",
28
+ INCLUDE: "include",
29
+ SAME_ORIGIN: "same-origin",
30
+ };
34
31
  /**
35
32
  * Enum for request modes
36
33
  */
37
- exports.RequestMode = void 0;
38
- (function (RequestMode) {
39
- RequestMode["CORS"] = "cors";
40
- RequestMode["NO_CORS"] = "no-cors";
41
- RequestMode["SAME_ORIGIN"] = "same-origin";
42
- RequestMode["NAVIGATE"] = "navigate";
43
- })(exports.RequestMode || (exports.RequestMode = {}));
34
+ const RequestMode = {
35
+ CORS: "cors",
36
+ NO_CORS: "no-cors",
37
+ SAME_ORIGIN: "same-origin",
38
+ NAVIGATE: "navigate",
39
+ };
44
40
  /**
45
41
  * Enum for redirect modes
46
42
  */
47
- exports.RedirectMode = void 0;
48
- (function (RedirectMode) {
49
- RedirectMode["ERROR"] = "error";
50
- RedirectMode["FOLLOW"] = "follow";
51
- RedirectMode["MANUAL"] = "manual";
52
- })(exports.RedirectMode || (exports.RedirectMode = {}));
43
+ const RedirectMode = {
44
+ ERROR: "error",
45
+ FOLLOW: "follow",
46
+ MANUAL: "manual",
47
+ };
53
48
  /**
54
49
  * Enum for cookie SameSite policies
55
50
  */
56
- exports.SameSitePolicy = void 0;
57
- (function (SameSitePolicy) {
58
- SameSitePolicy["LAX"] = "Lax";
59
- SameSitePolicy["NONE"] = "None";
60
- SameSitePolicy["STRICT"] = "Strict";
61
- })(exports.SameSitePolicy || (exports.SameSitePolicy = {}));
51
+ const SameSitePolicy = {
52
+ LAX: "Lax",
53
+ NONE: "None",
54
+ STRICT: "Strict",
55
+ };
62
56
  /**
63
57
  * Enum for body types
64
58
  */
65
- var BodyType;
66
- (function (BodyType) {
67
- BodyType["JSON"] = "json";
68
- BodyType["STRING"] = "string";
69
- BodyType["BINARY"] = "binary";
70
- })(BodyType || (BodyType = {}));
59
+ const BodyType = {
60
+ JSON: "json",
61
+ STRING: "string",
62
+ BINARY: "binary",
63
+ };
71
64
  /**
72
65
  * Referrer policies for fetch requests
73
66
  */
74
- exports.ReferrerPolicy = void 0;
75
- (function (ReferrerPolicy) {
76
- ReferrerPolicy["ORIGIN"] = "origin";
77
- ReferrerPolicy["UNSAFE_URL"] = "unsafe-url";
78
- ReferrerPolicy["SAME_ORIGIN"] = "same-origin";
79
- ReferrerPolicy["NO_REFERRER"] = "no-referrer";
80
- ReferrerPolicy["STRICT_ORIGIN"] = "strict-origin";
81
- ReferrerPolicy["ORIGIN_WHEN_CROSS_ORIGIN"] = "origin-when-cross-origin";
82
- ReferrerPolicy["NO_REFERRER_WHEN_DOWNGRADE"] = "no-referrer-when-downgrade";
83
- ReferrerPolicy["STRICT_ORIGIN_WHEN_CROSS_ORIGIN"] = "strict-origin-when-cross-origin";
84
- })(exports.ReferrerPolicy || (exports.ReferrerPolicy = {}));
67
+ const ReferrerPolicy = {
68
+ ORIGIN: "origin",
69
+ UNSAFE_URL: "unsafe-url",
70
+ SAME_ORIGIN: "same-origin",
71
+ NO_REFERRER: "no-referrer",
72
+ STRICT_ORIGIN: "strict-origin",
73
+ ORIGIN_WHEN_CROSS_ORIGIN: "origin-when-cross-origin",
74
+ NO_REFERRER_WHEN_DOWNGRADE: "no-referrer-when-downgrade",
75
+ STRICT_ORIGIN_WHEN_CROSS_ORIGIN: "strict-origin-when-cross-origin",
76
+ };
85
77
  /**
86
78
  * Cache modes for fetch requests
87
79
  */
88
- exports.CacheMode = void 0;
89
- (function (CacheMode) {
90
- CacheMode["RELOAD"] = "reload";
91
- CacheMode["DEFAULT"] = "default";
92
- CacheMode["NO_CACHE"] = "no-cache";
93
- CacheMode["NO_STORE"] = "no-store";
94
- CacheMode["FORCE_CACHE"] = "force-cache";
95
- CacheMode["ONLY_IF_CACHED"] = "only-if-cached";
96
- })(exports.CacheMode || (exports.CacheMode = {}));
80
+ const CacheMode = {
81
+ RELOAD: "reload",
82
+ DEFAULT: "default",
83
+ NO_CACHE: "no-cache",
84
+ NO_STORE: "no-store",
85
+ FORCE_CACHE: "force-cache",
86
+ ONLY_IF_CACHED: "only-if-cached",
87
+ };
97
88
 
89
+ /**
90
+ * Extract a message from an unknown thrown value
91
+ * @internal
92
+ */
93
+ const errorMessage = (e) => (e instanceof Error ? e.message : String(e));
94
+ /**
95
+ * Coerce an unknown thrown value to an Error
96
+ * @internal
97
+ */
98
+ const toError = (e) => (e instanceof Error ? e : new Error(String(e)));
98
99
  /**
99
100
  * Error class for HTTP request failures.
100
101
  * Extends the standard Error class with additional context about the failed request.
@@ -108,6 +109,8 @@ exports.CacheMode = void 0;
108
109
  * console.log(`URL: ${error.url}`);
109
110
  * console.log(`Method: ${error.method}`);
110
111
  * console.log(`Status: ${error.status}`);
112
+ * console.log(`Body: ${error.body}`); // Raw response body (if available)
113
+ * console.log(error.getJson()); // Body parsed as JSON (or undefined)
111
114
  * console.log(`Is timeout: ${error.isTimeout}`);
112
115
  * console.log(`Is aborted: ${error.isAborted}`);
113
116
  * }
@@ -118,6 +121,12 @@ class RequestError extends Error {
118
121
  status;
119
122
  /** The Response object if the request received a response before failing */
120
123
  response;
124
+ /**
125
+ * The raw response body as text, if a response was received and its body could be read.
126
+ * `undefined` for errors without a response (network errors, timeouts, aborts)
127
+ * or when the body could not be read.
128
+ */
129
+ body;
121
130
  /** The URL that was requested */
122
131
  url;
123
132
  /** The HTTP method that was used (e.g., 'GET', 'POST') */
@@ -126,6 +135,8 @@ class RequestError extends Error {
126
135
  isTimeout;
127
136
  /** Whether the request was aborted (cancelled) */
128
137
  isAborted;
138
+ /** Cached result of parsing `body` as JSON (lazily populated by getJson) */
139
+ _parsed;
129
140
  /**
130
141
  * Creates a new RequestError instance.
131
142
  *
@@ -135,6 +146,7 @@ class RequestError extends Error {
135
146
  * @param options - Additional error context
136
147
  * @param options.status - HTTP status code if available
137
148
  * @param options.response - The Response object if available
149
+ * @param options.body - The raw response body as text, if available
138
150
  * @param options.isTimeout - Whether this was a timeout error
139
151
  * @param options.isAborted - Whether the request was aborted
140
152
  * @param options.cause - The underlying error that caused this error
@@ -146,6 +158,7 @@ class RequestError extends Error {
146
158
  this.method = method;
147
159
  this.status = options.status;
148
160
  this.response = options.response;
161
+ this.body = options.body;
149
162
  this.isTimeout = !!options.isTimeout;
150
163
  this.isAborted = !!options.isAborted;
151
164
  // For better stack traces in modern environments
@@ -155,6 +168,62 @@ class RequestError extends Error {
155
168
  // Maintains proper prototype chain for instanceof checks
156
169
  Object.setPrototypeOf(this, RequestError.prototype);
157
170
  }
171
+ /**
172
+ * Parses the captured response body (`body`) as JSON.
173
+ * The result is cached, so repeated calls don't re-parse.
174
+ * This method never throws - it returns `undefined` when there is no body
175
+ * or the body is not valid JSON, making it safe to use in error handlers.
176
+ *
177
+ * @returns The parsed JSON body, or `undefined` if no body was captured or it isn't valid JSON
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * try {
182
+ * await create.post('/api/users').withBody(user).getJson();
183
+ * } catch (error) {
184
+ * if (error instanceof RequestError) {
185
+ * const details = error.getJson<{ message: string; code: string }>();
186
+ * console.log(details?.message ?? error.body ?? error.message);
187
+ * }
188
+ * }
189
+ * ```
190
+ */
191
+ getJson() {
192
+ if (this._parsed === undefined && this.body) {
193
+ try {
194
+ this._parsed = JSON.parse(this.body);
195
+ }
196
+ catch {
197
+ // Body is not valid JSON - leave parsedBody undefined
198
+ }
199
+ }
200
+ return this._parsed;
201
+ }
202
+ /**
203
+ * Safely reads the body of a Response as text without consuming it.
204
+ * The response is cloned before reading, so the original body remains readable.
205
+ * Never throws - returns `undefined` if the body is unavailable or cannot be read
206
+ * (e.g., already consumed, locked stream, or read failure).
207
+ *
208
+ * @param response - The Response to read the body from
209
+ * @returns The body as text, or `undefined` if it could not be read
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const body = await RequestError.captureBody(response);
214
+ * throw RequestError.fromResponse(response, url, 'GET', body);
215
+ * ```
216
+ */
217
+ static async captureBody(response) {
218
+ try {
219
+ if (!response.bodyUsed)
220
+ return await response.clone().text();
221
+ }
222
+ catch {
223
+ // Body could not be read (e.g., locked stream or read failure)
224
+ }
225
+ return undefined;
226
+ }
158
227
  /**
159
228
  * Creates a RequestError for a timeout failure.
160
229
  *
@@ -180,20 +249,23 @@ class RequestError extends Error {
180
249
  * @param response - The Response object from the failed request
181
250
  * @param url - The URL that was requested
182
251
  * @param method - The HTTP method that was used
183
- * @returns A RequestError with the status code and response object
252
+ * @param body - The response body as text, if already read (see {@link RequestError.captureBody})
253
+ * @returns A RequestError with the status code, response object, and body (if provided)
184
254
  *
185
255
  * @example
186
256
  * ```typescript
187
257
  * const response = await fetch('/api/users');
188
258
  * if (!response.ok) {
189
- * throw RequestError.fromResponse(response, '/api/users', 'GET');
259
+ * const body = await RequestError.captureBody(response);
260
+ * throw RequestError.fromResponse(response, '/api/users', 'GET', body);
190
261
  * }
191
262
  * ```
192
263
  */
193
- static fromResponse(response, url, method) {
264
+ static fromResponse(response, url, method, body) {
194
265
  return new RequestError(`HTTP ${response.status}`, url, method, {
195
266
  status: response.status,
196
267
  response,
268
+ body,
197
269
  });
198
270
  }
199
271
  /**
@@ -221,55 +293,29 @@ class RequestError extends Error {
221
293
  let message = originalError.message;
222
294
  // Check for Node.js error codes (e.g., from undici/dns errors)
223
295
  const errorCode = originalError.code;
224
- const errorName = originalError.name;
225
296
  const stack = originalError.stack || "";
226
- const errorMessageLower = message.toLowerCase();
227
297
  // Check for timeout errors (Node.js/undici TimeoutError)
228
298
  // Note: While explicit timeouts set via withTimeout() are handled in BaseRequest,
229
- // this detection serves as a safety net for:
230
- // 1. Timeout errors from external AbortControllers (e.g., AbortSignal.timeout())
231
- // 2. Different runtime implementations that may throw timeout errors differently
232
- // 3. Network-level timeouts (ETIMEDOUT)
233
- const isTimeoutError = errorName === "TimeoutError" ||
234
- errorMessageLower.includes("timeout") ||
235
- errorMessageLower.includes("aborted due to timeout") ||
299
+ // this detection serves as a safety net for timeout errors from external
300
+ // AbortControllers, other runtimes, and network-level timeouts (ETIMEDOUT).
301
+ const isTimeoutError = originalError.name === "TimeoutError" ||
302
+ message.toLowerCase().includes("timeout") ||
236
303
  errorCode === "ETIMEDOUT" ||
237
304
  stack.includes("TimeoutError") ||
238
305
  stack.includes("timeout");
239
306
  // If the error message is generic "fetch failed", provide more context
240
307
  if (message === "fetch failed" || message === "Failed to fetch") {
241
308
  // Check for DNS resolution errors
242
- const isDnsError = errorCode === "ENOTFOUND" ||
243
- errorCode === "EAI_AGAIN" ||
244
- errorCode === "EAI_NODATA" ||
245
- stack.includes("getaddrinfo") ||
246
- stack.includes("ENOTFOUND") ||
247
- stack.includes("EAI_AGAIN");
309
+ const isDnsError = errorCode === "ENOTFOUND" || errorCode === "EAI_AGAIN" || errorCode === "EAI_NODATA" || /getaddrinfo|ENOTFOUND|EAI_AGAIN/.test(stack);
248
310
  // Check for connection errors (but not timeout errors)
249
311
  const isConnectionError = !isTimeoutError && (errorCode === "ECONNREFUSED" || errorCode === "ECONNRESET" || stack.includes("ECONNREFUSED") || stack.includes("connect"));
250
- if (isTimeoutError) {
251
- message = `Timeout:${url}`;
252
- }
253
- else if (isDnsError) {
254
- message = `DNS:${url}`;
255
- }
256
- else if (isConnectionError) {
257
- message = `Conn:${url}`;
258
- }
259
- else {
260
- message = `Net:${url}`;
261
- }
312
+ message = (isTimeoutError ? "Timeout:" : isDnsError ? "DNS:" : isConnectionError ? "Conn:" : "Net:") + url;
262
313
  }
263
- const error = new RequestError(message, url, method, {
264
- ...(isTimeoutError ? { isTimeout: true } : {}),
265
- });
266
- // Create a proper RequestError stack trace, but append the original stack for debugging
267
- // This way Node.js will show "RequestError: ..." instead of "TypeError: ..."
314
+ const error = new RequestError(message, url, method, isTimeoutError ? { isTimeout: true } : {});
315
+ // Create a proper RequestError stack trace, but append the original stack for
316
+ // debugging context, so Node.js shows "RequestError: ..." instead of "TypeError: ..."
268
317
  if (originalError.stack) {
269
- // Get the current stack (which will start with RequestError)
270
- const currentStack = error.stack || "";
271
- // Append the original error's stack as "Caused by:" for debugging context
272
- error.stack = `${currentStack}\n\nCaused by: ${originalError.stack}`;
318
+ error.stack = `${error.stack || ""}\n\nCaused by: ${originalError.stack}`;
273
319
  }
274
320
  return error;
275
321
  }
@@ -313,19 +359,19 @@ class ResponseWrapper {
313
359
  url;
314
360
  /** The HTTP method that was used (if available) */
315
361
  method;
316
- response;
317
- graphQLOptions;
362
+ _res;
363
+ _gqlOpts;
318
364
  // Cache the body as the last used method
319
- cachedBlob;
320
- cachedText;
321
- cachedJson;
322
- cachedArrayBuffer;
365
+ _blob;
366
+ _text;
367
+ _json;
368
+ _buf;
323
369
  constructor(response, url, method, graphQLOptions) {
324
- this.response = response;
370
+ this._res = response;
325
371
  this.url = url;
326
372
  this.method = method;
327
373
  if (graphQLOptions) {
328
- this.graphQLOptions = {
374
+ this._gqlOpts = {
329
375
  throwOnError: graphQLOptions.throwOnError,
330
376
  };
331
377
  }
@@ -334,43 +380,65 @@ class ResponseWrapper {
334
380
  * HTTP status code (e.g., 200, 404, 500)
335
381
  */
336
382
  get status() {
337
- return this.response.status;
383
+ return this._res.status;
338
384
  }
339
385
  /**
340
386
  * HTTP status text (e.g., "OK", "Not Found", "Internal Server Error")
341
387
  */
342
388
  get statusText() {
343
- return this.response.statusText;
389
+ return this._res.statusText;
344
390
  }
345
391
  /**
346
392
  * Response headers as a Headers object
347
393
  */
348
394
  get headers() {
349
- return this.response.headers;
395
+ return this._res.headers;
350
396
  }
351
397
  /**
352
398
  * Whether the response status is in the 200-299 range (successful)
353
399
  */
354
400
  get ok() {
355
- return this.response.ok;
401
+ return this._res.ok;
356
402
  }
357
403
  /**
358
404
  * The raw Response object from the fetch API.
359
405
  * Use this if you need direct access to the underlying Response.
360
406
  */
361
407
  get raw() {
362
- return this.response;
408
+ return this._res;
409
+ }
410
+ /**
411
+ * Create a RequestError carrying this response's context
412
+ * @param message - The error message
413
+ * @param withBody - Whether to attach the cached body text to the error
414
+ */
415
+ _err(message, withBody) {
416
+ return new RequestError(message, this.url || "", this.method || "", {
417
+ status: this._res.status,
418
+ response: this._res,
419
+ body: withBody ? this._text : undefined,
420
+ });
421
+ }
422
+ /**
423
+ * Read the response body via the given reader, wrapping failures in a RequestError
424
+ * @throws RequestError if the body has already been consumed or reading fails
425
+ */
426
+ async _read(reader) {
427
+ this._checkUsed();
428
+ try {
429
+ return await reader();
430
+ }
431
+ catch (e) {
432
+ throw this._err(`Read: ${errorMessage(e)}`);
433
+ }
363
434
  }
364
435
  /**
365
436
  * Check if the response body has already been consumed and throw an error if so
366
437
  * @throws RequestError if the body has already been consumed
367
438
  */
368
- checkBodyNotConsumed() {
369
- if (this.response.bodyUsed) {
370
- throw new RequestError("Body used", this.url || "", this.method || "", {
371
- status: this.response.status,
372
- response: this.response,
373
- });
439
+ _checkUsed() {
440
+ if (this._res.bodyUsed) {
441
+ throw this._err("Body used");
374
442
  }
375
443
  }
376
444
  /**
@@ -378,8 +446,8 @@ class ResponseWrapper {
378
446
  * @param data - The parsed JSON data
379
447
  * @throws RequestError if GraphQL response contains errors and throwOnError is enabled
380
448
  */
381
- checkGraphQLErrors(data) {
382
- if (!this.graphQLOptions?.throwOnError || typeof data !== "object" || data === null)
449
+ _checkGql(data) {
450
+ if (!this._gqlOpts?.throwOnError || typeof data !== "object" || data === null)
383
451
  return;
384
452
  const responseData = data;
385
453
  if (!Array.isArray(responseData.errors) || responseData.errors.length === 0)
@@ -408,11 +476,7 @@ class ResponseWrapper {
408
476
  }
409
477
  return String(x);
410
478
  });
411
- const errorMessage = errorMessages.join(", ");
412
- throw new RequestError(`GQL: ${errorMessage}`, this.url || "", this.method || "", {
413
- status: this.response.status,
414
- response: this.response,
415
- });
479
+ throw this._err(`GQL: ${errorMessages.join(", ")}`, true);
416
480
  }
417
481
  /**
418
482
  * Parse the response body as JSON
@@ -441,38 +505,35 @@ class ResponseWrapper {
441
505
  * }
442
506
  */
443
507
  async getJson() {
444
- if (this.cachedJson !== undefined)
445
- return this.cachedJson;
508
+ if (this._json !== undefined)
509
+ return this._json;
446
510
  // Handle empty responses: 204 No Content or content-length: 0
447
- const contentLength = this.response.headers.get("content-length");
448
- if (this.response.status === 204 || contentLength === "0") {
449
- this.cachedJson = null;
511
+ const contentLength = this._res.headers.get("content-length");
512
+ if (this._res.status === 204 || contentLength === "0") {
513
+ this._json = null;
450
514
  return null;
451
515
  }
452
- this.checkBodyNotConsumed();
516
+ this._checkUsed();
453
517
  try {
454
518
  // Read as text first to handle empty bodies and cache for getText()
455
- const text = await this.response.text();
456
- this.cachedText = text;
519
+ const text = await this._res.text();
520
+ this._text = text;
457
521
  // Handle empty or whitespace-only responses
458
522
  if (!text || text.trim() === "") {
459
- this.cachedJson = null;
523
+ this._json = null;
460
524
  return null;
461
525
  }
462
526
  // Parse the text as JSON
463
527
  const parsed = JSON.parse(text);
464
- this.cachedJson = parsed;
465
- this.checkGraphQLErrors(parsed);
528
+ this._json = parsed;
529
+ this._checkGql(parsed);
466
530
  return parsed;
467
531
  }
468
532
  catch (error) {
469
533
  if (error instanceof RequestError) {
470
534
  throw error;
471
535
  }
472
- throw new RequestError(`Bad JSON: ${error instanceof Error ? error.message : String(error)}`, this.url || "", this.method || "", {
473
- status: this.response.status,
474
- response: this.response,
475
- });
536
+ throw this._err(`Bad JSON: ${errorMessage(error)}`, true);
476
537
  }
477
538
  }
478
539
  /**
@@ -489,20 +550,9 @@ class ResponseWrapper {
489
550
  * ```
490
551
  */
491
552
  async getText() {
492
- if (this.cachedText !== undefined)
493
- return this.cachedText;
494
- this.checkBodyNotConsumed();
495
- try {
496
- const text = await this.response.text();
497
- this.cachedText = text;
498
- return text;
499
- }
500
- catch (e) {
501
- throw new RequestError(`Read: ${e instanceof Error ? e.message : String(e)}`, this.url || "", this.method || "", {
502
- status: this.response.status,
503
- response: this.response,
504
- });
505
- }
553
+ if (this._text !== undefined)
554
+ return this._text;
555
+ return (this._text = await this._read(() => this._res.text()));
506
556
  }
507
557
  /**
508
558
  * Get the response body as a Blob.
@@ -520,20 +570,9 @@ class ResponseWrapper {
520
570
  * ```
521
571
  */
522
572
  async getBlob() {
523
- if (this.cachedBlob !== undefined)
524
- return this.cachedBlob;
525
- this.checkBodyNotConsumed();
526
- try {
527
- const blob = await this.response.blob();
528
- this.cachedBlob = blob;
529
- return blob;
530
- }
531
- catch (e) {
532
- throw new RequestError(`Read: ${e instanceof Error ? e.message : String(e)}`, this.url || "", this.method || "", {
533
- status: this.response.status,
534
- response: this.response,
535
- });
536
- }
573
+ if (this._blob !== undefined)
574
+ return this._blob;
575
+ return (this._blob = await this._read(() => this._res.blob()));
537
576
  }
538
577
  /**
539
578
  * Get the response body as an ArrayBuffer.
@@ -551,21 +590,10 @@ class ResponseWrapper {
551
590
  * ```
552
591
  */
553
592
  async getArrayBuffer() {
554
- if (this.cachedArrayBuffer !== undefined) {
555
- return this.cachedArrayBuffer;
556
- }
557
- this.checkBodyNotConsumed();
558
- try {
559
- const arrayBuffer = await this.response.arrayBuffer();
560
- this.cachedArrayBuffer = arrayBuffer;
561
- return arrayBuffer;
562
- }
563
- catch (e) {
564
- throw new RequestError(`Read: ${e instanceof Error ? e.message : String(e)}`, this.url || "", this.method || "", {
565
- status: this.response.status,
566
- response: this.response,
567
- });
593
+ if (this._buf !== undefined) {
594
+ return this._buf;
568
595
  }
596
+ return (this._buf = await this._read(() => this._res.arrayBuffer()));
569
597
  }
570
598
  /**
571
599
  * Get the raw response body as a ReadableStream
@@ -583,8 +611,8 @@ class ResponseWrapper {
583
611
  * }
584
612
  */
585
613
  getBody() {
586
- this.checkBodyNotConsumed();
587
- return this.response.body;
614
+ this._checkUsed();
615
+ return this._res.body;
588
616
  }
589
617
  /**
590
618
  * Extract specific data using a selector function
@@ -634,15 +662,11 @@ class ResponseWrapper {
634
662
  }
635
663
  // Enhance selector errors with context
636
664
  if (selector) {
637
- throw new RequestError(`Selector: ${error instanceof Error ? error.message : String(error)}`, this.url || "", this.method || "", {
638
- status: this.response.status,
639
- response: this.response,
640
- });
665
+ throw this._err(`Selector: ${errorMessage(error)}`, true);
641
666
  }
642
667
  // If we get here and it's not a RequestError, wrap it
643
668
  // This should rarely happen as getJson() should throw RequestError
644
- const errorObj = error instanceof Error ? error : new Error(String(error));
645
- throw RequestError.networkError(this.url || "", this.method || "", errorObj);
669
+ throw RequestError.networkError(this.url || "", this.method || "", toError(error));
646
670
  }
647
671
  }
648
672
  }
@@ -654,20 +678,9 @@ class CookieUtils {
654
678
  * @returns Formatted cookie string for the Cookie header
655
679
  */
656
680
  static formatRequestCookies(cookies) {
657
- const cookiePairs = [];
658
- Object.entries(cookies).forEach(([name, valueOrOptions]) => {
659
- let value;
660
- if (typeof valueOrOptions === "string") {
661
- value = valueOrOptions;
662
- }
663
- else {
664
- // Extract value from options object without validation
665
- value = valueOrOptions.value;
666
- }
667
- // Add the cookie to the request
668
- cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
669
- });
670
- return cookiePairs.join("; ");
681
+ return Object.entries(cookies)
682
+ .map(([name, valueOrOptions]) => `${encodeURIComponent(name)}=${encodeURIComponent(typeof valueOrOptions === "string" ? valueOrOptions : valueOrOptions.value)}`)
683
+ .join("; ");
671
684
  }
672
685
  }
673
686
 
@@ -721,17 +734,11 @@ class CsrfUtils {
721
734
  // If token is longer than 10 chars, perform additional security checks
722
735
  if (token.length > 10) {
723
736
  // Check for valid character set (alphanumeric & common token symbols)
724
- const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
725
- if (!validTokenRegex.test(token)) {
737
+ if (!/^[A-Za-z0-9\-_=+/.]+$/.test(token)) {
726
738
  return false;
727
739
  }
728
740
  // For longer tokens, check for sufficient entropy (at least 2 character types)
729
- const hasUpperCase = /[A-Z]/.test(token);
730
- const hasLowerCase = /[a-z]/.test(token);
731
- const hasNumbers = /[0-9]/.test(token);
732
- const hasSpecials = /[-_=+/.]/.test(token);
733
- const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
734
- return characterTypesCount >= 2;
741
+ return [/[A-Z]/, /[a-z]/, /[0-9]/, /[-_=+/.]/].filter(re => re.test(token)).length >= 2;
735
742
  }
736
743
  // For shorter tokens (8-10 chars), just return true if we reached here
737
744
  return true;
@@ -742,19 +749,19 @@ class CsrfUtils {
742
749
  * Global configuration for create-request
743
750
  */
744
751
  class Config {
745
- static instance;
752
+ static _instance;
746
753
  // CSRF configuration
747
- csrfHeaderName = "X-CSRF-Token";
748
- xsrfCookieName = "XSRF-TOKEN";
749
- xsrfHeaderName = "X-XSRF-TOKEN";
750
- csrfToken = null;
751
- enableAutoXsrf = true;
752
- enableAntiCsrf = true; // X-Requested-With header
754
+ _csrfHeader = "X-CSRF-Token";
755
+ _xsrfCookie = "XSRF-TOKEN";
756
+ _xsrfHeader = "X-XSRF-TOKEN";
757
+ _csrfToken = null;
758
+ _autoXsrf = true;
759
+ _antiCsrf = true; // X-Requested-With header
753
760
  // Interceptor configuration
754
- requestInterceptors = [];
755
- responseInterceptors = [];
756
- errorInterceptors = [];
757
- nextInterceptorId = 1;
761
+ _reqI = [];
762
+ _resI = [];
763
+ _errI = [];
764
+ _nextId = 1;
758
765
  constructor() { }
759
766
  /**
760
767
  * Get the singleton instance of the Config class
@@ -766,10 +773,10 @@ class Config {
766
773
  * config.setCsrfToken('token123');
767
774
  */
768
775
  static getInstance() {
769
- if (!Config.instance) {
770
- Config.instance = new Config();
776
+ if (!Config._instance) {
777
+ Config._instance = new Config();
771
778
  }
772
- return Config.instance;
779
+ return Config._instance;
773
780
  }
774
781
  /**
775
782
  * Set a global CSRF token to be used for all requests
@@ -781,7 +788,7 @@ class Config {
781
788
  * Config.getInstance().setCsrfToken('myToken123');
782
789
  */
783
790
  setCsrfToken(token) {
784
- this.csrfToken = token;
791
+ this._csrfToken = token;
785
792
  return this;
786
793
  }
787
794
  /**
@@ -790,7 +797,7 @@ class Config {
790
797
  * @returns The current CSRF token or null if not set
791
798
  */
792
799
  getCsrfToken() {
793
- return this.csrfToken;
800
+ return this._csrfToken;
794
801
  }
795
802
  /**
796
803
  * Set the CSRF header name used when sending the token
@@ -802,7 +809,7 @@ class Config {
802
809
  * Config.getInstance().setCsrfHeaderName('X-My-CSRF-Token');
803
810
  */
804
811
  setCsrfHeaderName(name) {
805
- this.csrfHeaderName = name;
812
+ this._csrfHeader = name;
806
813
  return this;
807
814
  }
808
815
  /**
@@ -811,7 +818,7 @@ class Config {
811
818
  * @returns The current CSRF header name
812
819
  */
813
820
  getCsrfHeaderName() {
814
- return this.csrfHeaderName;
821
+ return this._csrfHeader;
815
822
  }
816
823
  /**
817
824
  * Set the XSRF cookie name to look for when extracting tokens from cookies
@@ -823,7 +830,7 @@ class Config {
823
830
  * Config.getInstance().setXsrfCookieName('MY-XSRF-COOKIE');
824
831
  */
825
832
  setXsrfCookieName(name) {
826
- this.xsrfCookieName = name;
833
+ this._xsrfCookie = name;
827
834
  return this;
828
835
  }
829
836
  /**
@@ -832,7 +839,7 @@ class Config {
832
839
  * @returns The current XSRF cookie name
833
840
  */
834
841
  getXsrfCookieName() {
835
- return this.xsrfCookieName;
842
+ return this._xsrfCookie;
836
843
  }
837
844
  /**
838
845
  * Set the XSRF header name for sending tokens extracted from cookies
@@ -841,7 +848,7 @@ class Config {
841
848
  * @returns The config instance for chaining
842
849
  */
843
850
  setXsrfHeaderName(name) {
844
- this.xsrfHeaderName = name;
851
+ this._xsrfHeader = name;
845
852
  return this;
846
853
  }
847
854
  /**
@@ -850,7 +857,7 @@ class Config {
850
857
  * @returns The current XSRF header name
851
858
  */
852
859
  getXsrfHeaderName() {
853
- return this.xsrfHeaderName;
860
+ return this._xsrfHeader;
854
861
  }
855
862
  /**
856
863
  * Enable or disable automatic extraction of XSRF tokens from cookies
@@ -864,7 +871,7 @@ class Config {
864
871
  * Config.getInstance().setEnableAutoXsrf(false); // Disable XSRF extraction
865
872
  */
866
873
  setEnableAutoXsrf(enable) {
867
- this.enableAutoXsrf = enable;
874
+ this._autoXsrf = enable;
868
875
  return this;
869
876
  }
870
877
  /**
@@ -873,7 +880,7 @@ class Config {
873
880
  * @returns True if automatic XSRF is enabled
874
881
  */
875
882
  isAutoXsrfEnabled() {
876
- return this.enableAutoXsrf;
883
+ return this._autoXsrf;
877
884
  }
878
885
  /**
879
886
  * Enable or disable automatic addition of anti-CSRF headers
@@ -883,7 +890,7 @@ class Config {
883
890
  * @returns The config instance for chaining
884
891
  */
885
892
  setEnableAntiCsrf(enable) {
886
- this.enableAntiCsrf = enable;
893
+ this._antiCsrf = enable;
887
894
  return this;
888
895
  }
889
896
  /**
@@ -892,7 +899,7 @@ class Config {
892
899
  * @returns True if anti-CSRF protection is enabled
893
900
  */
894
901
  isAntiCsrfEnabled() {
895
- return this.enableAntiCsrf;
902
+ return this._antiCsrf;
896
903
  }
897
904
  /**
898
905
  * Add a global request interceptor
@@ -908,8 +915,8 @@ class Config {
908
915
  * });
909
916
  */
910
917
  addRequestInterceptor(interceptor) {
911
- const id = this.nextInterceptorId++;
912
- this.requestInterceptors.push({ id, interceptor });
918
+ const id = this._nextId++;
919
+ this._reqI.push([id, interceptor]);
913
920
  return id;
914
921
  }
915
922
  /**
@@ -926,8 +933,8 @@ class Config {
926
933
  * });
927
934
  */
928
935
  addResponseInterceptor(interceptor) {
929
- const id = this.nextInterceptorId++;
930
- this.responseInterceptors.push({ id, interceptor });
936
+ const id = this._nextId++;
937
+ this._resI.push([id, interceptor]);
931
938
  return id;
932
939
  }
933
940
  /**
@@ -944,8 +951,8 @@ class Config {
944
951
  * });
945
952
  */
946
953
  addErrorInterceptor(interceptor) {
947
- const id = this.nextInterceptorId++;
948
- this.errorInterceptors.push({ id, interceptor });
954
+ const id = this._nextId++;
955
+ this._errI.push([id, interceptor]);
949
956
  return id;
950
957
  }
951
958
  /**
@@ -957,7 +964,7 @@ class Config {
957
964
  * Config.getInstance().removeRequestInterceptor(id);
958
965
  */
959
966
  removeRequestInterceptor(id) {
960
- this.requestInterceptors = this.requestInterceptors.filter(item => item.id !== id);
967
+ this._reqI = this._reqI.filter(item => item[0] !== id);
961
968
  }
962
969
  /**
963
970
  * Remove a response interceptor by its ID
@@ -968,7 +975,7 @@ class Config {
968
975
  * Config.getInstance().removeResponseInterceptor(id);
969
976
  */
970
977
  removeResponseInterceptor(id) {
971
- this.responseInterceptors = this.responseInterceptors.filter(item => item.id !== id);
978
+ this._resI = this._resI.filter(item => item[0] !== id);
972
979
  }
973
980
  /**
974
981
  * Remove an error interceptor by its ID
@@ -979,7 +986,7 @@ class Config {
979
986
  * Config.getInstance().removeErrorInterceptor(id);
980
987
  */
981
988
  removeErrorInterceptor(id) {
982
- this.errorInterceptors = this.errorInterceptors.filter(item => item.id !== id);
989
+ this._errI = this._errI.filter(item => item[0] !== id);
983
990
  }
984
991
  /**
985
992
  * Clear all interceptors (request, response, and error)
@@ -988,30 +995,30 @@ class Config {
988
995
  * Config.getInstance().clearInterceptors();
989
996
  */
990
997
  clearInterceptors() {
991
- this.requestInterceptors = [];
992
- this.responseInterceptors = [];
993
- this.errorInterceptors = [];
998
+ this._reqI = [];
999
+ this._resI = [];
1000
+ this._errI = [];
994
1001
  }
995
1002
  /**
996
1003
  * Get all global request interceptors (in registration order)
997
1004
  * @internal
998
1005
  */
999
1006
  getRequestInterceptors() {
1000
- return this.requestInterceptors.map(item => item.interceptor);
1007
+ return this._reqI.map(item => item[1]);
1001
1008
  }
1002
1009
  /**
1003
1010
  * Get all global response interceptors (in registration order)
1004
1011
  * @internal
1005
1012
  */
1006
1013
  getResponseInterceptors() {
1007
- return this.responseInterceptors.map(item => item.interceptor);
1014
+ return this._resI.map(item => item[1]);
1008
1015
  }
1009
1016
  /**
1010
1017
  * Get all global error interceptors (in registration order)
1011
1018
  * @internal
1012
1019
  */
1013
1020
  getErrorInterceptors() {
1014
- return this.errorInterceptors.map(item => item.interceptor);
1021
+ return this._errI.map(item => item[1]);
1015
1022
  }
1016
1023
  /**
1017
1024
  * Reset all configuration options to their default values
@@ -1022,12 +1029,12 @@ class Config {
1022
1029
  * Config.getInstance().reset();
1023
1030
  */
1024
1031
  reset() {
1025
- this.csrfToken = null;
1026
- this.csrfHeaderName = "X-CSRF-Token";
1027
- this.enableAntiCsrf = true;
1028
- this.xsrfCookieName = "XSRF-TOKEN";
1029
- this.xsrfHeaderName = "X-XSRF-TOKEN";
1030
- this.enableAutoXsrf = true;
1032
+ this._csrfToken = null;
1033
+ this._csrfHeader = "X-CSRF-Token";
1034
+ this._antiCsrf = true;
1035
+ this._xsrfCookie = "XSRF-TOKEN";
1036
+ this._xsrfHeader = "X-XSRF-TOKEN";
1037
+ this._autoXsrf = true;
1031
1038
  this.clearInterceptors();
1032
1039
  return this;
1033
1040
  }
@@ -1038,53 +1045,54 @@ class Config {
1038
1045
  * Provides the core request building and execution capabilities.
1039
1046
  */
1040
1047
  class BaseRequest {
1041
- url;
1042
- requestOptions = {
1048
+ _url;
1049
+ _opts = {
1043
1050
  headers: {},
1044
1051
  };
1045
- abortController;
1046
- queryParams = new URLSearchParams();
1047
- autoApplyCsrfProtection = true;
1052
+ _ctrl;
1053
+ _fetch;
1054
+ _query = new URLSearchParams();
1055
+ _autoCsrf = true;
1048
1056
  // Per-request interceptors
1049
- requestInterceptors = [];
1050
- responseInterceptors = [];
1051
- errorInterceptors = [];
1057
+ _reqI = [];
1058
+ _resI = [];
1059
+ _errI = [];
1052
1060
  constructor(url) {
1053
- this.url = url;
1061
+ this._url = url;
1054
1062
  }
1055
1063
  /**
1056
1064
  * Get GraphQL options if set (only for BodyRequest subclasses)
1057
1065
  * @returns GraphQL options or undefined
1058
1066
  */
1059
- getGraphQLOptions() {
1067
+ _gql() {
1060
1068
  return undefined;
1061
1069
  }
1062
1070
  /**
1063
1071
  * Creates a fluent API for setting enum-based options
1064
1072
  * Combines direct setter with convenience methods
1065
1073
  */
1066
- createFluentSetter(optionName, options) {
1074
+ _fluent(optionName, options) {
1067
1075
  const fluent = {};
1068
1076
  // Create convenience methods for each enum value
1069
1077
  Object.entries(options).forEach(([key, value]) => {
1070
1078
  fluent[key] = () => {
1071
- this.requestOptions[optionName] = value;
1079
+ this._opts[optionName] = value;
1072
1080
  return this;
1073
1081
  };
1074
1082
  });
1075
1083
  // Create the callable setter
1076
1084
  const callable = (value) => {
1077
- this.requestOptions[optionName] = value;
1085
+ this._opts[optionName] = value;
1078
1086
  return this;
1079
1087
  };
1080
1088
  return Object.assign(callable, fluent);
1081
1089
  }
1082
- validateUrl(url) {
1090
+ _validateUrl(url) {
1083
1091
  const errorMessage = "Bad URL";
1084
1092
  if (!url?.trim())
1085
- throw new RequestError(errorMessage, url, this.method);
1093
+ throw new RequestError(errorMessage, url, this._method);
1086
1094
  if (url.includes("\0") || url.includes("\r") || url.includes("\n")) {
1087
- throw new RequestError(errorMessage, url, this.method);
1095
+ throw new RequestError(errorMessage, url, this._method);
1088
1096
  }
1089
1097
  const trimmed = url.trim();
1090
1098
  if (/^https?:\/\//.test(trimmed)) {
@@ -1092,7 +1100,7 @@ class BaseRequest {
1092
1100
  new URL(trimmed);
1093
1101
  }
1094
1102
  catch {
1095
- throw new RequestError(errorMessage, trimmed, this.method);
1103
+ throw new RequestError(errorMessage, trimmed, this._method);
1096
1104
  }
1097
1105
  }
1098
1106
  }
@@ -1110,16 +1118,12 @@ class BaseRequest {
1110
1118
  */
1111
1119
  withHeaders(headers) {
1112
1120
  // Filter out null and undefined values
1113
- const filteredHeaders = {};
1114
- Object.entries(headers).forEach(([key, value]) => {
1115
- if (value !== null && value !== undefined) {
1116
- filteredHeaders[key] = value;
1117
- }
1118
- });
1119
- this.requestOptions.headers = {
1120
- ...this.getHeadersRecord(),
1121
- ...filteredHeaders,
1122
- };
1121
+ const merged = { ...this._headers() };
1122
+ for (const [key, value] of Object.entries(headers)) {
1123
+ if (value != null)
1124
+ merged[key] = value;
1125
+ }
1126
+ this._opts.headers = merged;
1123
1127
  return this;
1124
1128
  }
1125
1129
  /**
@@ -1148,8 +1152,8 @@ class BaseRequest {
1148
1152
  */
1149
1153
  withTimeout(timeout) {
1150
1154
  if (!Number.isFinite(timeout) || timeout <= 0)
1151
- throw new RequestError("Bad timeout", this.url, this.method);
1152
- this.requestOptions.timeout = timeout;
1155
+ throw new RequestError("Bad timeout", this._url, this._method);
1156
+ this._opts.timeout = timeout;
1153
1157
  return this;
1154
1158
  }
1155
1159
  /**
@@ -1185,30 +1189,22 @@ class BaseRequest {
1185
1189
  * });
1186
1190
  */
1187
1191
  withRetries(retries) {
1188
- if (typeof retries === "number") {
1189
- if (!Number.isInteger(retries) || retries < 0) {
1190
- throw new RequestError(`Bad retries: ${retries}`, this.url, this.method);
1191
- }
1192
- this.requestOptions.retries = retries;
1192
+ const isNumber = typeof retries === "number";
1193
+ const attempts = isNumber ? retries : retries.attempts;
1194
+ if (!Number.isInteger(attempts) || attempts < 0) {
1195
+ throw new RequestError(`Bad ${isNumber ? "retries" : "attempts"}: ${attempts}`, this._url, this._method);
1193
1196
  }
1194
- else {
1195
- // Validate RetryConfig
1196
- if (!Number.isInteger(retries.attempts) || retries.attempts < 0) {
1197
- throw new RequestError(`Bad attempts: ${retries.attempts}`, this.url, this.method);
1197
+ if (!isNumber && retries.delay !== undefined) {
1198
+ const delay = retries.delay;
1199
+ if (typeof delay === "number") {
1200
+ if (!Number.isFinite(delay) || delay < 0)
1201
+ throw new RequestError(`Bad delay: ${delay}`, this._url, this._method);
1198
1202
  }
1199
- // Validate delay if provided
1200
- if (retries.delay !== undefined) {
1201
- if (typeof retries.delay === "number") {
1202
- if (!Number.isFinite(retries.delay) || retries.delay < 0) {
1203
- throw new RequestError(`Bad delay: ${retries.delay}`, this.url, this.method);
1204
- }
1205
- }
1206
- else if (typeof retries.delay !== "function") {
1207
- throw new RequestError(`Bad delay: ${typeof retries.delay}`, this.url, this.method);
1208
- }
1203
+ else if (typeof delay !== "function") {
1204
+ throw new RequestError(`Bad delay: ${typeof delay}`, this._url, this._method);
1209
1205
  }
1210
- this.requestOptions.retries = retries;
1211
1206
  }
1207
+ this._opts.retries = retries;
1212
1208
  return this;
1213
1209
  }
1214
1210
  /**
@@ -1225,7 +1221,7 @@ class BaseRequest {
1225
1221
  * });
1226
1222
  */
1227
1223
  onRetry(callback) {
1228
- this.requestOptions.onRetry = callback;
1224
+ this._opts.onRetry = callback;
1229
1225
  return this;
1230
1226
  }
1231
1227
  /**
@@ -1252,11 +1248,7 @@ class BaseRequest {
1252
1248
  * request.withCredentials.INCLUDE()
1253
1249
  */
1254
1250
  get withCredentials() {
1255
- return this.createFluentSetter("credentials", {
1256
- INCLUDE: exports.CredentialsPolicy.INCLUDE,
1257
- OMIT: exports.CredentialsPolicy.OMIT,
1258
- SAME_ORIGIN: exports.CredentialsPolicy.SAME_ORIGIN,
1259
- });
1251
+ return this._fluent("credentials", CredentialsPolicy);
1260
1252
  }
1261
1253
  /**
1262
1254
  * Allows providing an external AbortController to cancel the request.
@@ -1285,7 +1277,42 @@ class BaseRequest {
1285
1277
  * controller.abort();
1286
1278
  */
1287
1279
  withAbortController(controller) {
1288
- this.abortController = controller;
1280
+ this._ctrl = controller;
1281
+ return this;
1282
+ }
1283
+ /**
1284
+ * Sets a custom fetch implementation used to execute this request.
1285
+ * By default, requests use the global `fetch`. Injecting a custom function unlocks
1286
+ * testing without global mocks, custom undici dispatchers/agents in Node.js,
1287
+ * and framework-specific fetch extensions (e.g. Next.js caching options).
1288
+ *
1289
+ * The provided function receives the final URL and `RequestInit` (after interceptors)
1290
+ * and must return a `Promise<Response>`. It should honor `init.signal` so that
1291
+ * `withTimeout` and `withAbortController` keep working.
1292
+ *
1293
+ * @param fetchFn - A fetch-compatible function
1294
+ * @returns The request instance for chaining
1295
+ * @throws {RequestError} If fetchFn is not a function
1296
+ *
1297
+ * @example
1298
+ * // Testing: inject a stub instead of mocking the global fetch
1299
+ * const stubFetch: FetchFunction = async () => new Response('{"ok":true}');
1300
+ * const data = await create.get('/api/users').withFetch(stubFetch).getJson();
1301
+ *
1302
+ * @example
1303
+ * // Node.js: route through a custom undici agent (proxy, keep-alive tuning, ...)
1304
+ * import { fetch as undiciFetch, Agent } from 'undici';
1305
+ * const agent = new Agent({ keepAliveTimeout: 30_000 });
1306
+ * request.withFetch((url, init) => undiciFetch(url, { ...init, dispatcher: agent }));
1307
+ *
1308
+ * @example
1309
+ * // Next.js: pass caching hints through to the framework's patched fetch
1310
+ * request.withFetch((url, init) => fetch(url, { ...init, next: { revalidate: 60 } }));
1311
+ */
1312
+ withFetch(fetchFn) {
1313
+ if (typeof fetchFn !== "function")
1314
+ throw new RequestError("Bad fetch", this._url, this._method);
1315
+ this._fetch = fetchFn;
1289
1316
  return this;
1290
1317
  }
1291
1318
  /**
@@ -1307,7 +1334,7 @@ class BaseRequest {
1307
1334
  * request.withReferrer("")
1308
1335
  */
1309
1336
  withReferrer(referrer) {
1310
- this.requestOptions.referrer = referrer;
1337
+ this._opts.referrer = referrer;
1311
1338
  return this;
1312
1339
  }
1313
1340
  /**
@@ -1339,16 +1366,7 @@ class BaseRequest {
1339
1366
  * request.withReferrerPolicy.NO_REFERRER()
1340
1367
  */
1341
1368
  get withReferrerPolicy() {
1342
- return this.createFluentSetter("referrerPolicy", {
1343
- ORIGIN: exports.ReferrerPolicy.ORIGIN,
1344
- UNSAFE_URL: exports.ReferrerPolicy.UNSAFE_URL,
1345
- SAME_ORIGIN: exports.ReferrerPolicy.SAME_ORIGIN,
1346
- NO_REFERRER: exports.ReferrerPolicy.NO_REFERRER,
1347
- STRICT_ORIGIN: exports.ReferrerPolicy.STRICT_ORIGIN,
1348
- ORIGIN_WHEN_CROSS_ORIGIN: exports.ReferrerPolicy.ORIGIN_WHEN_CROSS_ORIGIN,
1349
- NO_REFERRER_WHEN_DOWNGRADE: exports.ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE,
1350
- STRICT_ORIGIN_WHEN_CROSS_ORIGIN: exports.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
1351
- });
1369
+ return this._fluent("referrerPolicy", ReferrerPolicy);
1352
1370
  }
1353
1371
  /**
1354
1372
  * Sets how the request handles HTTP redirects (3xx status codes).
@@ -1377,11 +1395,7 @@ class BaseRequest {
1377
1395
  * request.withRedirect.ERROR()
1378
1396
  */
1379
1397
  get withRedirect() {
1380
- return this.createFluentSetter("redirect", {
1381
- FOLLOW: exports.RedirectMode.FOLLOW,
1382
- ERROR: exports.RedirectMode.ERROR,
1383
- MANUAL: exports.RedirectMode.MANUAL,
1384
- });
1398
+ return this._fluent("redirect", RedirectMode);
1385
1399
  }
1386
1400
  /**
1387
1401
  * Sets the keepalive flag for the request. When enabled, the request can continue
@@ -1399,7 +1413,7 @@ class BaseRequest {
1399
1413
  * request.withKeepAlive(true)
1400
1414
  */
1401
1415
  withKeepAlive(keepalive) {
1402
- this.requestOptions.keepalive = keepalive;
1416
+ this._opts.keepalive = keepalive;
1403
1417
  return this;
1404
1418
  }
1405
1419
  /**
@@ -1430,11 +1444,7 @@ class BaseRequest {
1430
1444
  * request.withPriority.LOW()
1431
1445
  */
1432
1446
  get withPriority() {
1433
- return this.createFluentSetter("priority", {
1434
- HIGH: exports.RequestPriority.HIGH,
1435
- LOW: exports.RequestPriority.LOW,
1436
- AUTO: exports.RequestPriority.AUTO,
1437
- });
1447
+ return this._fluent("priority", RequestPriority);
1438
1448
  }
1439
1449
  /**
1440
1450
  * Sets the integrity hash for Subresource Integrity (SRI) verification.
@@ -1457,7 +1467,7 @@ class BaseRequest {
1457
1467
  * request.withIntegrity("sha256-... sha384-...")
1458
1468
  */
1459
1469
  withIntegrity(integrity) {
1460
- this.requestOptions.integrity = integrity;
1470
+ this._opts.integrity = integrity;
1461
1471
  return this;
1462
1472
  }
1463
1473
  /**
@@ -1495,15 +1505,7 @@ class BaseRequest {
1495
1505
  * request.withCache.ONLY_IF_CACHED()
1496
1506
  */
1497
1507
  get withCache() {
1498
- const cacheOptions = {
1499
- DEFAULT: "default",
1500
- NO_STORE: "no-store",
1501
- RELOAD: "reload",
1502
- NO_CACHE: "no-cache",
1503
- FORCE_CACHE: "force-cache",
1504
- ONLY_IF_CACHED: "only-if-cached",
1505
- };
1506
- return this.createFluentSetter("cache", cacheOptions);
1508
+ return this._fluent("cache", CacheMode);
1507
1509
  }
1508
1510
  /**
1509
1511
  * Adds query parameters to the request URL.
@@ -1542,10 +1544,10 @@ class BaseRequest {
1542
1544
  }
1543
1545
  if (Array.isArray(value)) {
1544
1546
  // Handle array values - add multiple entries with the same key
1545
- value.forEach(v => this.queryParams.append(key, String(v)));
1547
+ value.forEach(v => this._query.append(key, String(v)));
1546
1548
  }
1547
1549
  else {
1548
- this.queryParams.append(key, String(value));
1550
+ this._query.append(key, String(value));
1549
1551
  }
1550
1552
  });
1551
1553
  return this;
@@ -1603,12 +1605,7 @@ class BaseRequest {
1603
1605
  * request.withMode.SAME_ORIGIN()
1604
1606
  */
1605
1607
  get withMode() {
1606
- return this.createFluentSetter("mode", {
1607
- CORS: exports.RequestMode.CORS,
1608
- NO_CORS: exports.RequestMode.NO_CORS,
1609
- SAME_ORIGIN: exports.RequestMode.SAME_ORIGIN,
1610
- NAVIGATE: exports.RequestMode.NAVIGATE,
1611
- });
1608
+ return this._fluent("mode", RequestMode);
1612
1609
  }
1613
1610
  /**
1614
1611
  * Sets the Content-Type header for the request.
@@ -1666,28 +1663,26 @@ class BaseRequest {
1666
1663
  * ```
1667
1664
  */
1668
1665
  withBasicAuth(username, password) {
1669
- const credentials = this.encodeBase64(`${username}:${password}`);
1666
+ const credentials = this._b64(`${username}:${password}`);
1670
1667
  return this.withAuthorization(`Basic ${credentials}`);
1671
1668
  }
1672
1669
  /**
1673
1670
  * Cross-environment base64 encoding
1674
1671
  * Works in both browser and Node.js environments
1675
1672
  */
1676
- encodeBase64(str) {
1673
+ _b64(str) {
1677
1674
  // Modern approach using TextEncoder (available in both modern browsers and Node.js)
1678
- if (typeof TextEncoder !== "undefined" && typeof btoa === "function") {
1679
- const encoder = new TextEncoder();
1680
- const bytes = encoder.encode(str);
1681
- return btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
1682
- }
1683
- // Browser environment
1684
- if (typeof btoa === "function")
1675
+ if (typeof btoa === "function") {
1676
+ if (typeof TextEncoder !== "undefined")
1677
+ return btoa(String.fromCharCode(...new TextEncoder().encode(str)));
1678
+ // Browser environment without TextEncoder
1685
1679
  return btoa(str);
1680
+ }
1686
1681
  // Node.js environment
1687
1682
  if (typeof Buffer !== "undefined")
1688
1683
  return Buffer.from(str).toString("base64");
1689
1684
  // Fallback (should never happen in modern environments)
1690
- throw new RequestError("No encoder", this.url, this.method);
1685
+ throw new RequestError("No encoder", this._url, this._method);
1691
1686
  }
1692
1687
  /**
1693
1688
  * Sets a Bearer token for authentication.
@@ -1709,9 +1704,9 @@ class BaseRequest {
1709
1704
  * Safely get headers as a Record<string, string>
1710
1705
  * @returns The headers object
1711
1706
  */
1712
- getHeadersRecord() {
1713
- if (typeof this.requestOptions.headers === "object" && this.requestOptions.headers !== null) {
1714
- return this.requestOptions.headers;
1707
+ _headers() {
1708
+ if (typeof this._opts.headers === "object" && this._opts.headers !== null) {
1709
+ return this._opts.headers;
1715
1710
  }
1716
1711
  return {};
1717
1712
  }
@@ -1720,8 +1715,8 @@ class BaseRequest {
1720
1715
  * @param headerName Header name to check
1721
1716
  * @returns Boolean indicating if the header exists (case-insensitive)
1722
1717
  */
1723
- hasHeader(headerName) {
1724
- const headers = this.getHeadersRecord();
1718
+ _hasHeader(headerName) {
1719
+ const headers = this._headers();
1725
1720
  return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
1726
1721
  }
1727
1722
  /**
@@ -1750,37 +1745,26 @@ class BaseRequest {
1750
1745
  * ```
1751
1746
  */
1752
1747
  withCookies(cookies) {
1753
- const cookieEntries = Object.entries(cookies || {});
1754
- if (cookieEntries.length === 0) {
1748
+ if (Object.keys(cookies || {}).length === 0)
1755
1749
  return this;
1756
- }
1757
- const currentHeaders = this.getHeadersRecord();
1758
- // Get all existing cookie headers with different cases
1750
+ // Collect existing cookie header values (any casing), preserving the first header's case
1751
+ const newHeaders = { ...this._headers() };
1759
1752
  const cookieValues = [];
1760
- const cookieHeaderKeys = [];
1761
- Object.keys(currentHeaders).forEach(key => {
1753
+ let headerName = "";
1754
+ for (const key of Object.keys(newHeaders)) {
1762
1755
  if (key.toLowerCase() === "cookie") {
1763
1756
  // Don't add empty cookie values
1764
- if (currentHeaders[key])
1765
- cookieValues.push(currentHeaders[key]);
1766
- cookieHeaderKeys.push(key);
1757
+ if (newHeaders[key])
1758
+ cookieValues.push(newHeaders[key]);
1759
+ headerName ||= key;
1760
+ delete newHeaders[key];
1767
1761
  }
1768
- });
1769
- // Format the new cookies
1770
- const cookieString = CookieUtils.formatRequestCookies(cookies);
1771
- // Choose which header name to use - preserve existing case if possible
1772
- const headerName = cookieHeaderKeys.length > 0 ? cookieHeaderKeys[0] : "Cookie";
1773
- // Combine all existing cookie values with the new ones
1774
- const combinedCookieValue = [...cookieValues, cookieString].filter(Boolean).join("; ");
1775
- // Create a new headers object without any cookie headers
1776
- const newHeaders = { ...currentHeaders };
1777
- cookieHeaderKeys.forEach(key => {
1778
- delete newHeaders[key];
1779
- });
1780
- // Set the new combined cookie header
1781
- this.requestOptions.headers = {
1762
+ }
1763
+ // Combine all existing cookie values with the newly formatted ones
1764
+ cookieValues.push(CookieUtils.formatRequestCookies(cookies));
1765
+ this._opts.headers = {
1782
1766
  ...newHeaders,
1783
- [headerName]: combinedCookieValue,
1767
+ [headerName || "Cookie"]: cookieValues.filter(Boolean).join("; "),
1784
1768
  };
1785
1769
  return this;
1786
1770
  }
@@ -1834,7 +1818,7 @@ class BaseRequest {
1834
1818
  * @returns The instance for chaining
1835
1819
  */
1836
1820
  withoutCsrfProtection() {
1837
- this.autoApplyCsrfProtection = false;
1821
+ this._autoCsrf = false;
1838
1822
  return this;
1839
1823
  }
1840
1824
  /**
@@ -1858,7 +1842,7 @@ class BaseRequest {
1858
1842
  * });
1859
1843
  */
1860
1844
  withRequestInterceptor(interceptor) {
1861
- this.requestInterceptors.push(interceptor);
1845
+ this._reqI.push(interceptor);
1862
1846
  return this;
1863
1847
  }
1864
1848
  /**
@@ -1875,7 +1859,7 @@ class BaseRequest {
1875
1859
  * });
1876
1860
  */
1877
1861
  withResponseInterceptor(interceptor) {
1878
- this.responseInterceptors.push(interceptor);
1862
+ this._resI.push(interceptor);
1879
1863
  return this;
1880
1864
  }
1881
1865
  /**
@@ -1892,7 +1876,7 @@ class BaseRequest {
1892
1876
  * });
1893
1877
  */
1894
1878
  withErrorInterceptor(interceptor) {
1895
- this.errorInterceptors.push(interceptor);
1879
+ this._errI.push(interceptor);
1896
1880
  return this;
1897
1881
  }
1898
1882
  /**
@@ -1906,13 +1890,13 @@ class BaseRequest {
1906
1890
  * console.log(response.status);
1907
1891
  */
1908
1892
  async getResponse() {
1909
- const url = this.formatUrlWithQueryParams(this.url);
1910
- this.applyCsrfProtection();
1893
+ const url = this._fullUrl(this._url);
1894
+ this._applyCsrf();
1911
1895
  const fetchOptions = {
1912
- ...this.requestOptions,
1913
- method: this.method,
1896
+ ...this._opts,
1897
+ method: this._method,
1914
1898
  };
1915
- return !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
1899
+ return !this._opts.retries ? this._run(url, fetchOptions) : this._retry(url, fetchOptions);
1916
1900
  }
1917
1901
  /**
1918
1902
  * Execute the request and parse the response as JSON
@@ -2053,8 +2037,8 @@ class BaseRequest {
2053
2037
  /**
2054
2038
  * Apply CSRF protection headers based on configuration
2055
2039
  */
2056
- applyCsrfProtection() {
2057
- if (!this.autoApplyCsrfProtection)
2040
+ _applyCsrf() {
2041
+ if (!this._autoCsrf)
2058
2042
  return;
2059
2043
  const config = Config.getInstance();
2060
2044
  // Apply anti-CSRF headers if enabled
@@ -2065,7 +2049,7 @@ class BaseRequest {
2065
2049
  const globalToken = config.getCsrfToken();
2066
2050
  if (globalToken) {
2067
2051
  const csrfHeaderName = config.getCsrfHeaderName();
2068
- const hasLocalToken = this.hasHeader("X-CSRF-Token") || this.hasHeader(csrfHeaderName);
2052
+ const hasLocalToken = this._hasHeader("X-CSRF-Token") || this._hasHeader(csrfHeaderName);
2069
2053
  if (!hasLocalToken) {
2070
2054
  this.withHeader(csrfHeaderName, globalToken);
2071
2055
  }
@@ -2075,7 +2059,7 @@ class BaseRequest {
2075
2059
  const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
2076
2060
  if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
2077
2061
  const xsrfHeaderName = config.getXsrfHeaderName();
2078
- const hasLocalToken = this.hasHeader("X-XSRF-TOKEN") || this.hasHeader(xsrfHeaderName);
2062
+ const hasLocalToken = this._hasHeader("X-XSRF-TOKEN") || this._hasHeader(xsrfHeaderName);
2079
2063
  if (!hasLocalToken) {
2080
2064
  this.withHeader(xsrfHeaderName, xsrfToken);
2081
2065
  }
@@ -2087,8 +2071,8 @@ class BaseRequest {
2087
2071
  * @param url The base URL
2088
2072
  * @returns The URL with query parameters appended
2089
2073
  */
2090
- formatUrlWithQueryParams(url) {
2091
- const queryString = this.queryParams.toString();
2074
+ _fullUrl(url) {
2075
+ const queryString = this._query.toString();
2092
2076
  if (!queryString) {
2093
2077
  return url;
2094
2078
  }
@@ -2096,7 +2080,7 @@ class BaseRequest {
2096
2080
  // Try to use the URL constructor (works for absolute URLs)
2097
2081
  const urlObj = new URL(url);
2098
2082
  // Merge our query params with any that might be in the URL already
2099
- this.queryParams.forEach((value, key) => {
2083
+ this._query.forEach((value, key) => {
2100
2084
  urlObj.searchParams.append(key, value);
2101
2085
  });
2102
2086
  return urlObj.toString();
@@ -2115,21 +2099,21 @@ class BaseRequest {
2115
2099
  * @returns A wrapped response object
2116
2100
  * @throws RequestError if the request fails after all retries
2117
2101
  */
2118
- async executeWithRetries(url, fetchOptions) {
2119
- const retriesConfig = this.requestOptions.retries;
2102
+ async _retry(url, fetchOptions) {
2103
+ const retriesConfig = this._opts.retries;
2120
2104
  const maxRetries = typeof retriesConfig === "number" ? retriesConfig : retriesConfig?.attempts || 0;
2121
2105
  const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
2122
2106
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
2123
2107
  try {
2124
- return await this.executeRequest(url, fetchOptions);
2108
+ return await this._run(url, fetchOptions);
2125
2109
  }
2126
2110
  catch (error) {
2127
- const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, error instanceof Error ? error : new Error(String(error)));
2111
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, toError(error));
2128
2112
  if (attempt >= maxRetries)
2129
2113
  throw requestError;
2130
2114
  // Call onRetry callback if provided
2131
- if (this.requestOptions.onRetry) {
2132
- await this.requestOptions.onRetry({ attempt: attempt + 1, error: requestError });
2115
+ if (this._opts.onRetry) {
2116
+ await this._opts.onRetry({ attempt: attempt + 1, error: requestError });
2133
2117
  }
2134
2118
  // Apply delay if configured
2135
2119
  if (typeof retriesConfig === "object" && retriesConfig.delay !== undefined) {
@@ -2153,9 +2137,9 @@ class BaseRequest {
2153
2137
  * @param configParam - The request configuration
2154
2138
  * @returns Modified config or a Response to short-circuit
2155
2139
  */
2156
- async runRequestInterceptors(configParam) {
2140
+ async _runReqI(configParam) {
2157
2141
  const globalConfig = Config.getInstance();
2158
- const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this.requestInterceptors];
2142
+ const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this._reqI];
2159
2143
  let currentConfig = configParam;
2160
2144
  for (let i = 0; i < allInterceptors.length; i++) {
2161
2145
  try {
@@ -2167,7 +2151,7 @@ class BaseRequest {
2167
2151
  currentConfig = result;
2168
2152
  }
2169
2153
  catch (error) {
2170
- throw new RequestError(`ReqI: ${error instanceof Error ? error.message : String(error)}`, currentConfig.url, currentConfig.method);
2154
+ throw new RequestError(`ReqI: ${errorMessage(error)}`, currentConfig.url, currentConfig.method);
2171
2155
  }
2172
2156
  }
2173
2157
  return currentConfig;
@@ -2177,18 +2161,18 @@ class BaseRequest {
2177
2161
  * @param response - The response wrapper
2178
2162
  * @returns Modified response wrapper
2179
2163
  */
2180
- async runResponseInterceptors(response) {
2164
+ async _runResI(response) {
2181
2165
  const globalConfig = Config.getInstance();
2182
2166
  const globalInterceptors = globalConfig.getResponseInterceptors();
2183
2167
  // Per-request in order, then global in reverse
2184
- const allInterceptors = [...this.responseInterceptors, ...[...globalInterceptors].reverse()];
2168
+ const allInterceptors = [...this._resI, ...[...globalInterceptors].reverse()];
2185
2169
  let currentResponse = response;
2186
2170
  for (let i = 0; i < allInterceptors.length; i++) {
2187
2171
  try {
2188
2172
  currentResponse = await allInterceptors[i](currentResponse);
2189
2173
  }
2190
2174
  catch (error) {
2191
- throw new RequestError(`ResI: ${error instanceof Error ? error.message : String(error)}`, currentResponse.url || "", currentResponse.method || "");
2175
+ throw new RequestError(`ResI: ${errorMessage(error)}`, currentResponse.url || "", currentResponse.method || "");
2192
2176
  }
2193
2177
  }
2194
2178
  return currentResponse;
@@ -2198,11 +2182,11 @@ class BaseRequest {
2198
2182
  * @param error - The error that occurred
2199
2183
  * @returns Modified error or a ResponseWrapper to recover
2200
2184
  */
2201
- async runErrorInterceptors(error) {
2185
+ async _runErrI(error) {
2202
2186
  const globalConfig = Config.getInstance();
2203
2187
  const globalInterceptors = globalConfig.getErrorInterceptors();
2204
2188
  // Per-request in order, then global in reverse
2205
- const allInterceptors = [...this.errorInterceptors, ...[...globalInterceptors].reverse()];
2189
+ const allInterceptors = [...this._errI, ...[...globalInterceptors].reverse()];
2206
2190
  let currentError = error;
2207
2191
  for (let i = 0; i < allInterceptors.length; i++) {
2208
2192
  try {
@@ -2218,23 +2202,20 @@ class BaseRequest {
2218
2202
  if (interceptorError instanceof RequestError) {
2219
2203
  currentError = interceptorError;
2220
2204
  }
2221
- else {
2222
- const em = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
2205
+ else if (currentError instanceof RequestError) {
2223
2206
  // Always wrap in RequestError when we have context
2224
- if (currentError instanceof RequestError) {
2225
- currentError = new RequestError(`ErrI${i + 1}: ${em}`, currentError.url, currentError.method, {
2226
- status: currentError.status,
2227
- response: currentError.response,
2228
- });
2229
- }
2230
- else {
2231
- /* c8 ignore start */
2232
- // Last resort: if we have no context at all, use the original error's context
2233
- // This shouldn't happen in practice, but handle it gracefully
2234
- const errorObj = interceptorError instanceof Error ? interceptorError : new Error(String(interceptorError));
2235
- currentError = RequestError.networkError(error.url, error.method, errorObj);
2236
- /* c8 ignore end */
2237
- }
2207
+ currentError = new RequestError(`ErrI${i + 1}: ${errorMessage(interceptorError)}`, currentError.url, currentError.method, {
2208
+ status: currentError.status,
2209
+ response: currentError.response,
2210
+ body: currentError.body,
2211
+ });
2212
+ }
2213
+ else {
2214
+ /* c8 ignore start */
2215
+ // Last resort: if we have no context at all, use the original error's context
2216
+ // This shouldn't happen in practice, but handle it gracefully
2217
+ currentError = RequestError.networkError(error.url, error.method, toError(interceptorError));
2218
+ /* c8 ignore end */
2238
2219
  }
2239
2220
  }
2240
2221
  }
@@ -2244,7 +2225,7 @@ class BaseRequest {
2244
2225
  * Helper to create an abort signal with timeout support
2245
2226
  * Handles various AbortSignal API levels gracefully
2246
2227
  */
2247
- createAbortSignal(timeoutMs, externalController) {
2228
+ _signal(timeoutMs, externalController) {
2248
2229
  let timeoutId;
2249
2230
  let timeoutController;
2250
2231
  let isTimeout = false;
@@ -2280,7 +2261,7 @@ class BaseRequest {
2280
2261
  const finalSignal = externalController && hasAbortSignalAny
2281
2262
  ? AbortSignal.any([externalController.signal, timeoutSignal])
2282
2263
  : externalController
2283
- ? this.combineSignalsManually(externalController.signal, timeoutSignal)
2264
+ ? this._combine(externalController.signal, timeoutSignal)
2284
2265
  : timeoutSignal;
2285
2266
  return {
2286
2267
  signal: finalSignal,
@@ -2295,7 +2276,7 @@ class BaseRequest {
2295
2276
  * Manually combine two abort signals for older environments
2296
2277
  * Returns the first signal and listens to the second
2297
2278
  */
2298
- combineSignalsManually(signal1, signal2) {
2279
+ _combine(signal1, signal2) {
2299
2280
  // If either is already aborted, use that one
2300
2281
  if (signal1.aborted)
2301
2282
  return signal1;
@@ -2311,9 +2292,9 @@ class BaseRequest {
2311
2292
  /**
2312
2293
  * Convert fetchOptions to RequestConfig with proper typing
2313
2294
  */
2314
- createRequestConfig(url, fetchOptions) {
2295
+ _config(url, fetchOptions) {
2315
2296
  const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
2316
- const headers = this.getHeadersRecord();
2297
+ const headers = this._headers();
2317
2298
  const extendedOptions = fetchOptions;
2318
2299
  return {
2319
2300
  url,
@@ -2335,7 +2316,7 @@ class BaseRequest {
2335
2316
  /**
2336
2317
  * Apply interceptor results back to fetchOptions
2337
2318
  */
2338
- applyRequestConfig(config, fetchOptions) {
2319
+ _applyConfig(config, fetchOptions) {
2339
2320
  fetchOptions.headers = config.headers;
2340
2321
  if (config.body !== undefined) {
2341
2322
  fetchOptions.body = config.body;
@@ -2347,53 +2328,54 @@ class BaseRequest {
2347
2328
  fetchOptions.cache = config.cache;
2348
2329
  }
2349
2330
  }
2350
- async executeRequest(url, fetchOptions) {
2331
+ async _run(url, fetchOptions) {
2351
2332
  const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
2352
2333
  // Setup abort signal with timeout
2353
- const abortSignal = this.createAbortSignal(this.requestOptions.timeout, this.abortController);
2334
+ const abortSignal = this._signal(this._opts.timeout, this._ctrl);
2354
2335
  try {
2355
2336
  // Run request interceptors before making the request
2356
- const requestConfig = this.createRequestConfig(url, fetchOptions);
2357
- const interceptorResult = await this.runRequestInterceptors(requestConfig);
2337
+ const requestConfig = this._config(url, fetchOptions);
2338
+ const interceptorResult = await this._runReqI(requestConfig);
2358
2339
  // If interceptor returned a Response, short-circuit and wrap it
2359
2340
  if (interceptorResult instanceof Response) {
2360
- const graphQLOptions = this.getGraphQLOptions();
2361
- const wrappedResponse = new ResponseWrapper(interceptorResult, this.url, this.method, graphQLOptions);
2362
- return await this.runResponseInterceptors(wrappedResponse);
2341
+ const graphQLOptions = this._gql();
2342
+ const wrappedResponse = new ResponseWrapper(interceptorResult, this._url, this._method, graphQLOptions);
2343
+ return await this._runResI(wrappedResponse);
2363
2344
  }
2364
2345
  // Update fetchOptions with interceptor modifications
2365
2346
  url = interceptorResult.url;
2366
- this.validateUrl(url);
2367
- this.applyRequestConfig(interceptorResult, fetchOptions);
2347
+ this._validateUrl(url);
2348
+ this._applyConfig(interceptorResult, fetchOptions);
2368
2349
  // Set the combined abort signal
2369
2350
  if (abortSignal.signal) {
2370
2351
  fetchOptions.signal = abortSignal.signal;
2371
2352
  }
2372
- // Execute fetch
2353
+ // Execute fetch (custom implementation if provided, global fetch otherwise)
2354
+ const fetchFn = this._fetch ?? globalThis.fetch;
2373
2355
  let response;
2374
2356
  try {
2375
- response = await fetch(url, fetchOptions);
2357
+ response = await fetchFn(url, fetchOptions);
2376
2358
  }
2377
2359
  catch (error) {
2378
- const errorObj = error instanceof Error ? error : new Error(String(error));
2360
+ const errorObj = toError(error);
2379
2361
  const errorName = errorObj.name;
2380
- const errorMessage = errorObj.message.toLowerCase();
2362
+ const lowerMessage = errorObj.message.toLowerCase();
2381
2363
  // Check if this is a timeout error from our internal timeout
2382
2364
  const isOurTimeout = abortSignal.wasTimeout();
2383
2365
  // Check if it's an abort error (DOMException in browsers, or AbortSignal abort)
2384
2366
  if (error instanceof DOMException && error.name === "AbortError") {
2385
2367
  // If it was our timeout that caused the abort, throw timeout error
2386
- if (isOurTimeout && this.requestOptions.timeout) {
2387
- throw RequestError.timeout(url, method, this.requestOptions.timeout);
2368
+ if (isOurTimeout && this._opts.timeout) {
2369
+ throw RequestError.timeout(url, method, this._opts.timeout);
2388
2370
  }
2389
2371
  // Otherwise it's a manual abort
2390
2372
  throw RequestError.abortError(url, method);
2391
2373
  }
2392
2374
  // Check for Node.js/undici TimeoutError or other timeout indicators
2393
2375
  // This catches timeout errors that aren't thrown as AbortError
2394
- const isTimeoutError = isOurTimeout || errorName === "TimeoutError" || errorMessage.includes("timeout") || errorMessage.includes("aborted due to timeout");
2395
- if (isTimeoutError && this.requestOptions.timeout) {
2396
- throw RequestError.timeout(url, method, this.requestOptions.timeout);
2376
+ const isTimeoutError = isOurTimeout || errorName === "TimeoutError" || lowerMessage.includes("timeout");
2377
+ if (isTimeoutError && this._opts.timeout) {
2378
+ throw RequestError.timeout(url, method, this._opts.timeout);
2397
2379
  }
2398
2380
  // For other network errors, let RequestError.networkError handle them
2399
2381
  // It will check for timeout patterns as a safety net (useful for external AbortControllers)
@@ -2405,24 +2387,19 @@ class BaseRequest {
2405
2387
  throw RequestError.networkError(url, method, new Error("Failed with status 0 (network error or CORS blocked)"));
2406
2388
  }
2407
2389
  if (!response.ok) {
2408
- throw RequestError.fromResponse(response, url, method);
2390
+ // Capture the response body so it's available on the error object
2391
+ // (reads from a clone, so error.response remains readable)
2392
+ throw RequestError.fromResponse(response, url, method, await RequestError.captureBody(response));
2409
2393
  }
2410
- const graphQLOptions = this.getGraphQLOptions();
2394
+ const graphQLOptions = this._gql();
2411
2395
  const wrappedResponse = new ResponseWrapper(response, url, method, graphQLOptions);
2412
- return await this.runResponseInterceptors(wrappedResponse);
2396
+ return await this._runResI(wrappedResponse);
2413
2397
  }
2414
2398
  catch (error) {
2415
2399
  // Convert to RequestError if needed
2416
- let requestError;
2417
- if (error instanceof RequestError) {
2418
- requestError = error;
2419
- }
2420
- else {
2421
- const errorObj = error instanceof Error ? error : new Error(String(error));
2422
- requestError = RequestError.networkError(url, method, errorObj);
2423
- }
2400
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, toError(error));
2424
2401
  // Run error interceptors
2425
- const interceptorResult = await this.runErrorInterceptors(requestError);
2402
+ const interceptorResult = await this._runErrI(requestError);
2426
2403
  // If error interceptor returned a ResponseWrapper, recover from error
2427
2404
  if (interceptorResult instanceof ResponseWrapper) {
2428
2405
  return interceptorResult;
@@ -2439,12 +2416,9 @@ class BaseRequest {
2439
2416
  * Base class for requests that can have a body (POST, PUT, PATCH)
2440
2417
  */
2441
2418
  class BodyRequest extends BaseRequest {
2442
- body;
2443
- bodyType;
2444
- graphQLOptions = undefined;
2445
- constructor(url) {
2446
- super(url);
2447
- }
2419
+ _body;
2420
+ _bodyType;
2421
+ _gqlOpts = undefined;
2448
2422
  /**
2449
2423
  * Sets the request body. Automatically detects the body type and sets appropriate Content-Type header.
2450
2424
  * Supports JSON objects/arrays, strings, FormData, Blob, ArrayBuffer, URLSearchParams, and ReadableStream.
@@ -2481,11 +2455,11 @@ class BodyRequest extends BaseRequest {
2481
2455
  * ```
2482
2456
  */
2483
2457
  withBody(body) {
2484
- this.body = body;
2458
+ this._body = body;
2485
2459
  // Set body type and validate
2486
2460
  if (typeof body === "string") {
2487
- this.bodyType = BodyType.STRING;
2488
- this.setContentTypeIfNeeded("text/plain");
2461
+ this._bodyType = BodyType.STRING;
2462
+ this._setCT("text/plain");
2489
2463
  }
2490
2464
  else if (body !== null &&
2491
2465
  typeof body === "object" &&
@@ -2496,18 +2470,18 @@ class BodyRequest extends BaseRequest {
2496
2470
  ArrayBuffer.isView(body) || // Handles TypedArray and DataView
2497
2471
  body instanceof URLSearchParams ||
2498
2472
  body instanceof ReadableStream)) {
2499
- this.bodyType = BodyType.JSON;
2500
- this.setContentTypeIfNeeded("application/json");
2473
+ this._bodyType = BodyType.JSON;
2474
+ this._setCT("application/json");
2501
2475
  // Validate JSON is stringifiable early
2502
2476
  try {
2503
2477
  JSON.stringify(body);
2504
2478
  }
2505
2479
  catch (error) {
2506
- throw new RequestError(`Bad JSON: ${error instanceof Error ? error.message : String(error)}`, this.url, this.method);
2480
+ throw new RequestError(`Bad JSON: ${errorMessage(error)}`, this._url, this._method);
2507
2481
  }
2508
2482
  }
2509
2483
  else {
2510
- this.bodyType = BodyType.BINARY;
2484
+ this._bodyType = BodyType.BINARY;
2511
2485
  }
2512
2486
  return this;
2513
2487
  }
@@ -2550,25 +2524,25 @@ class BodyRequest extends BaseRequest {
2550
2524
  */
2551
2525
  withGraphQL(query, variables, options) {
2552
2526
  if (typeof query !== "string" || query.length === 0) {
2553
- throw new RequestError("Bad query", this.url, this.method);
2527
+ throw new RequestError("Bad query", this._url, this._method);
2554
2528
  }
2555
2529
  const graphQLBody = {
2556
2530
  query: query,
2557
2531
  };
2558
2532
  if (variables !== undefined) {
2559
2533
  if (typeof variables !== "object" || variables === null || Array.isArray(variables)) {
2560
- throw new RequestError("Bad vars", this.url, this.method);
2534
+ throw new RequestError("Bad vars", this._url, this._method);
2561
2535
  }
2562
2536
  graphQLBody.variables = variables;
2563
2537
  }
2564
2538
  // Store GraphQL options if provided
2565
2539
  if (options !== undefined) {
2566
2540
  if (typeof options !== "object" || options === null || Array.isArray(options)) {
2567
- throw new RequestError("Bad opts", this.url, this.method);
2541
+ throw new RequestError("Bad opts", this._url, this._method);
2568
2542
  }
2569
2543
  // Store only the known GraphQL options properties
2570
2544
  const opts = options;
2571
- this.graphQLOptions = {
2545
+ this._gqlOpts = {
2572
2546
  throwOnError: typeof opts.throwOnError === "boolean" ? opts.throwOnError : undefined,
2573
2547
  };
2574
2548
  }
@@ -2577,26 +2551,26 @@ class BodyRequest extends BaseRequest {
2577
2551
  JSON.stringify(graphQLBody);
2578
2552
  }
2579
2553
  catch (error) {
2580
- throw new RequestError(`Bad JSON: ${error instanceof Error ? error.message : String(error)}`, this.url, this.method);
2554
+ throw new RequestError(`Bad JSON: ${errorMessage(error)}`, this._url, this._method);
2581
2555
  }
2582
- this.body = graphQLBody;
2583
- this.bodyType = BodyType.JSON;
2584
- this.setContentTypeIfNeeded("application/json");
2556
+ this._body = graphQLBody;
2557
+ this._bodyType = BodyType.JSON;
2558
+ this._setCT("application/json");
2585
2559
  return this;
2586
2560
  }
2587
2561
  /**
2588
2562
  * Check if Content-Type header is already set (case-insensitive)
2589
2563
  */
2590
- hasContentType() {
2591
- const headers = this.requestOptions.headers;
2564
+ _hasCT() {
2565
+ const headers = this._opts.headers;
2592
2566
  if (typeof headers === "object" && headers !== null) {
2593
2567
  const headersObj = headers;
2594
2568
  return Object.keys(headersObj).some(header => header.toLowerCase() === "content-type");
2595
2569
  }
2596
2570
  return false;
2597
2571
  }
2598
- setContentTypeIfNeeded(contentType) {
2599
- if (!this.hasContentType()) {
2572
+ _setCT(contentType) {
2573
+ if (!this._hasCT()) {
2600
2574
  this.withContentType(contentType);
2601
2575
  }
2602
2576
  }
@@ -2604,24 +2578,24 @@ class BodyRequest extends BaseRequest {
2604
2578
  * Get the GraphQL options if set
2605
2579
  * @returns The GraphQL options or undefined
2606
2580
  */
2607
- getGraphQLOptions() {
2608
- return this.graphQLOptions;
2581
+ _gql() {
2582
+ return this._gqlOpts;
2609
2583
  }
2610
2584
  /**
2611
2585
  * Execute the request and return the ResponseWrapper
2612
2586
  * Overrides the base implementation to add body handling
2613
2587
  */
2614
2588
  async getResponse() {
2615
- if (this.body !== undefined) {
2589
+ if (this._body !== undefined) {
2616
2590
  // Remove previous body if it exists
2617
- if (this.requestOptions.body)
2618
- delete this.requestOptions.body;
2591
+ if (this._opts.body)
2592
+ delete this._opts.body;
2619
2593
  // Process the body based on its type
2620
- if (this.bodyType === BodyType.JSON) {
2621
- this.requestOptions.body = JSON.stringify(this.body);
2594
+ if (this._bodyType === BodyType.JSON) {
2595
+ this._opts.body = JSON.stringify(this._body);
2622
2596
  }
2623
2597
  else {
2624
- this.requestOptions.body = this.body;
2598
+ this._opts.body = this._body;
2625
2599
  }
2626
2600
  }
2627
2601
  return super.getResponse();
@@ -2638,10 +2612,7 @@ class BodyRequest extends BaseRequest {
2638
2612
  * const data = await request.getData();
2639
2613
  */
2640
2614
  class GetRequest extends BaseRequest {
2641
- method = exports.HttpMethod.GET;
2642
- constructor(url) {
2643
- super(url);
2644
- }
2615
+ _method = HttpMethod.GET;
2645
2616
  }
2646
2617
  /**
2647
2618
  * HTTP HEAD request implementation
@@ -2652,10 +2623,7 @@ class GetRequest extends BaseRequest {
2652
2623
  * const response = await request.getResponse();
2653
2624
  */
2654
2625
  class HeadRequest extends BaseRequest {
2655
- method = exports.HttpMethod.HEAD;
2656
- constructor(url) {
2657
- super(url);
2658
- }
2626
+ _method = HttpMethod.HEAD;
2659
2627
  }
2660
2628
  /**
2661
2629
  * HTTP OPTIONS request implementation
@@ -2666,10 +2634,7 @@ class HeadRequest extends BaseRequest {
2666
2634
  * const response = await request.getResponse();
2667
2635
  */
2668
2636
  class OptionsRequest extends BaseRequest {
2669
- method = exports.HttpMethod.OPTIONS;
2670
- constructor(url) {
2671
- super(url);
2672
- }
2637
+ _method = HttpMethod.OPTIONS;
2673
2638
  }
2674
2639
  /**
2675
2640
  * HTTP DELETE request implementation
@@ -2680,10 +2645,7 @@ class OptionsRequest extends BaseRequest {
2680
2645
  * await request.getData();
2681
2646
  */
2682
2647
  class DeleteRequest extends BaseRequest {
2683
- method = exports.HttpMethod.DELETE;
2684
- constructor(url) {
2685
- super(url);
2686
- }
2648
+ _method = HttpMethod.DELETE;
2687
2649
  }
2688
2650
  /**
2689
2651
  * HTTP POST request implementation
@@ -2695,10 +2657,7 @@ class DeleteRequest extends BaseRequest {
2695
2657
  * const data = await request.getData();
2696
2658
  */
2697
2659
  class PostRequest extends BodyRequest {
2698
- method = exports.HttpMethod.POST;
2699
- constructor(url) {
2700
- super(url);
2701
- }
2660
+ _method = HttpMethod.POST;
2702
2661
  }
2703
2662
  /**
2704
2663
  * HTTP PUT request implementation
@@ -2710,10 +2669,7 @@ class PostRequest extends BodyRequest {
2710
2669
  * const data = await request.getData();
2711
2670
  */
2712
2671
  class PutRequest extends BodyRequest {
2713
- method = exports.HttpMethod.PUT;
2714
- constructor(url) {
2715
- super(url);
2716
- }
2672
+ _method = HttpMethod.PUT;
2717
2673
  }
2718
2674
  /**
2719
2675
  * HTTP PATCH request implementation
@@ -2725,10 +2681,7 @@ class PutRequest extends BodyRequest {
2725
2681
  * const data = await request.getData();
2726
2682
  */
2727
2683
  class PatchRequest extends BodyRequest {
2728
- method = exports.HttpMethod.PATCH;
2729
- constructor(url) {
2730
- super(url);
2731
- }
2684
+ _method = HttpMethod.PATCH;
2732
2685
  }
2733
2686
 
2734
2687
  /**
@@ -2840,73 +2793,60 @@ function options(url) {
2840
2793
  * Internal API builder implementation.
2841
2794
  */
2842
2795
  class ApiBuilderImpl {
2843
- baseURL;
2844
- modifiers = [];
2845
- proxy;
2796
+ _baseURL;
2797
+ _mods = [];
2798
+ _proxy;
2846
2799
  withBaseURL(baseURL) {
2847
- this.baseURL = baseURL;
2848
- return this.getProxy();
2800
+ this._baseURL = baseURL;
2801
+ return this._getProxy();
2849
2802
  }
2850
- resolveURL(url) {
2803
+ _resolve(url) {
2851
2804
  if (!url)
2852
- return this.baseURL || "";
2805
+ return this._baseURL || "";
2853
2806
  if (/^https?:\/\//.test(url))
2854
2807
  return url;
2855
- if (!this.baseURL)
2808
+ if (!this._baseURL)
2856
2809
  return url;
2857
- return this.baseURL.replace(/\/$/, "") + (url[0] === "/" ? url : "/" + url);
2810
+ return this._baseURL.replace(/\/$/, "") + (url[0] === "/" ? url : "/" + url);
2858
2811
  }
2859
- applyModifiers(request) {
2860
- if (this.modifiers)
2861
- for (const modifier of this.modifiers)
2862
- modifier(request);
2812
+ _new(Ctor, url) {
2813
+ const request = new Ctor(this._resolve(url));
2814
+ for (const modifier of this._mods)
2815
+ modifier(request);
2816
+ return request;
2863
2817
  }
2864
2818
  get(url) {
2865
- const request = new GetRequest(this.resolveURL(url));
2866
- this.applyModifiers(request);
2867
- return request;
2819
+ return this._new(GetRequest, url);
2868
2820
  }
2869
2821
  post(url) {
2870
- const request = new PostRequest(this.resolveURL(url));
2871
- this.applyModifiers(request);
2872
- return request;
2822
+ return this._new(PostRequest, url);
2873
2823
  }
2874
2824
  put(url) {
2875
- const request = new PutRequest(this.resolveURL(url));
2876
- this.applyModifiers(request);
2877
- return request;
2825
+ return this._new(PutRequest, url);
2878
2826
  }
2879
2827
  del(url) {
2880
- const request = new DeleteRequest(this.resolveURL(url));
2881
- this.applyModifiers(request);
2882
- return request;
2828
+ return this._new(DeleteRequest, url);
2883
2829
  }
2884
2830
  patch(url) {
2885
- const request = new PatchRequest(this.resolveURL(url));
2886
- this.applyModifiers(request);
2887
- return request;
2831
+ return this._new(PatchRequest, url);
2888
2832
  }
2889
2833
  head(url) {
2890
- const request = new HeadRequest(this.resolveURL(url));
2891
- this.applyModifiers(request);
2892
- return request;
2834
+ return this._new(HeadRequest, url);
2893
2835
  }
2894
2836
  options(url) {
2895
- const request = new OptionsRequest(this.resolveURL(url));
2896
- this.applyModifiers(request);
2897
- return request;
2837
+ return this._new(OptionsRequest, url);
2898
2838
  }
2899
- addModifier(modifier) {
2900
- this.modifiers.push(modifier);
2901
- return this.getProxy();
2839
+ _add(modifier) {
2840
+ this._mods.push(modifier);
2841
+ return this._getProxy();
2902
2842
  }
2903
- getProxy() {
2904
- if (!this.proxy) {
2905
- this.proxy = this.createProxy();
2843
+ _getProxy() {
2844
+ if (!this._proxy) {
2845
+ this._proxy = this._mkProxy();
2906
2846
  }
2907
- return this.proxy;
2847
+ return this._proxy;
2908
2848
  }
2909
- createProxy() {
2849
+ _mkProxy() {
2910
2850
  const disallowedMethods = new Set(["withBody", "withGraphQL", "withAbortController"]);
2911
2851
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
2912
2852
  return new Proxy(this, {
@@ -2928,7 +2868,7 @@ class ApiBuilderImpl {
2928
2868
  // If it's a 'with...' method or other chainable method, create a modifier for it
2929
2869
  if (typeof prop === "string" && (prop.startsWith("with") || prop === "onRetry")) {
2930
2870
  return (...args) => {
2931
- return implTarget.addModifier((request) => {
2871
+ return implTarget._add((request) => {
2932
2872
  const method = request[prop];
2933
2873
  if (typeof method === "function") {
2934
2874
  method.apply(request, args);
@@ -2943,7 +2883,7 @@ class ApiBuilderImpl {
2943
2883
  });
2944
2884
  }
2945
2885
  static create() {
2946
- return new ApiBuilderImpl().getProxy();
2886
+ return new ApiBuilderImpl()._getProxy();
2947
2887
  }
2948
2888
  }
2949
2889
  /**
@@ -3024,16 +2964,24 @@ const create = {
3024
2964
  config: Config.getInstance(),
3025
2965
  };
3026
2966
 
2967
+ exports.CacheMode = CacheMode;
3027
2968
  exports.CookieUtils = CookieUtils;
2969
+ exports.CredentialsPolicy = CredentialsPolicy;
3028
2970
  exports.DeleteRequest = DeleteRequest;
3029
2971
  exports.GetRequest = GetRequest;
3030
2972
  exports.HeadRequest = HeadRequest;
2973
+ exports.HttpMethod = HttpMethod;
3031
2974
  exports.OptionsRequest = OptionsRequest;
3032
2975
  exports.PatchRequest = PatchRequest;
3033
2976
  exports.PostRequest = PostRequest;
3034
2977
  exports.PutRequest = PutRequest;
2978
+ exports.RedirectMode = RedirectMode;
2979
+ exports.ReferrerPolicy = ReferrerPolicy;
3035
2980
  exports.RequestError = RequestError;
2981
+ exports.RequestMode = RequestMode;
2982
+ exports.RequestPriority = RequestPriority;
3036
2983
  exports.ResponseWrapper = ResponseWrapper;
2984
+ exports.SameSitePolicy = SameSitePolicy;
3037
2985
  exports.createApi = api;
3038
2986
  exports.createDelete = del;
3039
2987
  exports.createGet = get;