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