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.
- package/README.md +366 -216
- package/dist/library/BaseRequest.d.ts +207 -51
- package/dist/library/BodyRequest.d.ts +25 -2
- package/dist/library/RequestError.d.ts +5 -2
- package/dist/library/ResponseWrapper.d.ts +17 -24
- package/dist/library/enums.d.ts +0 -11
- package/dist/library/index.cjs +2109 -0
- package/dist/library/index.cjs.map +1 -0
- package/dist/library/index.d.ts +2 -6
- package/dist/library/index.esm.js +1038 -804
- package/dist/library/index.esm.js.map +1 -1
- package/dist/library/index.esm.min.js +1 -1
- package/dist/library/index.esm.min.js.map +1 -1
- package/dist/library/index.min.cjs +1 -0
- package/dist/library/index.min.cjs.map +1 -0
- package/dist/library/requestFactories.d.ts +32 -30
- package/dist/library/requestMethods.d.ts +25 -18
- package/dist/library/types.d.ts +43 -4
- package/dist/library/utils/Config.d.ts +96 -0
- package/package.json +15 -15
- package/dist/library/index.js +0 -1863
- package/dist/library/index.js.map +0 -1
- package/dist/library/index.min.js +0 -1
- package/dist/library/index.min.js.map +0 -1
- package/dist/library/types/cache.d.ts +0 -62
- package/dist/library/utils/CacheManager.d.ts +0 -82
- package/dist/library/utils/CacheStorage.d.ts +0 -49
- package/dist/library/utils/StorageProvider.d.ts +0 -33
|
@@ -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
|
-
|
|
139
|
-
this.jsonCache =
|
|
140
|
-
return
|
|
220
|
+
const parsed = JSON.parse(this.textCache);
|
|
221
|
+
this.jsonCache = parsed;
|
|
222
|
+
return parsed;
|
|
141
223
|
}
|
|
142
224
|
catch (e) {
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
if (this.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
494
|
-
*
|
|
390
|
+
* Extract specific data using a selector function
|
|
391
|
+
* If no selector is provided, returns the full JSON response.
|
|
495
392
|
*
|
|
496
|
-
* @param
|
|
497
|
-
* @
|
|
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
|
-
*
|
|
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
|
-
*
|
|
551
|
-
*
|
|
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
|
|
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
|
|
562
|
-
//
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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
|
-
//
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
*
|
|
936
|
-
*
|
|
937
|
-
* @returns True if anti-CSRF protection is enabled
|
|
798
|
+
* Get all global error interceptors (in registration order)
|
|
799
|
+
* @internal
|
|
938
800
|
*/
|
|
939
|
-
|
|
940
|
-
return this.
|
|
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
|
-
|
|
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.
|
|
987
|
-
...
|
|
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
|
-
|
|
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
|
|
1056
|
-
* @
|
|
1057
|
-
*
|
|
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(
|
|
1060
|
-
this.
|
|
1061
|
-
|
|
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
|
|
1083
|
-
* @
|
|
1084
|
-
*
|
|
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(
|
|
1087
|
-
this.
|
|
1088
|
-
|
|
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
|
|
1092
|
-
* @
|
|
1093
|
-
*
|
|
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(
|
|
1096
|
-
this.
|
|
1097
|
-
|
|
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
|
|
1110
|
-
* @
|
|
1111
|
-
*
|
|
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(
|
|
1114
|
-
this.
|
|
1115
|
-
|
|
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
|
|
1154
|
-
* @
|
|
1155
|
-
*
|
|
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(
|
|
1158
|
-
this.
|
|
1159
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
*
|
|
1309
|
-
*
|
|
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
|
|
1312
|
-
* @returns The
|
|
1267
|
+
* @param interceptor - The request interceptor function
|
|
1268
|
+
* @returns The instance for chaining
|
|
1313
1269
|
*
|
|
1314
1270
|
* @example
|
|
1315
|
-
* request.
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
* storage: localStorage
|
|
1271
|
+
* request.withRequestInterceptor((config) => {
|
|
1272
|
+
* config.headers['X-Custom'] = 'value';
|
|
1273
|
+
* return config;
|
|
1319
1274
|
* });
|
|
1320
1275
|
*/
|
|
1321
|
-
|
|
1322
|
-
this.
|
|
1323
|
-
this.cacheManager = new CacheManager(options);
|
|
1276
|
+
withRequestInterceptor(interceptor) {
|
|
1277
|
+
this.requestInterceptors.push(interceptor);
|
|
1324
1278
|
return this;
|
|
1325
1279
|
}
|
|
1326
1280
|
/**
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1281
|
+
* Add a response interceptor for this specific request
|
|
1282
|
+
* Response interceptors can transform the response
|
|
1329
1283
|
*
|
|
1330
|
-
* @param
|
|
1331
|
-
* @returns
|
|
1284
|
+
* @param interceptor - The response interceptor function
|
|
1285
|
+
* @returns The instance for chaining
|
|
1332
1286
|
*
|
|
1333
1287
|
* @example
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
|
|
1419
|
+
[csrfHeaderName]: globalToken,
|
|
1347
1420
|
});
|
|
1348
1421
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
const hasLocalToken = this.hasHeader(
|
|
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
|
-
[
|
|
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,
|
|
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,
|
|
1486
|
+
throw new RequestError(`Max retries reached`, url, method);
|
|
1479
1487
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
let
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
// If
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
-
|
|
1518
|
-
|
|
1519
|
-
},
|
|
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
|
|
1529
|
-
if (error instanceof DOMException && error.name === "AbortError"
|
|
1530
|
-
|
|
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
|
-
|
|
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,
|
|
1730
|
+
throw RequestError.fromResponse(response, url, method);
|
|
1537
1731
|
}
|
|
1538
|
-
|
|
1539
|
-
return
|
|
1732
|
+
const wrappedResponse = new ResponseWrapper(response, url, method);
|
|
1733
|
+
return await this.runResponseInterceptors(wrappedResponse);
|
|
1540
1734
|
}
|
|
1541
|
-
|
|
1542
|
-
if
|
|
1543
|
-
|
|
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
|
-
|
|
1567
|
-
|
|
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
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
|
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
|
-
|
|
1623
|
-
|
|
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 (
|
|
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
|
-
*
|
|
1853
|
+
* Execute the request and return the ResponseWrapper
|
|
1631
1854
|
* Overrides the base implementation to add body handling
|
|
1632
1855
|
*/
|
|
1633
|
-
|
|
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.
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
1921
|
+
* const request = new DeleteRequest('/api/users/123');
|
|
1922
|
+
* await request.getData();
|
|
1698
1923
|
*/
|
|
1699
1924
|
class DeleteRequest extends BaseRequest {
|
|
1700
|
-
constructor() {
|
|
1701
|
-
super(
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
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(
|
|
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
|
|
1757
|
-
* .withQueryParams({ limit: 10 })
|
|
1758
|
-
*
|
|
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
|
|
1772
|
-
* .withJson({ name: 'John', email: 'john@example.com' })
|
|
1773
|
-
*
|
|
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
|
|
1787
|
-
* .withJson({ name: 'John Updated', email: 'john@example.com' })
|
|
1788
|
-
*
|
|
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
|
-
*
|
|
1802
|
-
*
|
|
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
|
|
1816
|
-
* .withJson({ status: 'active' })
|
|
1817
|
-
*
|
|
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
|
|
1831
|
-
*
|
|
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
|
|
1845
|
-
*
|
|
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
|
-
};
|
|
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
|