create-request 0.0.1-beta.23 → 0.0.1-beta.24

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.
@@ -48,18 +48,6 @@ var RedirectMode;
48
48
  RedirectMode["FOLLOW"] = "follow";
49
49
  RedirectMode["MANUAL"] = "manual";
50
50
  })(RedirectMode || (RedirectMode = {}));
51
- /**
52
- * Enum for cache modes
53
- */
54
- var CacheMode;
55
- (function (CacheMode) {
56
- CacheMode["DEFAULT"] = "default";
57
- CacheMode["RELOAD"] = "reload";
58
- CacheMode["NO_STORE"] = "no-store";
59
- CacheMode["NO_CACHE"] = "no-cache";
60
- CacheMode["FORCE_CACHE"] = "force-cache";
61
- CacheMode["ONLY_IF_CACHED"] = "only-if-cached";
62
- })(CacheMode || (CacheMode = {}));
63
51
  /**
64
52
  * Enum for cookie SameSite policies
65
53
  */
@@ -91,14 +79,108 @@ var ReferrerPolicy;
91
79
  ReferrerPolicy["ORIGIN_WHEN_CROSS_ORIGIN"] = "origin-when-cross-origin";
92
80
  ReferrerPolicy["NO_REFERRER_WHEN_DOWNGRADE"] = "no-referrer-when-downgrade";
93
81
  ReferrerPolicy["STRICT_ORIGIN_WHEN_CROSS_ORIGIN"] = "strict-origin-when-cross-origin";
94
- })(ReferrerPolicy || (ReferrerPolicy = {}));/**
82
+ })(ReferrerPolicy || (ReferrerPolicy = {}));
83
+
84
+ class RequestError extends Error {
85
+ constructor(message, url, method, options = {}) {
86
+ super(message);
87
+ this.name = "RequestError";
88
+ this.url = url;
89
+ this.method = method;
90
+ this.status = options.status;
91
+ this.response = options.response;
92
+ this.isTimeout = options.isTimeout;
93
+ this.isAborted = options.isAborted;
94
+ // For better stack traces in modern environments
95
+ if (Error.captureStackTrace) {
96
+ Error.captureStackTrace(this, RequestError);
97
+ }
98
+ // Maintains proper prototype chain for instanceof checks
99
+ Object.setPrototypeOf(this, RequestError.prototype);
100
+ }
101
+ /**
102
+ * Static methods for creating specific types of RequestError
103
+ */
104
+ static timeout(url, method, timeoutMs) {
105
+ return new RequestError(`Timeout: ${timeoutMs}ms`, url, method, {
106
+ isTimeout: true,
107
+ });
108
+ }
109
+ static fromResponse(response, url, method) {
110
+ return new RequestError(`HTTP ${response.status}`, url, method, {
111
+ status: response.status,
112
+ response,
113
+ });
114
+ }
115
+ static networkError(url, method, originalError) {
116
+ // Provide more descriptive error messages for common network errors
117
+ let message = originalError.message;
118
+ // Check for Node.js error codes (e.g., from undici/dns errors)
119
+ const errorCode = originalError.code;
120
+ const errorName = originalError.name;
121
+ const stack = originalError.stack || "";
122
+ const errorMessageLower = message.toLowerCase();
123
+ // Check for timeout errors (Node.js/undici TimeoutError)
124
+ const isTimeoutError = errorName === "TimeoutError" ||
125
+ errorMessageLower.includes("timeout") ||
126
+ errorMessageLower.includes("aborted due to timeout") ||
127
+ errorCode === "ETIMEDOUT" ||
128
+ stack.includes("TimeoutError") ||
129
+ stack.includes("timeout");
130
+ // If the error message is generic "fetch failed", provide more context
131
+ if (message === "fetch failed" || message === "Failed to fetch") {
132
+ // Check for DNS resolution errors
133
+ const isDnsError = errorCode === "ENOTFOUND" ||
134
+ errorCode === "EAI_AGAIN" ||
135
+ errorCode === "EAI_NODATA" ||
136
+ stack.includes("getaddrinfo") ||
137
+ stack.includes("ENOTFOUND") ||
138
+ stack.includes("EAI_AGAIN");
139
+ // Check for connection errors (but not timeout errors)
140
+ const isConnectionError = !isTimeoutError && (errorCode === "ECONNREFUSED" || errorCode === "ECONNRESET" || stack.includes("ECONNREFUSED") || stack.includes("connect"));
141
+ if (isTimeoutError) {
142
+ message = `Network error: Request timeout for ${url}`;
143
+ }
144
+ else if (isDnsError) {
145
+ message = `Network error: Unable to resolve hostname for ${url}`;
146
+ }
147
+ else if (isConnectionError) {
148
+ message = `Network error: Connection refused for ${url}`;
149
+ }
150
+ else {
151
+ message = `Network error: Failed to fetch ${url}`;
152
+ }
153
+ }
154
+ const error = new RequestError(message, url, method, {
155
+ ...(isTimeoutError ? { isTimeout: true } : {}),
156
+ });
157
+ // Create a proper RequestError stack trace, but append the original stack for debugging
158
+ // This way Node.js will show "RequestError: ..." instead of "TypeError: ..."
159
+ if (originalError.stack) {
160
+ // Get the current stack (which will start with RequestError)
161
+ const currentStack = error.stack || "";
162
+ // Append the original error's stack as "Caused by:" for debugging context
163
+ error.stack = `${currentStack}\n\nCaused by: ${originalError.stack}`;
164
+ }
165
+ return error;
166
+ }
167
+ static abortError(url, method) {
168
+ return new RequestError("Aborted", url, method, {
169
+ isAborted: true,
170
+ });
171
+ }
172
+ }
173
+
174
+ /**
95
175
  * Wrapper for HTTP responses with methods to transform the response data
96
176
  * Provides caching and conversion between different response formats.
97
177
  */
98
178
  class ResponseWrapper {
99
- constructor(response) {
179
+ constructor(response, url, method) {
100
180
  this.bodyUsed = false;
101
181
  this.response = response;
182
+ this.url = url;
183
+ this.method = method;
102
184
  }
103
185
  // Wrapper properties
104
186
  get status() {
@@ -135,19 +217,43 @@ class ResponseWrapper {
135
217
  // If we already have text, try to parse it as JSON
136
218
  if (this.textCache !== undefined) {
137
219
  try {
138
- // Fix unsafe assignment by explicitly casting to unknown first
139
- this.jsonCache = JSON.parse(this.textCache);
140
- return this.jsonCache;
220
+ const parsed = JSON.parse(this.textCache);
221
+ this.jsonCache = parsed;
222
+ return parsed;
141
223
  }
142
224
  catch (e) {
143
- throw new Error("Cannot parse cached text as JSON");
225
+ const errorMessage = e instanceof Error ? e.message : String(e);
226
+ if (this.url && this.method) {
227
+ throw new RequestError(`Invalid JSON: ${errorMessage}`, this.url, this.method, {
228
+ response: this.response,
229
+ });
230
+ }
231
+ throw new Error(`Invalid JSON: ${errorMessage}`);
144
232
  }
145
233
  }
146
- throw new Error("Response body has already been consumed in a different format");
234
+ if (this.url && this.method) {
235
+ throw new RequestError("Body already consumed", this.url, this.method, {
236
+ response: this.response,
237
+ });
238
+ }
239
+ throw new Error("Body already consumed");
147
240
  }
148
241
  this.bodyUsed = true;
149
- this.jsonCache = (await this.response.json());
150
- return this.jsonCache;
242
+ try {
243
+ const parsed = await this.response.json();
244
+ this.jsonCache = parsed;
245
+ return parsed;
246
+ }
247
+ catch (e) {
248
+ const errorMessage = e instanceof Error ? e.message : String(e);
249
+ if (this.url && this.method) {
250
+ throw new RequestError(`Invalid JSON: ${errorMessage}`, this.url, this.method, {
251
+ status: this.response.status,
252
+ response: this.response,
253
+ });
254
+ }
255
+ throw e;
256
+ }
151
257
  }
152
258
  /**
153
259
  * Get the response body as text
@@ -172,14 +278,42 @@ class ResponseWrapper {
172
278
  }
173
279
  // If we have blob, convert to text
174
280
  if (this.blobCache !== undefined) {
175
- this.textCache = await this.blobCache.text();
176
- return this.textCache;
281
+ try {
282
+ this.textCache = await this.blobCache.text();
283
+ return this.textCache;
284
+ }
285
+ catch (e) {
286
+ const errorMessage = e instanceof Error ? e.message : String(e);
287
+ if (this.url && this.method) {
288
+ throw new RequestError(`Cannot convert to text: ${errorMessage}`, this.url, this.method, {
289
+ response: this.response,
290
+ });
291
+ }
292
+ throw e;
293
+ }
177
294
  }
178
- throw new Error("Response body has already been consumed in a format that cannot be converted to text");
295
+ if (this.url && this.method) {
296
+ throw new RequestError("Cannot convert to text", this.url, this.method, {
297
+ response: this.response,
298
+ });
299
+ }
300
+ throw new Error("Cannot convert to text");
179
301
  }
180
302
  this.bodyUsed = true;
181
- this.textCache = await this.response.text();
182
- return this.textCache;
303
+ try {
304
+ this.textCache = await this.response.text();
305
+ return this.textCache;
306
+ }
307
+ catch (e) {
308
+ const errorMessage = e instanceof Error ? e.message : String(e);
309
+ if (this.url && this.method) {
310
+ throw new RequestError(`Read failed: ${errorMessage}`, this.url, this.method, {
311
+ status: this.response.status,
312
+ response: this.response,
313
+ });
314
+ }
315
+ throw e;
316
+ }
183
317
  }
184
318
  /**
185
319
  * Get the response body as a Blob
@@ -203,46 +337,28 @@ class ResponseWrapper {
203
337
  this.blobCache = new Blob([this.textCache], { type: "text/plain" });
204
338
  return this.blobCache;
205
339
  }
206
- throw new Error("Response body has already been consumed in a format that cannot be converted to Blob");
340
+ if (this.url && this.method) {
341
+ throw new RequestError("Cannot convert to Blob", this.url, this.method, {
342
+ response: this.response,
343
+ });
344
+ }
345
+ throw new Error("Cannot convert to Blob");
207
346
  }
208
347
  this.bodyUsed = true;
209
- this.blobCache = await this.response.blob();
210
- return this.blobCache;
211
- }
212
- /**
213
- * Get the response body as an ArrayBuffer
214
- * Caches the result for subsequent calls. Attempts to convert from text or Blob
215
- * if the body has already been consumed in those formats.
216
- *
217
- * @returns The response as an ArrayBuffer
218
- * @throws Error if the response has already been consumed in an incompatible format
219
- *
220
- * @example
221
- * const buffer = await response.getArrayBuffer();
222
- */
223
- async getArrayBuffer() {
224
- if (this.arrayBufferCache !== undefined) {
225
- return this.arrayBufferCache;
348
+ try {
349
+ this.blobCache = await this.response.blob();
350
+ return this.blobCache;
226
351
  }
227
- if (this.bodyUsed) {
228
- // If we have text, convert to ArrayBuffer
229
- if (this.textCache !== undefined) {
230
- const encoder = new TextEncoder();
231
- // Fix ArrayBufferLike by explicitly casting to ArrayBuffer
232
- const uint8Array = encoder.encode(this.textCache);
233
- this.arrayBufferCache = uint8Array.buffer;
234
- return this.arrayBufferCache;
235
- }
236
- // If we have blob, convert to ArrayBuffer
237
- if (this.blobCache !== undefined) {
238
- this.arrayBufferCache = await this.blobCache.arrayBuffer();
239
- return this.arrayBufferCache;
352
+ catch (e) {
353
+ const errorMessage = e instanceof Error ? e.message : String(e);
354
+ if (this.url && this.method) {
355
+ throw new RequestError(`Read failed: ${errorMessage}`, this.url, this.method, {
356
+ status: this.response.status,
357
+ response: this.response,
358
+ });
240
359
  }
241
- throw new Error("Response body has already been consumed in a format that cannot be converted to ArrayBuffer");
360
+ throw e;
242
361
  }
243
- this.bodyUsed = true;
244
- this.arrayBufferCache = await this.response.arrayBuffer();
245
- return this.arrayBufferCache;
246
362
  }
247
363
  /**
248
364
  * Get the raw response body as a ReadableStream
@@ -260,413 +376,66 @@ class ResponseWrapper {
260
376
  */
261
377
  getBody() {
262
378
  if (this.bodyUsed) {
263
- throw new Error("Response body has already been consumed");
379
+ if (this.url && this.method) {
380
+ throw new RequestError("Body already consumed", this.url, this.method, {
381
+ response: this.response,
382
+ });
383
+ }
384
+ throw new Error("Body already consumed");
264
385
  }
265
386
  this.bodyUsed = true;
266
387
  return this.response.body;
267
388
  }
268
- }/**
269
- * Create a memory storage provider using Map
270
- * This provider stores data in memory and is cleared when the application restarts.
271
- *
272
- * @returns A storage provider using an in-memory Map
273
- *
274
- * @example
275
- * const storage = createMemoryStorage();
276
- * storage.set("key", "value");
277
- */
278
- function createMemoryStorage() {
279
- const store = new Map();
280
- return {
281
- get(key) {
282
- return store.get(key) || null;
283
- },
284
- set(key, value) {
285
- store.set(key, value);
286
- },
287
- has(key) {
288
- return store.has(key);
289
- },
290
- delete(key) {
291
- store.delete(key);
292
- },
293
- clear() {
294
- store.clear();
295
- },
296
- };
297
- }
298
- /**
299
- * Create a localStorage-based storage provider
300
- * This provider persists data between browser sessions.
301
- *
302
- * @returns A storage provider using localStorage
303
- * @throws Console warnings if localStorage is not available or storage quota is exceeded
304
- *
305
- * @example
306
- * const storage = createLocalStorageStorage();
307
- * storage.set("key", "value");
308
- */
309
- function createLocalStorageStorage() {
310
- return {
311
- get(key) {
312
- if (typeof localStorage === "undefined")
313
- return null;
314
- return localStorage.getItem(key);
315
- },
316
- set(key, value) {
317
- if (typeof localStorage === "undefined")
318
- return;
319
- try {
320
- localStorage.setItem(key, value);
321
- }
322
- catch (e) {
323
- // Handle quota exceeded errors
324
- console.warn("Cache storage quota exceeded, clearing cache");
325
- void this.clear();
326
- try {
327
- localStorage.setItem(key, value);
328
- }
329
- catch (e) {
330
- console.error("Failed to set cache item even after clearing cache");
331
- }
332
- }
333
- },
334
- has(key) {
335
- if (typeof localStorage === "undefined")
336
- return false;
337
- return localStorage.getItem(key) !== null;
338
- },
339
- delete(key) {
340
- if (typeof localStorage === "undefined")
341
- return;
342
- localStorage.removeItem(key);
343
- },
344
- clear() {
345
- if (typeof localStorage === "undefined")
346
- return;
347
- localStorage.clear();
348
- },
349
- };
350
- }
351
- /**
352
- * Create a sessionStorage-based storage provider
353
- * This provider persists data only for the current browser session.
354
- *
355
- * @returns A storage provider using sessionStorage
356
- * @throws Console warnings if sessionStorage is not available or storage quota is exceeded
357
- *
358
- * @example
359
- * const storage = createSessionStorageStorage();
360
- * storage.set("key", "value");
361
- */
362
- function createSessionStorageStorage() {
363
- return {
364
- get(key) {
365
- if (typeof sessionStorage === "undefined")
366
- return null;
367
- return sessionStorage.getItem(key);
368
- },
369
- set(key, value) {
370
- if (typeof sessionStorage === "undefined")
371
- return;
372
- try {
373
- sessionStorage.setItem(key, value);
374
- }
375
- catch (e) {
376
- // Handle quota exceeded errors
377
- console.warn("Cache storage quota exceeded, clearing cache");
378
- void this.clear();
379
- try {
380
- sessionStorage.setItem(key, value);
381
- }
382
- catch (e) {
383
- console.error("Failed to set cache item even after clearing cache");
384
- }
385
- }
386
- },
387
- has(key) {
388
- if (typeof sessionStorage === "undefined")
389
- return false;
390
- return sessionStorage.getItem(key) !== null;
391
- },
392
- delete(key) {
393
- if (typeof sessionStorage === "undefined")
394
- return;
395
- sessionStorage.removeItem(key);
396
- },
397
- clear() {
398
- if (typeof sessionStorage === "undefined")
399
- return;
400
- sessionStorage.clear();
401
- },
402
- };
403
- }
404
- /**
405
- * Create a storage provider from cache options
406
- * Selects the appropriate storage implementation based on the provided options.
407
- * Falls back to memory storage if no suitable storage is found.
408
- *
409
- * @param options - Cache configuration options
410
- * @returns A storage provider based on the options
411
- *
412
- * @example
413
- * const storage = createStorageProvider({ storage: localStorage });
414
- */
415
- function createStorageProvider(options) {
416
- const { storage } = options || {};
417
- if (!storage) {
418
- // Default to memory storage
419
- return createMemoryStorage();
420
- }
421
- // Check if storage is already a StorageProvider
422
- if (typeof storage.get === "function" && typeof storage.set === "function") {
423
- return storage;
424
- }
425
- // Check if it's localStorage or sessionStorage
426
- if (storage === localStorage) {
427
- return createLocalStorageStorage();
428
- }
429
- if (storage === sessionStorage) {
430
- return createSessionStorageStorage();
431
- }
432
- // Check if it's a Map
433
- if (storage instanceof Map) {
434
- return {
435
- get(key) {
436
- return storage.get(key) || null;
437
- },
438
- set(key, value, _ttl) {
439
- storage.set(key, value);
440
- },
441
- has(key) {
442
- return storage.has(key);
443
- },
444
- delete(key) {
445
- storage.delete(key);
446
- },
447
- clear() {
448
- storage.clear();
449
- },
450
- };
451
- }
452
- // Default to memory storage
453
- console.warn("Unsupported storage type, falling back to memory storage");
454
- return createMemoryStorage();
455
- }/**
456
- * Parse size strings like "10MB" into bytes
457
- * Supports KB, MB, and GB units. If no unit is specified, assumes bytes.
458
- *
459
- * @param size - Size as number (bytes) or string with units (e.g., "5MB")
460
- * @returns Size in bytes
461
- */
462
- function parseSize(size) {
463
- var _a;
464
- if (typeof size === "number")
465
- return size;
466
- const match = size.match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/i);
467
- if (!match)
468
- return 0;
469
- const num = parseFloat(match[1]);
470
- const unit = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.toUpperCase();
471
- switch (unit) {
472
- case "KB":
473
- return num * 1024;
474
- case "MB":
475
- return num * 1024 * 1024;
476
- case "GB":
477
- return num * 1024 * 1024 * 1024;
478
- default:
479
- return num;
480
- }
481
- }/**
482
- * Cache manager that handles storage operations
483
- * Provides methods for storing, retrieving, and managing cached API responses.
484
- */
485
- class CacheManager {
486
- constructor(options = {}) {
487
- this.entryCount = 0;
488
- this.totalSize = 0;
489
- this.options = options;
490
- this.storage = createStorageProvider(options);
491
- }
492
389
  /**
493
- * Generate a cache key based on request details
494
- * Keys can vary based on URL, method, headers, and request body.
390
+ * Extract specific data using a selector function
391
+ * If no selector is provided, returns the full JSON response.
495
392
  *
496
- * @param url - The request URL
497
- * @param method - The HTTP method
498
- * @param headers - Request headers that might affect caching
499
- * @param body - Request body for non-GET requests
500
- * @returns A unique cache key string
393
+ * @param selector - Optional function to extract and transform data
394
+ * @returns A promise that resolves to the selected data
501
395
  *
502
396
  * @example
503
- * const key = cacheManager.generateKey("/api/users", "GET", { "Accept": "application/json" });
504
- */
505
- generateKey(url, method, headers, body) {
506
- // Use custom key generator if provided
507
- if (this.options.keyGenerator) {
508
- const customKey = this.options.keyGenerator(url, method, headers, body);
509
- return this.options.keyPrefix ? `${this.options.keyPrefix}:${customKey}` : customKey;
510
- }
511
- // Default key generation
512
- let key = `${method}:${url}`;
513
- // Add vary headers to key if specified
514
- if (this.options.varyByHeaders && headers) {
515
- const varyHeaders = {};
516
- this.options.varyByHeaders.forEach(header => {
517
- const normalizedHeader = header.toLowerCase();
518
- // Find the header in case-insensitive way
519
- Object.keys(headers).forEach(h => {
520
- if (h.toLowerCase() === normalizedHeader) {
521
- varyHeaders[normalizedHeader] = headers[h];
522
- }
523
- });
524
- });
525
- if (Object.keys(varyHeaders).length > 0) {
526
- key += `:${JSON.stringify(varyHeaders)}`;
527
- }
528
- }
529
- // Include body in cache key for non-GET requests if it's a simple value
530
- if (method !== "GET" && body !== undefined && body !== null) {
531
- try {
532
- if (typeof body === "string" || typeof body === "number" || typeof body === "boolean") {
533
- key += `:${String(body)}`;
534
- }
535
- else if (typeof body === "object") {
536
- key += `:${JSON.stringify(body)}`;
537
- }
538
- }
539
- catch (e) {
540
- console.warn("Failed to stringify request body for cache key");
541
- }
542
- }
543
- // Add prefix if specified
544
- return this.options.keyPrefix ? `${this.options.keyPrefix}:${key}` : key;
545
- }
546
- /**
547
- * Get a cached value by key
548
- * Checks if the entry exists and isn't expired before returning it.
397
+ * // Get full response
398
+ * const data = await response.getData();
549
399
  *
550
- * @param key - The cache key to retrieve
551
- * @returns The cached entry or null if not found or expired
552
- *
553
- * @example
554
- * const entry = await cacheManager.get("GET:/api/users");
400
+ * // Extract specific data
401
+ * const users = await response.getData(data => data.results.users);
555
402
  */
556
- async get(key) {
557
- const value = await Promise.resolve(this.storage.get(key));
558
- if (!value)
559
- return null;
403
+ async getData(selector) {
560
404
  try {
561
- const entry = JSON.parse(value);
562
- // Check if the entry has expired
563
- if (entry.expiry && entry.expiry < Date.now()) {
564
- await Promise.resolve(this.storage.delete(key));
565
- return null;
566
- }
567
- return entry;
568
- }
569
- catch (e) {
570
- // If we can't parse the cached value, remove it
571
- await Promise.resolve(this.storage.delete(key));
572
- return null;
405
+ const data = await this.getJson();
406
+ // If no selector is provided, return the raw JSON data
407
+ if (!selector)
408
+ return data;
409
+ // Apply the selector if provided
410
+ return selector(data);
573
411
  }
574
- }
575
- /**
576
- * Store a value in the cache
577
- * Enforces size and entry count limits if configured.
578
- *
579
- * @param key - The cache key
580
- * @param value - The value to cache
581
- * @param headers - Response headers to store with the cached value
582
- * @returns Promise that resolves when caching is complete
583
- *
584
- * @example
585
- * await cacheManager.set("GET:/api/users", userData, { "Content-Type": "application/json" });
586
- */
587
- async set(key, value, headers) {
588
- const now = Date.now();
589
- const ttl = this.options.ttl;
590
- const entry = {
591
- value,
592
- timestamp: now,
593
- expiry: ttl ? now + ttl : null,
594
- headers,
595
- };
596
- const serialized = JSON.stringify(entry);
597
- // Check size constraints if applicable
598
- if (this.options.maxSize) {
599
- const maxBytes = parseSize(this.options.maxSize);
600
- const entrySize = new TextEncoder().encode(serialized).length;
601
- // If a single entry exceeds the max size, reject it without clearing the cache
602
- if (entrySize > maxBytes) {
603
- console.warn(`Cache entry size (${entrySize} bytes) exceeds maximum cache size (${maxBytes} bytes)`);
604
- return;
412
+ catch (error) {
413
+ // If it's already a RequestError, re-throw it
414
+ if (error instanceof RequestError) {
415
+ throw error;
605
416
  }
606
- // Only clear if adding this entry would exceed the total limit
607
- if (this.totalSize + entrySize > maxBytes) {
608
- // Simple strategy: clear everything
609
- // A more sophisticated strategy would remove oldest entries first
610
- await Promise.resolve(this.storage.clear());
611
- this.totalSize = 0;
612
- this.entryCount = 0;
417
+ // Enhance selector errors with context
418
+ if (selector) {
419
+ const errorMessage = error instanceof Error ? error.message : String(error);
420
+ if (this.url && this.method) {
421
+ throw new RequestError(`Data selector failed: ${errorMessage}`, this.url, this.method, {
422
+ response: this.response,
423
+ });
424
+ }
425
+ throw new Error(`Data selector failed: ${errorMessage}`);
613
426
  }
614
- this.totalSize += entrySize;
615
- }
616
- // Check max entries constraint
617
- if (this.options.maxEntries) {
618
- this.entryCount++;
619
- if (this.entryCount > this.options.maxEntries) {
620
- // Simple strategy: clear everything
621
- // A more sophisticated strategy would use LRU cache
622
- await Promise.resolve(this.storage.clear());
623
- this.entryCount = 1;
427
+ // If we get here and it's not a RequestError, wrap it
428
+ // This should rarely happen as getJson() should throw RequestError
429
+ const errorObj = error instanceof Error ? error : new Error(String(error));
430
+ if (this.url && this.method) {
431
+ throw RequestError.networkError(this.url, this.method, errorObj);
624
432
  }
433
+ throw errorObj;
625
434
  }
626
- // Pass ttl to storage provider if available
627
- await Promise.resolve(this.storage.set(key, serialized, ttl));
628
435
  }
629
- /**
630
- * Check if a key exists in the cache
631
- *
632
- * @param key - The cache key to check
633
- * @returns Promise that resolves to true if the key exists, false otherwise
634
- *
635
- * @example
636
- * if (await cacheManager.has("GET:/api/users")) {
637
- * // Use cached value
638
- * }
639
- */
640
- async has(key) {
641
- return Promise.resolve(this.storage.has(key));
642
- }
643
- /**
644
- * Delete a specific key from the cache
645
- *
646
- * @param key - The cache key to delete
647
- * @returns Promise that resolves when deletion is complete
648
- *
649
- * @example
650
- * await cacheManager.delete("GET:/api/users");
651
- */
652
- async delete(key) {
653
- return Promise.resolve(this.storage.delete(key));
654
- }
655
- /**
656
- * Clear the entire cache
657
- * Resets internal counters for entry count and size.
658
- *
659
- * @returns Promise that resolves when the cache is cleared
660
- *
661
- * @example
662
- * await cacheManager.clear();
663
- */
664
- async clear() {
665
- this.entryCount = 0;
666
- this.totalSize = 0;
667
- return Promise.resolve(this.storage.clear());
668
- }
669
- }class CookieUtils {
436
+ }
437
+
438
+ class CookieUtils {
670
439
  /**
671
440
  * Formats cookies for a request
672
441
  * @param cookies Object containing cookie name-value pairs or cookie options
@@ -688,42 +457,9 @@ class CacheManager {
688
457
  });
689
458
  return cookiePairs.join("; ");
690
459
  }
691
- }class RequestError extends Error {
692
- constructor(message, url, method, options = {}) {
693
- super(message);
694
- this.name = "RequestError";
695
- this.url = url;
696
- this.method = method;
697
- this.status = options.status;
698
- this.response = options.response;
699
- this.timeoutError = options.timeoutError;
700
- // For better stack traces in modern environments
701
- if (Error.captureStackTrace) {
702
- Error.captureStackTrace(this, RequestError);
703
- }
704
- // Maintains proper prototype chain for instanceof checks
705
- Object.setPrototypeOf(this, RequestError.prototype);
706
- }
707
- /**
708
- * Static methods for creating specific types of RequestError
709
- */
710
- static timeout(url, method, timeoutMs) {
711
- return new RequestError(`Request timed out after ${timeoutMs}ms`, url, method, {
712
- timeoutError: true,
713
- });
714
- }
715
- static fromResponse(response, url, method) {
716
- return new RequestError(`Request failed with status ${response.status}`, url, method, {
717
- status: response.status,
718
- response,
719
- });
720
- }
721
- static networkError(url, method, originalError) {
722
- const error = new RequestError(`Network error: ${originalError.message}`, url, method);
723
- error.stack = originalError.stack;
724
- return error;
725
- }
726
- }/**
460
+ }
461
+
462
+ /**
727
463
  * Utility class for CSRF token management
728
464
  */
729
465
  class CsrfUtils {
@@ -788,7 +524,9 @@ class CsrfUtils {
788
524
  // For shorter tokens (8-10 chars), just return true if we reached here
789
525
  return true;
790
526
  }
791
- }/**
527
+ }
528
+
529
+ /**
792
530
  * Global configuration for create-request
793
531
  */
794
532
  class Config {
@@ -800,6 +538,11 @@ class Config {
800
538
  this.csrfToken = null;
801
539
  this.enableAutoXsrf = true;
802
540
  this.enableAntiCsrf = true; // X-Requested-With header
541
+ // Interceptor configuration
542
+ this.requestInterceptors = [];
543
+ this.responseInterceptors = [];
544
+ this.errorInterceptors = [];
545
+ this.nextInterceptorId = 1;
803
546
  }
804
547
  /**
805
548
  * Get the singleton instance of the Config class
@@ -927,17 +670,136 @@ class Config {
927
670
  * @param enable - Whether to enable this feature
928
671
  * @returns The config instance for chaining
929
672
  */
930
- setEnableAntiCsrf(enable) {
931
- this.enableAntiCsrf = enable;
932
- return this;
673
+ setEnableAntiCsrf(enable) {
674
+ this.enableAntiCsrf = enable;
675
+ return this;
676
+ }
677
+ /**
678
+ * Check if anti-CSRF protection is enabled
679
+ *
680
+ * @returns True if anti-CSRF protection is enabled
681
+ */
682
+ isAntiCsrfEnabled() {
683
+ return this.enableAntiCsrf;
684
+ }
685
+ /**
686
+ * Add a global request interceptor
687
+ * Request interceptors can modify the request configuration or return an early response
688
+ *
689
+ * @param interceptor - The request interceptor function
690
+ * @returns The interceptor ID for later removal
691
+ *
692
+ * @example
693
+ * const id = Config.getInstance().addRequestInterceptor((config) => {
694
+ * config.headers['X-Custom'] = 'value';
695
+ * return config;
696
+ * });
697
+ */
698
+ addRequestInterceptor(interceptor) {
699
+ const id = this.nextInterceptorId++;
700
+ this.requestInterceptors.push({ id, interceptor });
701
+ return id;
702
+ }
703
+ /**
704
+ * Add a global response interceptor
705
+ * Response interceptors can transform the response
706
+ *
707
+ * @param interceptor - The response interceptor function
708
+ * @returns The interceptor ID for later removal
709
+ *
710
+ * @example
711
+ * const id = Config.getInstance().addResponseInterceptor((response) => {
712
+ * console.log('Response received:', response.status);
713
+ * return response;
714
+ * });
715
+ */
716
+ addResponseInterceptor(interceptor) {
717
+ const id = this.nextInterceptorId++;
718
+ this.responseInterceptors.push({ id, interceptor });
719
+ return id;
720
+ }
721
+ /**
722
+ * Add a global error interceptor
723
+ * Error interceptors can handle or transform errors
724
+ *
725
+ * @param interceptor - The error interceptor function
726
+ * @returns The interceptor ID for later removal
727
+ *
728
+ * @example
729
+ * const id = Config.getInstance().addErrorInterceptor((error) => {
730
+ * console.error('Request failed:', error);
731
+ * throw error;
732
+ * });
733
+ */
734
+ addErrorInterceptor(interceptor) {
735
+ const id = this.nextInterceptorId++;
736
+ this.errorInterceptors.push({ id, interceptor });
737
+ return id;
738
+ }
739
+ /**
740
+ * Remove a request interceptor by its ID
741
+ *
742
+ * @param id - The interceptor ID returned from addRequestInterceptor
743
+ *
744
+ * @example
745
+ * Config.getInstance().removeRequestInterceptor(id);
746
+ */
747
+ removeRequestInterceptor(id) {
748
+ this.requestInterceptors = this.requestInterceptors.filter(item => item.id !== id);
749
+ }
750
+ /**
751
+ * Remove a response interceptor by its ID
752
+ *
753
+ * @param id - The interceptor ID returned from addResponseInterceptor
754
+ *
755
+ * @example
756
+ * Config.getInstance().removeResponseInterceptor(id);
757
+ */
758
+ removeResponseInterceptor(id) {
759
+ this.responseInterceptors = this.responseInterceptors.filter(item => item.id !== id);
760
+ }
761
+ /**
762
+ * Remove an error interceptor by its ID
763
+ *
764
+ * @param id - The interceptor ID returned from addErrorInterceptor
765
+ *
766
+ * @example
767
+ * Config.getInstance().removeErrorInterceptor(id);
768
+ */
769
+ removeErrorInterceptor(id) {
770
+ this.errorInterceptors = this.errorInterceptors.filter(item => item.id !== id);
771
+ }
772
+ /**
773
+ * Clear all interceptors (request, response, and error)
774
+ *
775
+ * @example
776
+ * Config.getInstance().clearInterceptors();
777
+ */
778
+ clearInterceptors() {
779
+ this.requestInterceptors = [];
780
+ this.responseInterceptors = [];
781
+ this.errorInterceptors = [];
782
+ }
783
+ /**
784
+ * Get all global request interceptors (in registration order)
785
+ * @internal
786
+ */
787
+ getRequestInterceptors() {
788
+ return this.requestInterceptors.map(item => item.interceptor);
789
+ }
790
+ /**
791
+ * Get all global response interceptors (in registration order)
792
+ * @internal
793
+ */
794
+ getResponseInterceptors() {
795
+ return this.responseInterceptors.map(item => item.interceptor);
933
796
  }
934
797
  /**
935
- * Check if anti-CSRF protection is enabled
936
- *
937
- * @returns True if anti-CSRF protection is enabled
798
+ * Get all global error interceptors (in registration order)
799
+ * @internal
938
800
  */
939
- isAntiCsrfEnabled() {
940
- return this.enableAntiCsrf;
801
+ getErrorInterceptors() {
802
+ return this.errorInterceptors.map(item => item.interceptor);
941
803
  }
942
804
  /**
943
805
  * Reset all configuration options to their default values
@@ -954,20 +816,63 @@ class Config {
954
816
  this.xsrfCookieName = "XSRF-TOKEN";
955
817
  this.xsrfHeaderName = "X-XSRF-TOKEN";
956
818
  this.enableAutoXsrf = true;
819
+ this.clearInterceptors();
957
820
  return this;
958
821
  }
959
- }/**
822
+ }
823
+
824
+ /**
960
825
  * Base class with common functionality for all request types
961
826
  * Provides the core request building and execution capabilities.
962
827
  */
963
828
  class BaseRequest {
964
- constructor() {
829
+ constructor(url) {
965
830
  this.requestOptions = {
966
831
  headers: {},
967
832
  };
968
833
  this.queryParams = new URLSearchParams();
969
834
  this.autoApplyCsrfProtection = true;
970
- this.cacheEnabled = false;
835
+ // Per-request interceptors
836
+ this.requestInterceptors = [];
837
+ this.responseInterceptors = [];
838
+ this.errorInterceptors = [];
839
+ this.url = url;
840
+ }
841
+ /**
842
+ * Creates a fluent API for setting enum-based options
843
+ * Combines direct setter with convenience methods
844
+ */
845
+ createFluentSetter(optionName, options) {
846
+ const fluent = {};
847
+ // Create convenience methods for each enum value
848
+ Object.entries(options).forEach(([key, value]) => {
849
+ fluent[key] = () => {
850
+ this.requestOptions[optionName] = value;
851
+ return this;
852
+ };
853
+ });
854
+ // Create the callable setter
855
+ const callable = (value) => {
856
+ this.requestOptions[optionName] = value;
857
+ return this;
858
+ };
859
+ return Object.assign(callable, fluent);
860
+ }
861
+ validateUrl(url) {
862
+ if (!(url === null || url === void 0 ? void 0 : url.trim()))
863
+ throw new RequestError("URL cannot be empty", url, this.method);
864
+ if (url.includes("\0") || url.includes("\r") || url.includes("\n")) {
865
+ throw new RequestError("Invalid URL (control chars)", url, this.method);
866
+ }
867
+ const trimmed = url.trim();
868
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
869
+ try {
870
+ new URL(trimmed);
871
+ }
872
+ catch (_a) {
873
+ throw new RequestError(`Invalid URL: ${trimmed}`, trimmed, this.method);
874
+ }
875
+ }
971
876
  }
972
877
  /**
973
878
  * Add multiple HTTP headers to the request
@@ -982,9 +887,16 @@ class BaseRequest {
982
887
  * });
983
888
  */
984
889
  withHeaders(headers) {
890
+ // Filter out null and undefined values
891
+ const filteredHeaders = {};
892
+ Object.entries(headers).forEach(([key, value]) => {
893
+ if (value !== null && value !== undefined) {
894
+ filteredHeaders[key] = value;
895
+ }
896
+ });
985
897
  this.requestOptions.headers = {
986
- ...this.requestOptions.headers,
987
- ...headers,
898
+ ...this.getHeadersRecord(),
899
+ ...filteredHeaders,
988
900
  };
989
901
  return this;
990
902
  }
@@ -999,7 +911,11 @@ class BaseRequest {
999
911
  * request.withHeader('Accept', 'application/json');
1000
912
  */
1001
913
  withHeader(key, value) {
1002
- return this.withHeaders({ [key]: value });
914
+ // Ignore null and undefined values
915
+ if (value !== null && value !== undefined) {
916
+ return this.withHeaders({ [key]: value });
917
+ }
918
+ return this;
1003
919
  }
1004
920
  /**
1005
921
  * Set a timeout for the request
@@ -1052,13 +968,18 @@ class BaseRequest {
1052
968
  return this;
1053
969
  }
1054
970
  /**
1055
- * Sets the credentials policy for the request
1056
- * @param credentialsPolicy - Can be 'omit', 'same-origin', or 'include'
1057
- * @returns The instance for chaining
971
+ * Sets credentials policy - supports both direct call and fluent API
972
+ * @example
973
+ * request.withCredentials("include")
974
+ * request.withCredentials(CredentialsPolicy.INCLUDE)
975
+ * request.withCredentials.INCLUDE()
1058
976
  */
1059
- withCredentials(credentialsPolicy = CredentialsPolicy.SAME_ORIGIN) {
1060
- this.requestOptions.credentials = credentialsPolicy;
1061
- return this;
977
+ get withCredentials() {
978
+ return this.createFluentSetter("credentials", {
979
+ INCLUDE: CredentialsPolicy.INCLUDE,
980
+ OMIT: CredentialsPolicy.OMIT,
981
+ SAME_ORIGIN: CredentialsPolicy.SAME_ORIGIN,
982
+ });
1062
983
  }
1063
984
  /**
1064
985
  * Allows providing an external AbortController to cancel the request
@@ -1079,22 +1000,37 @@ class BaseRequest {
1079
1000
  return this;
1080
1001
  }
1081
1002
  /**
1082
- * Sets the referrer policy for the request
1083
- * @param policy - The referrer policy to use
1084
- * @returns The instance for chaining
1003
+ * Sets referrer policy - supports both direct call and fluent API
1004
+ * @example
1005
+ * request.withReferrerPolicy("no-referrer")
1006
+ * request.withReferrerPolicy(ReferrerPolicy.NO_REFERRER)
1007
+ * request.withReferrerPolicy.NO_REFERRER()
1085
1008
  */
1086
- withReferrerPolicy(policy) {
1087
- this.requestOptions.referrerPolicy = policy;
1088
- return this;
1009
+ get withReferrerPolicy() {
1010
+ return this.createFluentSetter("referrerPolicy", {
1011
+ ORIGIN: ReferrerPolicy.ORIGIN,
1012
+ UNSAFE_URL: ReferrerPolicy.UNSAFE_URL,
1013
+ SAME_ORIGIN: ReferrerPolicy.SAME_ORIGIN,
1014
+ NO_REFERRER: ReferrerPolicy.NO_REFERRER,
1015
+ STRICT_ORIGIN: ReferrerPolicy.STRICT_ORIGIN,
1016
+ ORIGIN_WHEN_CROSS_ORIGIN: ReferrerPolicy.ORIGIN_WHEN_CROSS_ORIGIN,
1017
+ NO_REFERRER_WHEN_DOWNGRADE: ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE,
1018
+ STRICT_ORIGIN_WHEN_CROSS_ORIGIN: ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
1019
+ });
1089
1020
  }
1090
1021
  /**
1091
- * Sets the redirect policy for the request
1092
- * @param redirect - Can be 'follow', 'error', or 'manual'
1093
- * @returns The instance for chaining
1022
+ * Sets redirect mode - supports both direct call and fluent API
1023
+ * @example
1024
+ * request.withRedirect("follow")
1025
+ * request.withRedirect(RedirectMode.FOLLOW)
1026
+ * request.withRedirect.FOLLOW()
1094
1027
  */
1095
- withRedirect(redirect) {
1096
- this.requestOptions.redirect = redirect;
1097
- return this;
1028
+ get withRedirect() {
1029
+ return this.createFluentSetter("redirect", {
1030
+ FOLLOW: RedirectMode.FOLLOW,
1031
+ ERROR: RedirectMode.ERROR,
1032
+ MANUAL: RedirectMode.MANUAL,
1033
+ });
1098
1034
  }
1099
1035
  /**
1100
1036
  * Sets the keepalive flag for the request
@@ -1106,13 +1042,18 @@ class BaseRequest {
1106
1042
  return this;
1107
1043
  }
1108
1044
  /**
1109
- * Sets the priority of the request
1110
- * @param priority - Can be 'high', 'low', or 'auto'
1111
- * @returns The instance for chaining
1045
+ * Sets request priority - supports both direct call and fluent API
1046
+ * @example
1047
+ * request.withPriority("high")
1048
+ * request.withPriority(RequestPriority.HIGH)
1049
+ * request.withPriority.HIGH()
1112
1050
  */
1113
- withPriority(priority) {
1114
- this.requestOptions.priority = priority;
1115
- return this;
1051
+ get withPriority() {
1052
+ return this.createFluentSetter("priority", {
1053
+ HIGH: RequestPriority.HIGH,
1054
+ LOW: RequestPriority.LOW,
1055
+ AUTO: RequestPriority.AUTO,
1056
+ });
1116
1057
  }
1117
1058
  /**
1118
1059
  * Adds query parameters to the request URL
@@ -1150,13 +1091,19 @@ class BaseRequest {
1150
1091
  return this;
1151
1092
  }
1152
1093
  /**
1153
- * Sets the mode of the request
1154
- * @param mode - The mode for the request (cors, no-cors, same-origin, navigate)
1155
- * @returns The instance for chaining
1094
+ * Sets request mode - supports both direct call and fluent API
1095
+ * @example
1096
+ * request.withMode("cors")
1097
+ * request.withMode(RequestMode.CORS)
1098
+ * request.withMode.CORS()
1156
1099
  */
1157
- withMode(mode) {
1158
- this.requestOptions.mode = mode;
1159
- return this;
1100
+ get withMode() {
1101
+ return this.createFluentSetter("mode", {
1102
+ CORS: RequestMode.CORS,
1103
+ NO_CORS: RequestMode.NO_CORS,
1104
+ SAME_ORIGIN: RequestMode.SAME_ORIGIN,
1105
+ NAVIGATE: RequestMode.NAVIGATE,
1106
+ });
1160
1107
  }
1161
1108
  /**
1162
1109
  * Shorthand for setting the Content-Type header
@@ -1216,13 +1163,23 @@ class BaseRequest {
1216
1163
  withBearerToken(token) {
1217
1164
  return this.withAuthorization(`Bearer ${token}`);
1218
1165
  }
1166
+ /**
1167
+ * Safely get headers as a Record<string, string>
1168
+ * @returns The headers object
1169
+ */
1170
+ getHeadersRecord() {
1171
+ if (typeof this.requestOptions.headers === "object" && this.requestOptions.headers !== null) {
1172
+ return this.requestOptions.headers;
1173
+ }
1174
+ return {};
1175
+ }
1219
1176
  /**
1220
1177
  * Helper function to check for header presence in a case-insensitive way
1221
- * @param headers Headers object
1222
1178
  * @param headerName Header name to check
1223
1179
  * @returns Boolean indicating if the header exists (case-insensitive)
1224
1180
  */
1225
- hasHeader(headers, headerName) {
1181
+ hasHeader(headerName) {
1182
+ const headers = this.getHeadersRecord();
1226
1183
  return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
1227
1184
  }
1228
1185
  /**
@@ -1235,8 +1192,7 @@ class BaseRequest {
1235
1192
  if (cookieEntries.length === 0) {
1236
1193
  return this;
1237
1194
  }
1238
- // Get current headers or initialize empty object
1239
- const currentHeaders = this.requestOptions.headers || {};
1195
+ const currentHeaders = this.getHeadersRecord();
1240
1196
  // Get all existing cookie headers with different cases
1241
1197
  const cookieValues = [];
1242
1198
  const cookieHeaderKeys = [];
@@ -1305,127 +1261,178 @@ class BaseRequest {
1305
1261
  });
1306
1262
  }
1307
1263
  /**
1308
- * Configure request caching behavior
1309
- * When enabled, responses will be cached and reused for subsequent identical requests.
1264
+ * Add a request interceptor for this specific request
1265
+ * Request interceptors can modify the request configuration or return an early response
1310
1266
  *
1311
- * @param options - Cache configuration options
1312
- * @returns The request instance for chaining
1267
+ * @param interceptor - The request interceptor function
1268
+ * @returns The instance for chaining
1313
1269
  *
1314
1270
  * @example
1315
- * request.withCache({
1316
- * ttl: 60000, // Cache for 1 minute
1317
- * maxEntries: 100,
1318
- * storage: localStorage
1271
+ * request.withRequestInterceptor((config) => {
1272
+ * config.headers['X-Custom'] = 'value';
1273
+ * return config;
1319
1274
  * });
1320
1275
  */
1321
- withCache(options = {}) {
1322
- this.cacheEnabled = true;
1323
- this.cacheManager = new CacheManager(options);
1276
+ withRequestInterceptor(interceptor) {
1277
+ this.requestInterceptors.push(interceptor);
1324
1278
  return this;
1325
1279
  }
1326
1280
  /**
1327
- * Send the request to the specified URL
1328
- * This is the final method in the chain that executes the request.
1281
+ * Add a response interceptor for this specific request
1282
+ * Response interceptors can transform the response
1329
1283
  *
1330
- * @param url - The URL to send the request to
1331
- * @returns A promise with additional methods for processing the response
1284
+ * @param interceptor - The response interceptor function
1285
+ * @returns The instance for chaining
1332
1286
  *
1333
1287
  * @example
1334
- * const response = await request.sendTo('/api/users');
1335
- * const data = await response.getJson();
1288
+ * request.withResponseInterceptor((response) => {
1289
+ * console.log('Status:', response.status);
1290
+ * return response;
1291
+ * });
1292
+ */
1293
+ withResponseInterceptor(interceptor) {
1294
+ this.responseInterceptors.push(interceptor);
1295
+ return this;
1296
+ }
1297
+ /**
1298
+ * Add an error interceptor for this specific request
1299
+ * Error interceptors can handle or transform errors
1300
+ *
1301
+ * @param interceptor - The error interceptor function
1302
+ * @returns The instance for chaining
1303
+ *
1304
+ * @example
1305
+ * request.withErrorInterceptor((error) => {
1306
+ * console.error('Request failed:', error);
1307
+ * throw error;
1308
+ * });
1309
+ */
1310
+ withErrorInterceptor(interceptor) {
1311
+ this.errorInterceptors.push(interceptor);
1312
+ return this;
1313
+ }
1314
+ /**
1315
+ * Execute the request and return the ResponseWrapper
1316
+ * This is the base method for getting the full response.
1317
+ *
1318
+ * @returns A promise that resolves to the ResponseWrapper
1319
+ *
1320
+ * @example
1321
+ * const response = await request.getResponse();
1322
+ * console.log(response.status);
1323
+ */
1324
+ async getResponse() {
1325
+ const url = this.formatUrlWithQueryParams(this.url);
1326
+ this.applyCsrfProtection();
1327
+ const fetchOptions = {
1328
+ ...this.requestOptions,
1329
+ method: this.method,
1330
+ };
1331
+ return !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
1332
+ }
1333
+ /**
1334
+ * Execute the request and parse the response as JSON
1335
+ *
1336
+ * @returns A promise that resolves to the parsed JSON data
1337
+ *
1338
+ * @example
1339
+ * const users = await request.getJson<User[]>();
1340
+ */
1341
+ async getJson() {
1342
+ const response = await this.getResponse();
1343
+ return response.getJson();
1344
+ }
1345
+ /**
1346
+ * Execute the request and get the response as text
1347
+ *
1348
+ * @returns A promise that resolves to the response text
1349
+ *
1350
+ * @example
1351
+ * const text = await request.getText();
1352
+ */
1353
+ async getText() {
1354
+ const response = await this.getResponse();
1355
+ return response.getText();
1356
+ }
1357
+ /**
1358
+ * Execute the request and get the response as a Blob
1359
+ *
1360
+ * @returns A promise that resolves to the response Blob
1361
+ *
1362
+ * @example
1363
+ * const blob = await request.getBlob();
1364
+ */
1365
+ async getBlob() {
1366
+ const response = await this.getResponse();
1367
+ return response.getBlob();
1368
+ }
1369
+ /**
1370
+ * Execute the request and get the response body as a ReadableStream
1371
+ *
1372
+ * @returns A promise that resolves to the response body stream
1373
+ *
1374
+ * @example
1375
+ * const stream = await request.getBody();
1376
+ */
1377
+ async getBody() {
1378
+ const response = await this.getResponse();
1379
+ return response.getBody();
1380
+ }
1381
+ /**
1382
+ * Execute the request and extract specific data using a selector function
1383
+ * If no selector is provided, returns the full JSON response.
1384
+ *
1385
+ * @param selector - Optional function to extract and transform data
1386
+ * @returns A promise that resolves to the selected data
1387
+ *
1388
+ * @example
1389
+ * // Get full response
1390
+ * const data = await request.getData();
1391
+ *
1392
+ * // Extract specific data
1393
+ * const users = await request.getData(data => data.results.users);
1336
1394
  */
1337
- sendTo(url) {
1338
- // Format the URL with query parameters
1339
- url = this.formatUrlWithQueryParams(url);
1340
- // Apply automatic CSRF protection if enabled
1341
- if (this.autoApplyCsrfProtection) {
1342
- const config = Config.getInstance();
1343
- // Apply anti-CSRF headers if enabled
1344
- if (config.isAntiCsrfEnabled()) {
1395
+ async getData(selector) {
1396
+ const response = await this.getResponse();
1397
+ return response.getData(selector);
1398
+ }
1399
+ /**
1400
+ * Apply CSRF protection headers based on configuration
1401
+ */
1402
+ applyCsrfProtection() {
1403
+ if (!this.autoApplyCsrfProtection)
1404
+ return;
1405
+ const config = Config.getInstance();
1406
+ // Apply anti-CSRF headers if enabled
1407
+ if (config.isAntiCsrfEnabled()) {
1408
+ this.withHeaders({
1409
+ "X-Requested-With": "XMLHttpRequest",
1410
+ });
1411
+ }
1412
+ // Apply global CSRF token if set
1413
+ const globalToken = config.getCsrfToken();
1414
+ if (globalToken) {
1415
+ const csrfHeaderName = config.getCsrfHeaderName();
1416
+ const hasLocalToken = this.hasHeader("X-CSRF-Token") || this.hasHeader(csrfHeaderName);
1417
+ if (!hasLocalToken) {
1345
1418
  this.withHeaders({
1346
- "X-Requested-With": "XMLHttpRequest",
1419
+ [csrfHeaderName]: globalToken,
1347
1420
  });
1348
1421
  }
1349
- // Apply global CSRF token if set
1350
- const globalToken = config.getCsrfToken();
1351
- if (globalToken) {
1352
- // Check if local token exists using case-insensitive comparison
1353
- const headers = this.requestOptions.headers;
1354
- const csrfHeaderName = config.getCsrfHeaderName();
1355
- const hasLocalToken = this.hasHeader(headers, "X-CSRF-Token") || this.hasHeader(headers, csrfHeaderName);
1422
+ }
1423
+ // Check for XSRF token in cookies and send it as a header
1424
+ if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
1425
+ const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
1426
+ if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
1427
+ const xsrfHeaderName = config.getXsrfHeaderName();
1428
+ const hasLocalToken = this.hasHeader("X-XSRF-TOKEN") || this.hasHeader(xsrfHeaderName);
1356
1429
  if (!hasLocalToken) {
1357
1430
  this.withHeaders({
1358
- [csrfHeaderName]: globalToken,
1431
+ [xsrfHeaderName]: xsrfToken,
1359
1432
  });
1360
1433
  }
1361
1434
  }
1362
- // check for XSRF token in cookies and send it as a header
1363
- if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
1364
- const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
1365
- if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
1366
- const headers = this.requestOptions.headers;
1367
- const xsrfHeaderName = config.getXsrfHeaderName();
1368
- const hasLocalToken = this.hasHeader(headers, "X-XSRF-TOKEN") || this.hasHeader(headers, xsrfHeaderName);
1369
- if (!hasLocalToken) {
1370
- this.withHeaders({
1371
- [xsrfHeaderName]: xsrfToken,
1372
- });
1373
- }
1374
- }
1375
- }
1376
- }
1377
- // Cast the request options to RequestInit to ensure compatibility
1378
- const fetchOptions = {
1379
- ...this.requestOptions,
1380
- method: this.method,
1381
- };
1382
- // Create the base promise - handle caching if enabled
1383
- let basePromise;
1384
- if (this.cacheEnabled && this.cacheManager) {
1385
- const cacheKey = this.cacheManager.generateKey(url, this.method, this.requestOptions.headers, this.requestOptions.body);
1386
- basePromise = this.executeWithCache(url, fetchOptions, cacheKey);
1387
- }
1388
- else {
1389
- basePromise = !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
1390
1435
  }
1391
- const responsePromise = basePromise;
1392
- // Attach convenience methods that begin processing immediately
1393
- responsePromise.getJson = async () => {
1394
- return basePromise.then(response => response.getJson());
1395
- };
1396
- responsePromise.getText = async () => {
1397
- return basePromise.then(response => response.getText());
1398
- };
1399
- responsePromise.getBlob = async () => {
1400
- return basePromise.then(response => response.getBlob());
1401
- };
1402
- responsePromise.getArrayBuffer = async () => {
1403
- return basePromise.then(response => response.getArrayBuffer());
1404
- };
1405
- responsePromise.getBody = async () => {
1406
- return basePromise.then(response => response.getBody());
1407
- };
1408
- responsePromise.getData = async (selector) => {
1409
- try {
1410
- const data = await basePromise.then(response => response.getJson());
1411
- // If no selector is provided, return the raw JSON data
1412
- if (!selector)
1413
- return data;
1414
- // Apply the selector if provided
1415
- return selector(data);
1416
- }
1417
- catch (error) {
1418
- // Always enhance the error with the original data
1419
- const errorMessage = error instanceof Error ? error.message : String(error);
1420
- const enhancedError = new Error(`Data selector failed: ${errorMessage}. Original data: ${JSON.stringify(await basePromise.then(response => response.getJson()), null, 2)}`);
1421
- // Preserve the stack trace if available
1422
- if (error instanceof Error && error.stack) {
1423
- enhancedError.stack = error.stack;
1424
- }
1425
- throw enhancedError;
1426
- }
1427
- };
1428
- return responsePromise;
1429
1436
  }
1430
1437
  /**
1431
1438
  * Formats the URL with any query parameters
@@ -1461,12 +1468,13 @@ class BaseRequest {
1461
1468
  */
1462
1469
  async executeWithRetries(url, fetchOptions) {
1463
1470
  const maxRetries = this.requestOptions.retries || 0;
1471
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1464
1472
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
1465
1473
  try {
1466
1474
  return await this.executeRequest(url, fetchOptions);
1467
1475
  }
1468
1476
  catch (error) {
1469
- const requestError = error instanceof RequestError ? error : RequestError.networkError(url, fetchOptions.method, error);
1477
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, error instanceof Error ? error : new Error(String(error)));
1470
1478
  if (attempt >= maxRetries)
1471
1479
  throw requestError;
1472
1480
  if (this.requestOptions.onRetry) {
@@ -1475,121 +1483,286 @@ class BaseRequest {
1475
1483
  }
1476
1484
  }
1477
1485
  // This should never happen but is needed for type safety
1478
- throw new RequestError(`Max retries reached`, url, fetchOptions.method);
1486
+ throw new RequestError(`Max retries reached`, url, method);
1479
1487
  }
1480
- async executeRequest(url, fetchOptions) {
1481
- // Create a request controller for timeout if needed, or use the provided one
1482
- const controller = this.abortController || new AbortController();
1483
- const { signal } = controller;
1484
- // Use the controller signal for the fetch request, but don't overwrite an existing signal
1485
- fetchOptions.signal = fetchOptions.signal || signal;
1486
- let timeoutId;
1487
- // Flag to track if abort was caused by our timeout
1488
- let abortedByTimeout = false;
1489
- // Handle timeout regardless of whether an external controller is provided
1490
- if (this.requestOptions.timeout) {
1491
- if (this.abortController) {
1492
- // If we have an external controller, create a separate timeout controller
1493
- // that won't interfere with the external one
1494
- // Use AbortSignal.timeout if available (modern browsers)
1495
- if (typeof AbortSignal.timeout === "function") {
1496
- const timeoutSignal = AbortSignal.timeout(this.requestOptions.timeout);
1497
- // Add event listener to propagate the abort to the main controller
1498
- timeoutSignal.addEventListener("abort", () => {
1499
- if (!controller.signal.aborted) {
1500
- abortedByTimeout = true;
1501
- controller.abort();
1502
- }
1503
- });
1488
+ /**
1489
+ * Run request interceptors in order: global interceptors first, then per-request
1490
+ * @param configParam - The request configuration
1491
+ * @returns Modified config or a Response to short-circuit
1492
+ */
1493
+ async runRequestInterceptors(configParam) {
1494
+ const globalConfig = Config.getInstance();
1495
+ const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this.requestInterceptors];
1496
+ let currentConfig = configParam;
1497
+ for (let i = 0; i < allInterceptors.length; i++) {
1498
+ try {
1499
+ const result = await allInterceptors[i](currentConfig);
1500
+ // If interceptor returns a Response, short-circuit
1501
+ if (result instanceof Response) {
1502
+ return result;
1503
+ }
1504
+ currentConfig = result;
1505
+ }
1506
+ catch (error) {
1507
+ const errorMessage = error instanceof Error ? error.message : String(error);
1508
+ // Use RequestError when we have request context
1509
+ throw new RequestError(`Interceptor failed: ${errorMessage}`, currentConfig.url, currentConfig.method);
1510
+ }
1511
+ }
1512
+ return currentConfig;
1513
+ }
1514
+ /**
1515
+ * Run response interceptors in reverse order: per-request interceptors first, then global in reverse
1516
+ * @param response - The response wrapper
1517
+ * @returns Modified response wrapper
1518
+ */
1519
+ async runResponseInterceptors(response) {
1520
+ const globalConfig = Config.getInstance();
1521
+ const globalInterceptors = globalConfig.getResponseInterceptors();
1522
+ // Per-request in order, then global in reverse
1523
+ const allInterceptors = [...this.responseInterceptors, ...globalInterceptors.reverse()];
1524
+ let currentResponse = response;
1525
+ for (let i = 0; i < allInterceptors.length; i++) {
1526
+ try {
1527
+ currentResponse = await allInterceptors[i](currentResponse);
1528
+ }
1529
+ catch (error) {
1530
+ const errorMessage = error instanceof Error ? error.message : String(error);
1531
+ // Use RequestError when we have response context (url/method from ResponseWrapper)
1532
+ if (currentResponse.url && currentResponse.method) {
1533
+ throw new RequestError(`Interceptor failed: ${errorMessage}`, currentResponse.url, currentResponse.method);
1534
+ }
1535
+ // Fallback to generic Error if no context available (shouldn't happen in practice)
1536
+ throw new Error(`Interceptor failed: ${errorMessage}`);
1537
+ }
1538
+ }
1539
+ return currentResponse;
1540
+ }
1541
+ /**
1542
+ * Run error interceptors in reverse order: per-request interceptors first, then global in reverse
1543
+ * @param error - The error that occurred
1544
+ * @returns Modified error or a ResponseWrapper to recover
1545
+ */
1546
+ async runErrorInterceptors(error) {
1547
+ const globalConfig = Config.getInstance();
1548
+ const globalInterceptors = globalConfig.getErrorInterceptors();
1549
+ // Per-request in order, then global in reverse
1550
+ const allInterceptors = [...this.errorInterceptors, ...globalInterceptors.reverse()];
1551
+ let currentError = error;
1552
+ for (let i = 0; i < allInterceptors.length; i++) {
1553
+ try {
1554
+ // If previous interceptor recovered with a response, stop processing
1555
+ if (currentError instanceof ResponseWrapper) {
1556
+ return currentError;
1557
+ }
1558
+ const result = await allInterceptors[i](currentError);
1559
+ currentError = result;
1560
+ }
1561
+ catch (interceptorError) {
1562
+ // If an error interceptor throws, that becomes the new error
1563
+ if (interceptorError instanceof RequestError) {
1564
+ currentError = interceptorError;
1504
1565
  }
1505
1566
  else {
1506
- // Fallback for browsers without AbortSignal.timeout
1507
- timeoutId = setTimeout(() => {
1508
- if (!controller.signal.aborted) {
1509
- abortedByTimeout = true;
1510
- controller.abort();
1511
- }
1512
- }, this.requestOptions.timeout);
1567
+ const errorMessage = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
1568
+ // Always wrap in RequestError when we have context
1569
+ if (currentError instanceof RequestError) {
1570
+ currentError = new RequestError(`Error interceptor ${i + 1} failed: ${errorMessage}`, currentError.url, currentError.method);
1571
+ }
1572
+ else if (currentError instanceof ResponseWrapper && currentError.url && currentError.method) {
1573
+ // If it's a ResponseWrapper, we can still create a proper RequestError from its context
1574
+ currentError = new RequestError(`Error interceptor ${i + 1} failed: ${errorMessage}`, currentError.url, currentError.method);
1575
+ }
1576
+ else {
1577
+ // Last resort: if we have no context at all, use the original error's context
1578
+ // This shouldn't happen in practice, but handle it gracefully
1579
+ const errorObj = interceptorError instanceof Error ? interceptorError : new Error(String(interceptorError));
1580
+ currentError = RequestError.networkError(error.url, error.method, errorObj);
1581
+ }
1513
1582
  }
1514
1583
  }
1584
+ }
1585
+ return currentError;
1586
+ }
1587
+ /**
1588
+ * Helper to create an abort signal with timeout support
1589
+ * Handles various AbortSignal API levels gracefully
1590
+ */
1591
+ createAbortSignal(timeoutMs, externalController) {
1592
+ let timeoutId;
1593
+ let timeoutController;
1594
+ let isTimeout = false;
1595
+ // No timeout - just use external controller
1596
+ if (!timeoutMs) {
1597
+ return {
1598
+ signal: externalController === null || externalController === void 0 ? void 0 : externalController.signal,
1599
+ cleanup: () => { },
1600
+ wasTimeout: () => false,
1601
+ };
1602
+ }
1603
+ // Check if AbortSignal.any() is available (modern browsers)
1604
+ const hasAbortSignalAny = typeof AbortSignal.any === "function";
1605
+ const hasAbortSignalTimeout = typeof AbortSignal.timeout === "function";
1606
+ // Create timeout signal
1607
+ const createTimeoutSignal = () => {
1608
+ if (hasAbortSignalTimeout) {
1609
+ const signal = AbortSignal.timeout(timeoutMs);
1610
+ signal.addEventListener("abort", () => (isTimeout = true), { once: true });
1611
+ return signal;
1612
+ }
1515
1613
  else {
1614
+ timeoutController = new AbortController();
1516
1615
  timeoutId = setTimeout(() => {
1517
- abortedByTimeout = true;
1518
- controller.abort();
1519
- }, this.requestOptions.timeout);
1616
+ isTimeout = true;
1617
+ timeoutController.abort();
1618
+ }, timeoutMs);
1619
+ return timeoutController.signal;
1520
1620
  }
1621
+ };
1622
+ const timeoutSignal = createTimeoutSignal();
1623
+ // Combine signals if we have both timeout and external controller
1624
+ const finalSignal = externalController && hasAbortSignalAny
1625
+ ? AbortSignal.any([externalController.signal, timeoutSignal])
1626
+ : externalController
1627
+ ? this.combineSignalsManually(externalController.signal, timeoutSignal)
1628
+ : timeoutSignal;
1629
+ return {
1630
+ signal: finalSignal,
1631
+ cleanup: () => {
1632
+ if (timeoutId !== undefined)
1633
+ clearTimeout(timeoutId);
1634
+ },
1635
+ wasTimeout: () => isTimeout,
1636
+ };
1637
+ }
1638
+ /**
1639
+ * Manually combine two abort signals for older environments
1640
+ * Returns the first signal and listens to the second
1641
+ */
1642
+ combineSignalsManually(signal1, signal2) {
1643
+ // If either is already aborted, use that one
1644
+ if (signal1.aborted)
1645
+ return signal1;
1646
+ if (signal2.aborted)
1647
+ return signal2;
1648
+ // Use a controller to create a combined signal
1649
+ const controller = new AbortController();
1650
+ const abort = () => controller.abort();
1651
+ signal1.addEventListener("abort", abort, { once: true });
1652
+ signal2.addEventListener("abort", abort, { once: true });
1653
+ return controller.signal;
1654
+ }
1655
+ /**
1656
+ * Convert fetchOptions to RequestConfig with proper typing
1657
+ */
1658
+ createRequestConfig(url, fetchOptions) {
1659
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1660
+ const headers = this.getHeadersRecord();
1661
+ const extendedOptions = fetchOptions;
1662
+ return {
1663
+ url,
1664
+ method,
1665
+ headers,
1666
+ body: fetchOptions.body,
1667
+ signal: fetchOptions.signal || undefined,
1668
+ credentials: fetchOptions.credentials,
1669
+ mode: fetchOptions.mode,
1670
+ redirect: fetchOptions.redirect,
1671
+ referrer: fetchOptions.referrer,
1672
+ referrerPolicy: fetchOptions.referrerPolicy,
1673
+ keepalive: fetchOptions.keepalive,
1674
+ priority: extendedOptions.priority,
1675
+ };
1676
+ }
1677
+ /**
1678
+ * Apply interceptor results back to fetchOptions
1679
+ */
1680
+ applyRequestConfig(config, fetchOptions) {
1681
+ fetchOptions.headers = config.headers;
1682
+ if (config.body !== undefined) {
1683
+ fetchOptions.body = config.body;
1521
1684
  }
1685
+ }
1686
+ async executeRequest(url, fetchOptions) {
1687
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1688
+ // Setup abort signal with timeout
1689
+ const abortSignal = this.createAbortSignal(this.requestOptions.timeout, this.abortController);
1522
1690
  try {
1691
+ // Run request interceptors before making the request
1692
+ const requestConfig = this.createRequestConfig(url, fetchOptions);
1693
+ const interceptorResult = await this.runRequestInterceptors(requestConfig);
1694
+ // If interceptor returned a Response, short-circuit and wrap it
1695
+ if (interceptorResult instanceof Response) {
1696
+ const wrappedResponse = new ResponseWrapper(interceptorResult, this.url, this.method);
1697
+ return await this.runResponseInterceptors(wrappedResponse);
1698
+ }
1699
+ // Update fetchOptions with interceptor modifications
1700
+ url = interceptorResult.url;
1701
+ this.validateUrl(url);
1702
+ this.applyRequestConfig(interceptorResult, fetchOptions);
1703
+ // Set the combined abort signal
1704
+ fetchOptions.signal = abortSignal.signal;
1705
+ // Execute fetch
1523
1706
  let response;
1524
1707
  try {
1525
1708
  response = await fetch(url, fetchOptions);
1526
1709
  }
1527
1710
  catch (error) {
1528
- // Check if this is an abort error that was specifically caused by our timeout
1529
- if (error instanceof DOMException && error.name === "AbortError" && abortedByTimeout) {
1530
- throw RequestError.timeout(url, fetchOptions.method, this.requestOptions.timeout);
1711
+ // Check if it's an abort error (DOMException in browsers, or AbortSignal abort)
1712
+ if (error instanceof DOMException && error.name === "AbortError") {
1713
+ if (abortSignal.wasTimeout()) {
1714
+ throw RequestError.timeout(url, method, this.requestOptions.timeout);
1715
+ }
1716
+ throw RequestError.abortError(url, method);
1717
+ }
1718
+ // Check for Node.js/undici TimeoutError
1719
+ const errorObj = error instanceof Error ? error : new Error(String(error));
1720
+ const errorName = errorObj.name;
1721
+ const errorMessage = errorObj.message.toLowerCase();
1722
+ // Detect timeout errors from Node.js/undici (TimeoutError)
1723
+ const isTimeoutError = errorName === "TimeoutError" || errorMessage.includes("timeout") || errorMessage.includes("aborted due to timeout") || abortSignal.wasTimeout();
1724
+ if (isTimeoutError && this.requestOptions.timeout) {
1725
+ throw RequestError.timeout(url, method, this.requestOptions.timeout);
1531
1726
  }
1532
- // Otherwise it's either a user-triggered abort or another network error
1533
- throw RequestError.networkError(url, fetchOptions.method, error);
1727
+ throw RequestError.networkError(url, method, errorObj);
1534
1728
  }
1535
1729
  if (!response.ok) {
1536
- throw RequestError.fromResponse(response, url, fetchOptions.method);
1730
+ throw RequestError.fromResponse(response, url, method);
1537
1731
  }
1538
- // Return a wrapped response instead of parsing it
1539
- return new ResponseWrapper(response);
1732
+ const wrappedResponse = new ResponseWrapper(response, url, method);
1733
+ return await this.runResponseInterceptors(wrappedResponse);
1540
1734
  }
1541
- finally {
1542
- if (timeoutId) {
1543
- clearTimeout(timeoutId);
1544
- }
1545
- }
1546
- }
1547
- /**
1548
- * Executes a request using cache when possible
1549
- * @param url The formatted URL to send the request to
1550
- * @param fetchOptions The fetch options to use
1551
- * @param cacheKey The cache key for this request
1552
- */
1553
- async executeWithCache(url, fetchOptions, cacheKey) {
1554
- // Try to get from cache first
1555
- if (this.cacheManager) {
1556
- const cachedEntry = await this.cacheManager.get(cacheKey);
1557
- if (cachedEntry) {
1558
- // Create a synthetic Response object from cached data
1559
- const cachedResponse = new Response(JSON.stringify(cachedEntry.value), {
1560
- status: 200,
1561
- headers: new Headers(cachedEntry.headers || {}),
1562
- });
1563
- return new ResponseWrapper(cachedResponse);
1735
+ catch (error) {
1736
+ // Convert to RequestError if needed
1737
+ let requestError;
1738
+ if (error instanceof RequestError) {
1739
+ requestError = error;
1564
1740
  }
1565
- }
1566
- // Not in cache, make the actual request
1567
- const response = await (!this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions));
1568
- // Cache the successful response if caching is enabled
1569
- // Only cache successful responses
1570
- if (response.ok && this.cacheManager) {
1571
- // Clone the response so we don't consume it
1572
- try {
1573
- const responseData = await response.getJson();
1574
- // Extract headers to store in cache
1575
- const headers = {};
1576
- response.headers.forEach((value, key) => {
1577
- headers[key] = value;
1578
- });
1579
- // Store in cache
1580
- await this.cacheManager.set(cacheKey, responseData, headers);
1741
+ else {
1742
+ const errorObj = error instanceof Error ? error : new Error(String(error));
1743
+ requestError = RequestError.networkError(url, method, errorObj);
1581
1744
  }
1582
- catch (error) {
1583
- // Failed to cache, but we can still return the response
1584
- console.warn("Failed to cache response:", error);
1745
+ // Run error interceptors
1746
+ const interceptorResult = await this.runErrorInterceptors(requestError);
1747
+ // If error interceptor returned a ResponseWrapper, recover from error
1748
+ if (interceptorResult instanceof ResponseWrapper) {
1749
+ return interceptorResult;
1585
1750
  }
1751
+ throw interceptorResult;
1752
+ }
1753
+ finally {
1754
+ abortSignal.cleanup();
1586
1755
  }
1587
- return response;
1588
1756
  }
1589
- }/**
1757
+ }
1758
+
1759
+ /**
1590
1760
  * Base class for requests that can have a body (POST, PUT, PATCH)
1591
1761
  */
1592
1762
  class BodyRequest extends BaseRequest {
1763
+ constructor(url) {
1764
+ super(url);
1765
+ }
1593
1766
  /**
1594
1767
  * Sets the body of the request
1595
1768
  * @param body - The request body
@@ -1611,7 +1784,7 @@ class BodyRequest extends BaseRequest {
1611
1784
  JSON.stringify(body);
1612
1785
  }
1613
1786
  catch (error) {
1614
- throw new Error(`Failed to stringify request body: ${String(error)}`);
1787
+ throw new RequestError(`Stringify failed: ${String(error)}`, this.url, this.method);
1615
1788
  }
1616
1789
  }
1617
1790
  else {
@@ -1619,18 +1792,68 @@ class BodyRequest extends BaseRequest {
1619
1792
  }
1620
1793
  return this;
1621
1794
  }
1622
- setContentTypeIfNeeded(contentType) {
1623
- // Only set if not already set
1795
+ /**
1796
+ * Sets a GraphQL query or mutation as the request body
1797
+ * Automatically formats the body as JSON and sets Content-Type to application/json
1798
+ *
1799
+ * @param query - The GraphQL query or mutation string
1800
+ * @param variables - Optional variables object to pass with the query
1801
+ * @returns The request instance for chaining
1802
+ *
1803
+ * @example
1804
+ * const request = new PostRequest('/graphql')
1805
+ * .withGraphQL('query { user(id: $id) { name email } }', { id: '123' });
1806
+ *
1807
+ * @example
1808
+ * const request = new PostRequest('/graphql')
1809
+ * .withGraphQL('mutation { createUser(name: $name) { id } }', { name: 'John' });
1810
+ */
1811
+ withGraphQL(query, variables) {
1812
+ if (typeof query !== "string" || query.trim().length === 0) {
1813
+ throw new RequestError("Invalid GraphQL query", this.url, this.method);
1814
+ }
1815
+ const graphQLBody = {
1816
+ query: query.trim(),
1817
+ };
1818
+ if (variables !== undefined) {
1819
+ if (typeof variables !== "object" || variables === null || Array.isArray(variables)) {
1820
+ throw new RequestError("Invalid GraphQL variables", this.url, this.method);
1821
+ }
1822
+ graphQLBody.variables = variables;
1823
+ }
1824
+ // Validate JSON is stringifiable early
1825
+ try {
1826
+ JSON.stringify(graphQLBody);
1827
+ }
1828
+ catch (error) {
1829
+ throw new RequestError(`Stringify failed: ${String(error)}`, this.url, this.method);
1830
+ }
1831
+ this.body = graphQLBody;
1832
+ this.bodyType = BodyType.JSON;
1833
+ this.setContentTypeIfNeeded("application/json");
1834
+ return this;
1835
+ }
1836
+ /**
1837
+ * Check if Content-Type header is already set (case-insensitive)
1838
+ */
1839
+ hasContentType() {
1624
1840
  const headers = this.requestOptions.headers;
1625
- if (!Object.keys(headers).some(header => header.toLowerCase() === "content-type")) {
1841
+ if (typeof headers === "object" && headers !== null) {
1842
+ const headersObj = headers;
1843
+ return Object.keys(headersObj).some(header => header.toLowerCase() === "content-type");
1844
+ }
1845
+ return false;
1846
+ }
1847
+ setContentTypeIfNeeded(contentType) {
1848
+ if (!this.hasContentType()) {
1626
1849
  this.withContentType(contentType);
1627
1850
  }
1628
1851
  }
1629
1852
  /**
1630
- * Send the request to the specified URL
1853
+ * Execute the request and return the ResponseWrapper
1631
1854
  * Overrides the base implementation to add body handling
1632
1855
  */
1633
- sendTo(url) {
1856
+ async getResponse() {
1634
1857
  if (this.body !== undefined) {
1635
1858
  // Remove previous body if it exists
1636
1859
  if (this.requestOptions.body)
@@ -1643,20 +1866,22 @@ class BodyRequest extends BaseRequest {
1643
1866
  this.requestOptions.body = this.body;
1644
1867
  }
1645
1868
  }
1646
- return super.sendTo(url);
1869
+ return super.getResponse();
1647
1870
  }
1648
- }/**
1871
+ }
1872
+
1873
+ /**
1649
1874
  * HTTP GET request implementation
1650
1875
  * Used for retrieving data from an API endpoint.
1651
1876
  *
1652
1877
  * @example
1653
- * const request = new GetRequest()
1654
- * .withQueryParams({ id: '123' })
1655
- * .sendTo('/api/users');
1878
+ * const request = new GetRequest('/api/users')
1879
+ * .withQueryParams({ id: '123' });
1880
+ * const data = await request.getData();
1656
1881
  */
1657
1882
  class GetRequest extends BaseRequest {
1658
- constructor() {
1659
- super(...arguments);
1883
+ constructor(url) {
1884
+ super(url);
1660
1885
  this.method = HttpMethod.GET;
1661
1886
  }
1662
1887
  }
@@ -1665,12 +1890,12 @@ class GetRequest extends BaseRequest {
1665
1890
  * Similar to GET but returns only headers without a response body.
1666
1891
  *
1667
1892
  * @example
1668
- * const request = new HeadRequest()
1669
- * .sendTo('/api/users/123');
1893
+ * const request = new HeadRequest('/api/users/123');
1894
+ * const response = await request.getResponse();
1670
1895
  */
1671
1896
  class HeadRequest extends BaseRequest {
1672
- constructor() {
1673
- super(...arguments);
1897
+ constructor(url) {
1898
+ super(url);
1674
1899
  this.method = HttpMethod.HEAD;
1675
1900
  }
1676
1901
  }
@@ -1679,12 +1904,12 @@ class HeadRequest extends BaseRequest {
1679
1904
  * Used to describe the communication options for the target resource.
1680
1905
  *
1681
1906
  * @example
1682
- * const request = new OptionsRequest()
1683
- * .sendTo('/api/users');
1907
+ * const request = new OptionsRequest('/api/users');
1908
+ * const response = await request.getResponse();
1684
1909
  */
1685
1910
  class OptionsRequest extends BaseRequest {
1686
- constructor() {
1687
- super(...arguments);
1911
+ constructor(url) {
1912
+ super(url);
1688
1913
  this.method = HttpMethod.OPTIONS;
1689
1914
  }
1690
1915
  }
@@ -1693,12 +1918,12 @@ class OptionsRequest extends BaseRequest {
1693
1918
  * Used to delete a resource on the server.
1694
1919
  *
1695
1920
  * @example
1696
- * const request = new DeleteRequest()
1697
- * .sendTo('/api/users/123');
1921
+ * const request = new DeleteRequest('/api/users/123');
1922
+ * await request.getData();
1698
1923
  */
1699
1924
  class DeleteRequest extends BaseRequest {
1700
- constructor() {
1701
- super(...arguments);
1925
+ constructor(url) {
1926
+ super(url);
1702
1927
  this.method = HttpMethod.DELETE;
1703
1928
  }
1704
1929
  }
@@ -1707,13 +1932,13 @@ class DeleteRequest extends BaseRequest {
1707
1932
  * Used to create a new resource or submit data for processing.
1708
1933
  *
1709
1934
  * @example
1710
- * const request = new PostRequest()
1711
- * .withJson({ name: 'John', email: 'john@example.com' })
1712
- * .sendTo('/api/users');
1935
+ * const request = new PostRequest('/api/users')
1936
+ * .withJson({ name: 'John', email: 'john@example.com' });
1937
+ * const data = await request.getData();
1713
1938
  */
1714
1939
  class PostRequest extends BodyRequest {
1715
- constructor() {
1716
- super(...arguments);
1940
+ constructor(url) {
1941
+ super(url);
1717
1942
  this.method = HttpMethod.POST;
1718
1943
  }
1719
1944
  }
@@ -1722,13 +1947,13 @@ class PostRequest extends BodyRequest {
1722
1947
  * Used to replace or update an existing resource.
1723
1948
  *
1724
1949
  * @example
1725
- * const request = new PutRequest()
1726
- * .withJson({ id: '123', name: 'John', email: 'john@example.com' })
1727
- * .sendTo('/api/users/123');
1950
+ * const request = new PutRequest('/api/users/123')
1951
+ * .withJson({ id: '123', name: 'John', email: 'john@example.com' });
1952
+ * const data = await request.getData();
1728
1953
  */
1729
1954
  class PutRequest extends BodyRequest {
1730
- constructor() {
1731
- super(...arguments);
1955
+ constructor(url) {
1956
+ super(url);
1732
1957
  this.method = HttpMethod.PUT;
1733
1958
  }
1734
1959
  }
@@ -1737,117 +1962,123 @@ class PutRequest extends BodyRequest {
1737
1962
  * Used to apply partial modifications to a resource.
1738
1963
  *
1739
1964
  * @example
1740
- * const request = new PatchRequest()
1741
- * .withJson({ email: 'new.email@example.com' })
1742
- * .sendTo('/api/users/123');
1965
+ * const request = new PatchRequest('/api/users/123')
1966
+ * .withJson({ email: 'new.email@example.com' });
1967
+ * const data = await request.getData();
1743
1968
  */
1744
1969
  class PatchRequest extends BodyRequest {
1745
- constructor() {
1746
- super(...arguments);
1970
+ constructor(url) {
1971
+ super(url);
1747
1972
  this.method = HttpMethod.PATCH;
1748
1973
  }
1749
- }/**
1974
+ }
1975
+
1976
+ /**
1750
1977
  * Create a GET request
1751
1978
  * Used for retrieving resources from the server.
1752
1979
  *
1980
+ * @param url - The URL to send the request to
1753
1981
  * @returns A new GET request instance
1754
1982
  *
1755
1983
  * @example
1756
- * const users = await get()
1757
- * .withQueryParams({ limit: 10 })
1758
- * .sendTo('/api/users')
1759
- * .getJson();
1984
+ * const request = get('/api/users')
1985
+ * .withQueryParams({ limit: 10 });
1986
+ * const users = await request.getData();
1760
1987
  */
1761
- function get() {
1762
- return new GetRequest();
1988
+ function get(url) {
1989
+ return new GetRequest(url);
1763
1990
  }
1764
1991
  /**
1765
1992
  * Create a POST request
1766
1993
  * Used for creating new resources or submitting data.
1767
1994
  *
1995
+ * @param url - The URL to send the request to
1768
1996
  * @returns A new POST request instance
1769
1997
  *
1770
1998
  * @example
1771
- * const newUser = await post()
1772
- * .withJson({ name: 'John', email: 'john@example.com' })
1773
- * .sendTo('/api/users')
1774
- * .getJson();
1999
+ * const request = post('/api/users')
2000
+ * .withJson({ name: 'John', email: 'john@example.com' });
2001
+ * const newUser = await request.getData();
1775
2002
  */
1776
- function post() {
1777
- return new PostRequest();
2003
+ function post(url) {
2004
+ return new PostRequest(url);
1778
2005
  }
1779
2006
  /**
1780
2007
  * Create a PUT request
1781
2008
  * Used for replacing or updating an existing resource.
1782
2009
  *
2010
+ * @param url - The URL to send the request to
1783
2011
  * @returns A new PUT request instance
1784
2012
  *
1785
2013
  * @example
1786
- * const updatedUser = await put()
1787
- * .withJson({ name: 'John Updated', email: 'john@example.com' })
1788
- * .sendTo('/api/users/123')
1789
- * .getJson();
2014
+ * const request = put('/api/users/123')
2015
+ * .withJson({ name: 'John Updated', email: 'john@example.com' });
2016
+ * const updatedUser = await request.getData();
1790
2017
  */
1791
- function put() {
1792
- return new PutRequest();
2018
+ function put(url) {
2019
+ return new PutRequest(url);
1793
2020
  }
1794
2021
  /**
1795
2022
  * Create a DELETE request
1796
2023
  * Used for removing resources from the server.
1797
2024
  *
2025
+ * @param url - The URL to send the request to
1798
2026
  * @returns A new DELETE request instance
1799
2027
  *
1800
2028
  * @example
1801
- * await del()
1802
- * .sendTo('/api/users/123')
1803
- * .getJson();
2029
+ * const request = del('/api/users/123');
2030
+ * await request.getData();
1804
2031
  */
1805
- function del() {
1806
- return new DeleteRequest();
2032
+ function del(url) {
2033
+ return new DeleteRequest(url);
1807
2034
  }
1808
2035
  /**
1809
2036
  * Create a PATCH request
1810
2037
  * Used for applying partial modifications to a resource.
1811
2038
  *
2039
+ * @param url - The URL to send the request to
1812
2040
  * @returns A new PATCH request instance
1813
2041
  *
1814
2042
  * @example
1815
- * const patchedUser = await patch()
1816
- * .withJson({ status: 'active' })
1817
- * .sendTo('/api/users/123')
1818
- * .getJson();
2043
+ * const request = patch('/api/users/123')
2044
+ * .withJson({ status: 'active' });
2045
+ * const patchedUser = await request.getData();
1819
2046
  */
1820
- function patch() {
1821
- return new PatchRequest();
2047
+ function patch(url) {
2048
+ return new PatchRequest(url);
1822
2049
  }
1823
2050
  /**
1824
2051
  * Create a HEAD request
1825
2052
  * Similar to GET but returns only headers without a body.
1826
2053
  *
2054
+ * @param url - The URL to send the request to
1827
2055
  * @returns A new HEAD request instance
1828
2056
  *
1829
2057
  * @example
1830
- * const response = await head()
1831
- * .sendTo('/api/users/123');
2058
+ * const request = head('/api/users/123');
2059
+ * const response = await request.getResponse();
1832
2060
  * console.log(response.headers.get('Last-Modified'));
1833
2061
  */
1834
- function head() {
1835
- return new HeadRequest();
2062
+ function head(url) {
2063
+ return new HeadRequest(url);
1836
2064
  }
1837
2065
  /**
1838
2066
  * Create an OPTIONS request
1839
2067
  * Used to describe the communication options for a resource.
1840
2068
  *
2069
+ * @param url - The URL to send the request to
1841
2070
  * @returns A new OPTIONS request instance
1842
2071
  *
1843
2072
  * @example
1844
- * const response = await options()
1845
- * .sendTo('/api/users');
2073
+ * const request = options('/api/users');
2074
+ * const response = await request.getResponse();
1846
2075
  * console.log(response.headers.get('Allow'));
1847
2076
  */
1848
- function options() {
1849
- return new OptionsRequest();
1850
- }/**
2077
+ function options(url) {
2078
+ return new OptionsRequest(url);
2079
+ }
2080
+
2081
+ /**
1851
2082
  * Main API object for creating HTTP requests
1852
2083
  * Provides factory methods for all HTTP methods and access to global configuration.
1853
2084
  */
@@ -1860,4 +2091,7 @@ const create = {
1860
2091
  head,
1861
2092
  options,
1862
2093
  config: Config.getInstance(),
1863
- };export{CacheManager,CacheMode,CookieUtils,CredentialsPolicy,DeleteRequest,GetRequest,HeadRequest,HttpMethod,OptionsRequest,PatchRequest,PostRequest,PutRequest,RedirectMode,ReferrerPolicy,RequestError,RequestMode,RequestPriority,ResponseWrapper,SameSitePolicy,createLocalStorageStorage,createMemoryStorage,createSessionStorageStorage,create as default};//# sourceMappingURL=index.esm.js.map
2094
+ };
2095
+
2096
+ export { CookieUtils, CredentialsPolicy, DeleteRequest, GetRequest, HeadRequest, HttpMethod, OptionsRequest, PatchRequest, PostRequest, PutRequest, RedirectMode, ReferrerPolicy, RequestError, RequestMode, RequestPriority, ResponseWrapper, SameSitePolicy, create as default };
2097
+ //# sourceMappingURL=index.esm.js.map