create-request 0.0.1-beta.8 → 1.0.0
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 +538 -331
- package/dist/library/BaseRequest.d.ts +275 -41
- package/dist/library/BodyRequest.d.ts +27 -4
- package/dist/library/RequestError.d.ts +5 -2
- package/dist/library/ResponseWrapper.d.ts +66 -13
- 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 +12 -12
- package/dist/library/index.esm.js +1322 -723
- package/dist/library/index.esm.js.map +1 -0
- package/dist/library/index.esm.min.js +1 -0
- package/dist/library/index.esm.min.js.map +1 -0
- package/dist/library/index.min.cjs +1 -0
- package/dist/library/index.min.cjs.map +1 -0
- package/dist/library/requestFactories.d.ts +70 -9
- package/dist/library/requestMethods.d.ts +70 -3
- package/dist/library/types.d.ts +44 -5
- package/dist/library/utils/Config.d.ts +164 -12
- package/dist/library/utils/CookieUtils.d.ts +1 -1
- package/dist/library/utils/sizeUtils.d.ts +8 -0
- package/package.json +74 -16
- package/dist/library/index.js +0 -1522
- package/dist/library/types/cache.d.ts +0 -62
- package/dist/library/utils/CacheManager.d.ts +0 -35
- package/dist/library/utils/CacheStorage.d.ts +0 -57
- 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
|
*/
|
|
@@ -101,7 +89,8 @@ class RequestError extends Error {
|
|
|
101
89
|
this.method = method;
|
|
102
90
|
this.status = options.status;
|
|
103
91
|
this.response = options.response;
|
|
104
|
-
this.
|
|
92
|
+
this.isTimeout = options.isTimeout;
|
|
93
|
+
this.isAborted = options.isAborted;
|
|
105
94
|
// For better stack traces in modern environments
|
|
106
95
|
if (Error.captureStackTrace) {
|
|
107
96
|
Error.captureStackTrace(this, RequestError);
|
|
@@ -113,30 +102,85 @@ class RequestError extends Error {
|
|
|
113
102
|
* Static methods for creating specific types of RequestError
|
|
114
103
|
*/
|
|
115
104
|
static timeout(url, method, timeoutMs) {
|
|
116
|
-
return new RequestError(`
|
|
117
|
-
|
|
105
|
+
return new RequestError(`Timeout: ${timeoutMs}ms`, url, method, {
|
|
106
|
+
isTimeout: true,
|
|
118
107
|
});
|
|
119
108
|
}
|
|
120
109
|
static fromResponse(response, url, method) {
|
|
121
|
-
return new RequestError(`
|
|
110
|
+
return new RequestError(`HTTP ${response.status}`, url, method, {
|
|
122
111
|
status: response.status,
|
|
123
112
|
response,
|
|
124
113
|
});
|
|
125
114
|
}
|
|
126
115
|
static networkError(url, method, originalError) {
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|
|
129
165
|
return error;
|
|
130
166
|
}
|
|
167
|
+
static abortError(url, method) {
|
|
168
|
+
return new RequestError("Aborted", url, method, {
|
|
169
|
+
isAborted: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
131
172
|
}
|
|
132
173
|
|
|
133
174
|
/**
|
|
134
175
|
* Wrapper for HTTP responses with methods to transform the response data
|
|
176
|
+
* Provides caching and conversion between different response formats.
|
|
135
177
|
*/
|
|
136
178
|
class ResponseWrapper {
|
|
137
|
-
constructor(response) {
|
|
179
|
+
constructor(response, url, method) {
|
|
138
180
|
this.bodyUsed = false;
|
|
139
181
|
this.response = response;
|
|
182
|
+
this.url = url;
|
|
183
|
+
this.method = method;
|
|
140
184
|
}
|
|
141
185
|
// Wrapper properties
|
|
142
186
|
get status() {
|
|
@@ -154,7 +198,17 @@ class ResponseWrapper {
|
|
|
154
198
|
get raw() {
|
|
155
199
|
return this.response;
|
|
156
200
|
}
|
|
157
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Parse the response body as JSON
|
|
203
|
+
* Caches the result for subsequent calls.
|
|
204
|
+
*
|
|
205
|
+
* @returns The parsed JSON data
|
|
206
|
+
* @throws Error if the response has already been consumed in a different format
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* const data = await response.getJson();
|
|
210
|
+
* console.log(data.items);
|
|
211
|
+
*/
|
|
158
212
|
async getJson() {
|
|
159
213
|
if (this.jsonCache !== undefined) {
|
|
160
214
|
return this.jsonCache;
|
|
@@ -163,20 +217,55 @@ class ResponseWrapper {
|
|
|
163
217
|
// If we already have text, try to parse it as JSON
|
|
164
218
|
if (this.textCache !== undefined) {
|
|
165
219
|
try {
|
|
166
|
-
|
|
167
|
-
this.jsonCache =
|
|
168
|
-
return
|
|
220
|
+
const parsed = JSON.parse(this.textCache);
|
|
221
|
+
this.jsonCache = parsed;
|
|
222
|
+
return parsed;
|
|
169
223
|
}
|
|
170
224
|
catch (e) {
|
|
171
|
-
|
|
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}`);
|
|
172
232
|
}
|
|
173
233
|
}
|
|
174
|
-
|
|
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");
|
|
175
240
|
}
|
|
176
241
|
this.bodyUsed = true;
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Get the response body as text
|
|
260
|
+
* Caches the result for subsequent calls. Attempts to convert from JSON or Blob
|
|
261
|
+
* if the body has already been consumed in those formats.
|
|
262
|
+
*
|
|
263
|
+
* @returns The response text
|
|
264
|
+
* @throws Error if the response has already been consumed in an incompatible format
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* const text = await response.getText();
|
|
268
|
+
*/
|
|
180
269
|
async getText() {
|
|
181
270
|
if (this.textCache !== undefined) {
|
|
182
271
|
return this.textCache;
|
|
@@ -189,15 +278,55 @@ class ResponseWrapper {
|
|
|
189
278
|
}
|
|
190
279
|
// If we have blob, convert to text
|
|
191
280
|
if (this.blobCache !== undefined) {
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (this.url && this.method) {
|
|
296
|
+
throw new RequestError("Cannot convert to text", this.url, this.method, {
|
|
297
|
+
response: this.response,
|
|
298
|
+
});
|
|
194
299
|
}
|
|
195
|
-
throw new Error("
|
|
300
|
+
throw new Error("Cannot convert to text");
|
|
196
301
|
}
|
|
197
302
|
this.bodyUsed = true;
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Get the response body as a Blob
|
|
320
|
+
* Caches the result for subsequent calls. Attempts to convert from text
|
|
321
|
+
* if the body has already been consumed as text.
|
|
322
|
+
*
|
|
323
|
+
* @returns The response as a Blob
|
|
324
|
+
* @throws Error if the response has already been consumed in an incompatible format
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* const blob = await response.getBlob();
|
|
328
|
+
* const url = URL.createObjectURL(blob);
|
|
329
|
+
*/
|
|
201
330
|
async getBlob() {
|
|
202
331
|
if (this.blobCache !== undefined) {
|
|
203
332
|
return this.blobCache;
|
|
@@ -208,367 +337,192 @@ class ResponseWrapper {
|
|
|
208
337
|
this.blobCache = new Blob([this.textCache], { type: "text/plain" });
|
|
209
338
|
return this.blobCache;
|
|
210
339
|
}
|
|
211
|
-
|
|
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");
|
|
212
346
|
}
|
|
213
347
|
this.bodyUsed = true;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
async getArrayBuffer() {
|
|
218
|
-
if (this.arrayBufferCache !== undefined) {
|
|
219
|
-
return this.arrayBufferCache;
|
|
348
|
+
try {
|
|
349
|
+
this.blobCache = await this.response.blob();
|
|
350
|
+
return this.blobCache;
|
|
220
351
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (this.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return this.arrayBufferCache;
|
|
229
|
-
}
|
|
230
|
-
// If we have blob, convert to ArrayBuffer
|
|
231
|
-
if (this.blobCache !== undefined) {
|
|
232
|
-
this.arrayBufferCache = await this.blobCache.arrayBuffer();
|
|
233
|
-
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
|
+
});
|
|
234
359
|
}
|
|
235
|
-
throw
|
|
360
|
+
throw e;
|
|
236
361
|
}
|
|
237
|
-
this.bodyUsed = true;
|
|
238
|
-
this.arrayBufferCache = await this.response.arrayBuffer();
|
|
239
|
-
return this.arrayBufferCache;
|
|
240
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Get the raw response body as a ReadableStream
|
|
365
|
+
* This method consumes the body and should only be called once.
|
|
366
|
+
*
|
|
367
|
+
* @returns The response body as a ReadableStream or null
|
|
368
|
+
* @throws Error if the response body has already been consumed
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* const stream = response.getBody();
|
|
372
|
+
* if (stream) {
|
|
373
|
+
* const reader = stream.getReader();
|
|
374
|
+
* // Process the stream
|
|
375
|
+
* }
|
|
376
|
+
*/
|
|
241
377
|
getBody() {
|
|
242
378
|
if (this.bodyUsed) {
|
|
243
|
-
|
|
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");
|
|
244
385
|
}
|
|
245
386
|
this.bodyUsed = true;
|
|
246
387
|
return this.response.body;
|
|
247
388
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
* Create a memory storage provider using Map
|
|
276
|
-
*/
|
|
277
|
-
function createMemoryStorage() {
|
|
278
|
-
const store = new Map();
|
|
279
|
-
return {
|
|
280
|
-
get(key) {
|
|
281
|
-
return store.get(key) || null;
|
|
282
|
-
},
|
|
283
|
-
set(key, value) {
|
|
284
|
-
store.set(key, value);
|
|
285
|
-
},
|
|
286
|
-
has(key) {
|
|
287
|
-
return store.has(key);
|
|
288
|
-
},
|
|
289
|
-
delete(key) {
|
|
290
|
-
store.delete(key);
|
|
291
|
-
},
|
|
292
|
-
clear() {
|
|
293
|
-
store.clear();
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Create a localStorage-based storage provider
|
|
299
|
-
*/
|
|
300
|
-
function createLocalStorageStorage() {
|
|
301
|
-
return {
|
|
302
|
-
get(key) {
|
|
303
|
-
if (typeof localStorage === "undefined")
|
|
304
|
-
return null;
|
|
305
|
-
return localStorage.getItem(key);
|
|
306
|
-
},
|
|
307
|
-
set(key, value) {
|
|
308
|
-
if (typeof localStorage === "undefined")
|
|
309
|
-
return;
|
|
310
|
-
try {
|
|
311
|
-
localStorage.setItem(key, value);
|
|
389
|
+
/**
|
|
390
|
+
* Extract specific data using a selector function
|
|
391
|
+
* If no selector is provided, returns the full JSON response.
|
|
392
|
+
*
|
|
393
|
+
* @param selector - Optional function to extract and transform data
|
|
394
|
+
* @returns A promise that resolves to the selected data
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* // Get full response
|
|
398
|
+
* const data = await response.getData();
|
|
399
|
+
*
|
|
400
|
+
* // Extract specific data
|
|
401
|
+
* const users = await response.getData(data => data.results.users);
|
|
402
|
+
*/
|
|
403
|
+
async getData(selector) {
|
|
404
|
+
try {
|
|
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);
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
// If it's already a RequestError, re-throw it
|
|
414
|
+
if (error instanceof RequestError) {
|
|
415
|
+
throw error;
|
|
312
416
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
catch (e) {
|
|
321
|
-
console.error("Failed to set cache item even after clearing cache");
|
|
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
|
+
});
|
|
322
424
|
}
|
|
425
|
+
throw new Error(`Data selector failed: ${errorMessage}`);
|
|
323
426
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
localStorage.removeItem(key);
|
|
334
|
-
},
|
|
335
|
-
clear() {
|
|
336
|
-
if (typeof localStorage === "undefined")
|
|
337
|
-
return;
|
|
338
|
-
localStorage.clear();
|
|
339
|
-
},
|
|
340
|
-
};
|
|
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);
|
|
432
|
+
}
|
|
433
|
+
throw errorObj;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
341
436
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (typeof
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
sessionStorage.setItem(key, value);
|
|
437
|
+
|
|
438
|
+
class CookieUtils {
|
|
439
|
+
/**
|
|
440
|
+
* Formats cookies for a request
|
|
441
|
+
* @param cookies Object containing cookie name-value pairs or cookie options
|
|
442
|
+
* @returns Formatted cookie string for the Cookie header
|
|
443
|
+
*/
|
|
444
|
+
static formatRequestCookies(cookies) {
|
|
445
|
+
const cookiePairs = [];
|
|
446
|
+
Object.entries(cookies).forEach(([name, valueOrOptions]) => {
|
|
447
|
+
let value;
|
|
448
|
+
if (typeof valueOrOptions === "string") {
|
|
449
|
+
value = valueOrOptions;
|
|
357
450
|
}
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
void this.clear();
|
|
362
|
-
try {
|
|
363
|
-
sessionStorage.setItem(key, value);
|
|
364
|
-
}
|
|
365
|
-
catch (e) {
|
|
366
|
-
console.error("Failed to set cache item even after clearing cache");
|
|
367
|
-
}
|
|
451
|
+
else {
|
|
452
|
+
// Extract value from options object without validation
|
|
453
|
+
value = valueOrOptions.value;
|
|
368
454
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return sessionStorage.getItem(key) !== null;
|
|
374
|
-
},
|
|
375
|
-
delete(key) {
|
|
376
|
-
if (typeof sessionStorage === "undefined")
|
|
377
|
-
return;
|
|
378
|
-
sessionStorage.removeItem(key);
|
|
379
|
-
},
|
|
380
|
-
clear() {
|
|
381
|
-
if (typeof sessionStorage === "undefined")
|
|
382
|
-
return;
|
|
383
|
-
sessionStorage.clear();
|
|
384
|
-
},
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* Create a storage provider from a cache options object
|
|
389
|
-
*/
|
|
390
|
-
function createStorageProvider(options) {
|
|
391
|
-
const { storage } = options || {};
|
|
392
|
-
if (!storage) {
|
|
393
|
-
// Default to memory storage
|
|
394
|
-
return createMemoryStorage();
|
|
395
|
-
}
|
|
396
|
-
// Check if storage is already a StorageProvider
|
|
397
|
-
if (typeof storage.get === "function" && typeof storage.set === "function") {
|
|
398
|
-
return storage;
|
|
399
|
-
}
|
|
400
|
-
// Check if it's localStorage or sessionStorage
|
|
401
|
-
if (storage === localStorage) {
|
|
402
|
-
return createLocalStorageStorage();
|
|
403
|
-
}
|
|
404
|
-
if (storage === sessionStorage) {
|
|
405
|
-
return createSessionStorageStorage();
|
|
406
|
-
}
|
|
407
|
-
// Check if it's a Map
|
|
408
|
-
if (storage instanceof Map) {
|
|
409
|
-
return {
|
|
410
|
-
get(key) {
|
|
411
|
-
return storage.get(key) || null;
|
|
412
|
-
},
|
|
413
|
-
set(key, value, _ttl) {
|
|
414
|
-
storage.set(key, value);
|
|
415
|
-
},
|
|
416
|
-
has(key) {
|
|
417
|
-
return storage.has(key);
|
|
418
|
-
},
|
|
419
|
-
delete(key) {
|
|
420
|
-
storage.delete(key);
|
|
421
|
-
},
|
|
422
|
-
clear() {
|
|
423
|
-
storage.clear();
|
|
424
|
-
},
|
|
425
|
-
};
|
|
455
|
+
// Add the cookie to the request
|
|
456
|
+
cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
|
|
457
|
+
});
|
|
458
|
+
return cookiePairs.join("; ");
|
|
426
459
|
}
|
|
427
|
-
// Default to memory storage
|
|
428
|
-
console.warn("Unsupported storage type, falling back to memory storage");
|
|
429
|
-
return createMemoryStorage();
|
|
430
460
|
}
|
|
431
461
|
|
|
432
462
|
/**
|
|
433
|
-
*
|
|
463
|
+
* Utility class for CSRF token management
|
|
434
464
|
*/
|
|
435
|
-
class
|
|
436
|
-
constructor(options = {}) {
|
|
437
|
-
this.entryCount = 0;
|
|
438
|
-
this.totalSize = 0;
|
|
439
|
-
this.options = options;
|
|
440
|
-
this.storage = createStorageProvider(options);
|
|
441
|
-
}
|
|
465
|
+
class CsrfUtils {
|
|
442
466
|
/**
|
|
443
|
-
*
|
|
467
|
+
* Extracts CSRF token from a meta tag in the document head
|
|
468
|
+
* @param metaName The name attribute of the meta tag (default: "csrf-token")
|
|
469
|
+
* @returns The CSRF token or null if not found
|
|
444
470
|
*/
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const customKey = this.options.keyGenerator(url, method, headers, body);
|
|
449
|
-
return this.options.keyPrefix ? `${this.options.keyPrefix}:${customKey}` : customKey;
|
|
450
|
-
}
|
|
451
|
-
// Default key generation
|
|
452
|
-
let key = `${method}:${url}`;
|
|
453
|
-
// Add vary headers to key if specified
|
|
454
|
-
if (this.options.varyByHeaders && headers) {
|
|
455
|
-
const varyHeaders = {};
|
|
456
|
-
this.options.varyByHeaders.forEach(header => {
|
|
457
|
-
const normalizedHeader = header.toLowerCase();
|
|
458
|
-
// Find the header in case-insensitive way
|
|
459
|
-
Object.keys(headers).forEach(h => {
|
|
460
|
-
if (h.toLowerCase() === normalizedHeader) {
|
|
461
|
-
varyHeaders[normalizedHeader] = headers[h];
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
});
|
|
465
|
-
if (Object.keys(varyHeaders).length > 0) {
|
|
466
|
-
key += `:${JSON.stringify(varyHeaders)}`;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
// Include body in cache key for non-GET requests if it's a simple value
|
|
470
|
-
if (method !== "GET" && body !== undefined && body !== null) {
|
|
471
|
-
try {
|
|
472
|
-
if (typeof body === "string" || typeof body === "number" || typeof body === "boolean") {
|
|
473
|
-
key += `:${String(body)}`;
|
|
474
|
-
}
|
|
475
|
-
else if (typeof body === "object") {
|
|
476
|
-
key += `:${JSON.stringify(body)}`;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
catch (e) {
|
|
480
|
-
console.warn("Failed to stringify request body for cache key");
|
|
481
|
-
}
|
|
471
|
+
static getTokenFromMeta(metaName = "csrf-token") {
|
|
472
|
+
if (typeof document === "undefined") {
|
|
473
|
+
return null;
|
|
482
474
|
}
|
|
483
|
-
|
|
484
|
-
return
|
|
475
|
+
const meta = document.querySelector(`meta[name="${metaName}"]`);
|
|
476
|
+
return (meta === null || meta === void 0 ? void 0 : meta.getAttribute("content")) || null;
|
|
485
477
|
}
|
|
486
478
|
/**
|
|
487
|
-
*
|
|
479
|
+
* Extracts CSRF token from a cookie
|
|
480
|
+
* @param cookieName The name of the cookie containing the CSRF token
|
|
481
|
+
* @returns The CSRF token or null if not found
|
|
488
482
|
*/
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (!value)
|
|
483
|
+
static getTokenFromCookie(cookieName = "csrf-token") {
|
|
484
|
+
if (typeof document === "undefined") {
|
|
492
485
|
return null;
|
|
493
|
-
try {
|
|
494
|
-
const entry = JSON.parse(value);
|
|
495
|
-
// Check if the entry has expired
|
|
496
|
-
if (entry.expiry && entry.expiry < Date.now()) {
|
|
497
|
-
await Promise.resolve(this.storage.delete(key));
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
500
|
-
return entry;
|
|
501
486
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
487
|
+
const cookies = document.cookie.split(";");
|
|
488
|
+
for (const cookie of cookies) {
|
|
489
|
+
const [name, value] = cookie.trim().split("=");
|
|
490
|
+
if (name === cookieName) {
|
|
491
|
+
return decodeURIComponent(value);
|
|
492
|
+
}
|
|
506
493
|
}
|
|
494
|
+
return null;
|
|
507
495
|
}
|
|
508
496
|
/**
|
|
509
|
-
*
|
|
497
|
+
* Validates if the provided string is a potential CSRF token
|
|
498
|
+
* Checks if the token meets security requirements
|
|
499
|
+
* @param token The token to validate
|
|
500
|
+
* @returns Whether the token is valid
|
|
510
501
|
*/
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
value,
|
|
516
|
-
timestamp: now,
|
|
517
|
-
expiry: ttl ? now + ttl : null,
|
|
518
|
-
headers,
|
|
519
|
-
};
|
|
520
|
-
const serialized = JSON.stringify(entry);
|
|
521
|
-
// Check size constraints if applicable
|
|
522
|
-
if (this.options.maxSize) {
|
|
523
|
-
const maxBytes = parseSize(this.options.maxSize);
|
|
524
|
-
const entrySize = new TextEncoder().encode(serialized).length;
|
|
525
|
-
// If a single entry exceeds the max size, reject it without clearing the cache
|
|
526
|
-
if (entrySize > maxBytes) {
|
|
527
|
-
console.warn(`Cache entry size (${entrySize} bytes) exceeds maximum cache size (${maxBytes} bytes)`);
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
// Only clear if adding this entry would exceed the total limit
|
|
531
|
-
if (this.totalSize + entrySize > maxBytes) {
|
|
532
|
-
// Simple strategy: clear everything
|
|
533
|
-
// A more sophisticated strategy would remove oldest entries first
|
|
534
|
-
await Promise.resolve(this.storage.clear());
|
|
535
|
-
this.totalSize = 0;
|
|
536
|
-
this.entryCount = 0;
|
|
537
|
-
}
|
|
538
|
-
this.totalSize += entrySize;
|
|
502
|
+
static isValidToken(token) {
|
|
503
|
+
// Basic checks for null/undefined and type
|
|
504
|
+
if (typeof token !== "string") {
|
|
505
|
+
return false;
|
|
539
506
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
507
|
+
if (token.length < 8)
|
|
508
|
+
return false;
|
|
509
|
+
// If token is longer than 10 chars, perform additional security checks
|
|
510
|
+
if (token.length > 10) {
|
|
511
|
+
// Check for valid character set (alphanumeric & common token symbols)
|
|
512
|
+
const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
|
|
513
|
+
if (!validTokenRegex.test(token)) {
|
|
514
|
+
return false;
|
|
548
515
|
}
|
|
516
|
+
// For longer tokens, check for sufficient entropy (at least 2 character types)
|
|
517
|
+
const hasUpperCase = /[A-Z]/.test(token);
|
|
518
|
+
const hasLowerCase = /[a-z]/.test(token);
|
|
519
|
+
const hasNumbers = /[0-9]/.test(token);
|
|
520
|
+
const hasSpecials = /[-_=+/.]/.test(token);
|
|
521
|
+
const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
|
|
522
|
+
return characterTypesCount >= 2;
|
|
549
523
|
}
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Check if a key exists in the cache
|
|
555
|
-
*/
|
|
556
|
-
async has(key) {
|
|
557
|
-
return Promise.resolve(this.storage.has(key));
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Delete a key from the cache
|
|
561
|
-
*/
|
|
562
|
-
async delete(key) {
|
|
563
|
-
return Promise.resolve(this.storage.delete(key));
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Clear the entire cache
|
|
567
|
-
*/
|
|
568
|
-
async clear() {
|
|
569
|
-
this.entryCount = 0;
|
|
570
|
-
this.totalSize = 0;
|
|
571
|
-
return Promise.resolve(this.storage.clear());
|
|
524
|
+
// For shorter tokens (8-10 chars), just return true if we reached here
|
|
525
|
+
return true;
|
|
572
526
|
}
|
|
573
527
|
}
|
|
574
528
|
|
|
@@ -584,9 +538,20 @@ class Config {
|
|
|
584
538
|
this.csrfToken = null;
|
|
585
539
|
this.enableAutoXsrf = true;
|
|
586
540
|
this.enableAntiCsrf = true; // X-Requested-With header
|
|
541
|
+
// Interceptor configuration
|
|
542
|
+
this.requestInterceptors = [];
|
|
543
|
+
this.responseInterceptors = [];
|
|
544
|
+
this.errorInterceptors = [];
|
|
545
|
+
this.nextInterceptorId = 1;
|
|
587
546
|
}
|
|
588
547
|
/**
|
|
589
|
-
* Get the singleton instance
|
|
548
|
+
* Get the singleton instance of the Config class
|
|
549
|
+
*
|
|
550
|
+
* @returns The global configuration instance
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* const config = Config.getInstance();
|
|
554
|
+
* config.setCsrfToken('token123');
|
|
590
555
|
*/
|
|
591
556
|
static getInstance() {
|
|
592
557
|
if (!Config.instance) {
|
|
@@ -596,228 +561,425 @@ class Config {
|
|
|
596
561
|
}
|
|
597
562
|
/**
|
|
598
563
|
* Set a global CSRF token to be used for all requests
|
|
564
|
+
*
|
|
565
|
+
* @param token - The CSRF token value
|
|
566
|
+
* @returns The config instance for chaining
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* Config.getInstance().setCsrfToken('myToken123');
|
|
599
570
|
*/
|
|
600
571
|
setCsrfToken(token) {
|
|
601
572
|
this.csrfToken = token;
|
|
602
573
|
return this;
|
|
603
574
|
}
|
|
604
575
|
/**
|
|
605
|
-
* Get the global CSRF token
|
|
576
|
+
* Get the global CSRF token that will be automatically applied to requests
|
|
577
|
+
*
|
|
578
|
+
* @returns The current CSRF token or null if not set
|
|
606
579
|
*/
|
|
607
580
|
getCsrfToken() {
|
|
608
581
|
return this.csrfToken;
|
|
609
582
|
}
|
|
610
583
|
/**
|
|
611
|
-
* Set the CSRF header name
|
|
584
|
+
* Set the CSRF header name used when sending the token
|
|
585
|
+
*
|
|
586
|
+
* @param name - The header name to use
|
|
587
|
+
* @returns The config instance for chaining
|
|
588
|
+
*
|
|
589
|
+
* @example
|
|
590
|
+
* Config.getInstance().setCsrfHeaderName('X-My-CSRF-Token');
|
|
612
591
|
*/
|
|
613
592
|
setCsrfHeaderName(name) {
|
|
614
593
|
this.csrfHeaderName = name;
|
|
615
594
|
return this;
|
|
616
595
|
}
|
|
617
596
|
/**
|
|
618
|
-
* Get the CSRF header name
|
|
597
|
+
* Get the configured CSRF header name
|
|
598
|
+
*
|
|
599
|
+
* @returns The current CSRF header name
|
|
619
600
|
*/
|
|
620
601
|
getCsrfHeaderName() {
|
|
621
602
|
return this.csrfHeaderName;
|
|
622
603
|
}
|
|
623
604
|
/**
|
|
624
|
-
* Set the XSRF cookie name to look for
|
|
605
|
+
* Set the XSRF cookie name to look for when extracting tokens from cookies
|
|
606
|
+
*
|
|
607
|
+
* @param name - The cookie name to look for
|
|
608
|
+
* @returns The config instance for chaining
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* Config.getInstance().setXsrfCookieName('MY-XSRF-COOKIE');
|
|
625
612
|
*/
|
|
626
613
|
setXsrfCookieName(name) {
|
|
627
614
|
this.xsrfCookieName = name;
|
|
628
615
|
return this;
|
|
629
616
|
}
|
|
630
617
|
/**
|
|
631
|
-
* Get the XSRF cookie name
|
|
618
|
+
* Get the configured XSRF cookie name
|
|
619
|
+
*
|
|
620
|
+
* @returns The current XSRF cookie name
|
|
632
621
|
*/
|
|
633
622
|
getXsrfCookieName() {
|
|
634
623
|
return this.xsrfCookieName;
|
|
635
624
|
}
|
|
636
625
|
/**
|
|
637
|
-
* Set the XSRF header name
|
|
626
|
+
* Set the XSRF header name for sending tokens extracted from cookies
|
|
627
|
+
*
|
|
628
|
+
* @param name - The header name to use
|
|
629
|
+
* @returns The config instance for chaining
|
|
638
630
|
*/
|
|
639
631
|
setXsrfHeaderName(name) {
|
|
640
632
|
this.xsrfHeaderName = name;
|
|
641
633
|
return this;
|
|
642
634
|
}
|
|
643
635
|
/**
|
|
644
|
-
* Get the XSRF header name
|
|
636
|
+
* Get the configured XSRF header name
|
|
637
|
+
*
|
|
638
|
+
* @returns The current XSRF header name
|
|
645
639
|
*/
|
|
646
640
|
getXsrfHeaderName() {
|
|
647
641
|
return this.xsrfHeaderName;
|
|
648
642
|
}
|
|
649
643
|
/**
|
|
650
|
-
* Enable or disable automatic XSRF
|
|
644
|
+
* Enable or disable automatic extraction of XSRF tokens from cookies
|
|
645
|
+
* When enabled, the library will look for XSRF tokens in cookies and
|
|
646
|
+
* automatically add them to request headers.
|
|
647
|
+
*
|
|
648
|
+
* @param enable - Whether to enable this feature
|
|
649
|
+
* @returns The config instance for chaining
|
|
650
|
+
*
|
|
651
|
+
* @example
|
|
652
|
+
* Config.getInstance().setEnableAutoXsrf(false); // Disable XSRF extraction
|
|
651
653
|
*/
|
|
652
654
|
setEnableAutoXsrf(enable) {
|
|
653
655
|
this.enableAutoXsrf = enable;
|
|
654
656
|
return this;
|
|
655
657
|
}
|
|
656
658
|
/**
|
|
657
|
-
* Check if automatic XSRF is enabled
|
|
659
|
+
* Check if automatic XSRF token extraction is enabled
|
|
660
|
+
*
|
|
661
|
+
* @returns True if automatic XSRF is enabled
|
|
658
662
|
*/
|
|
659
663
|
isAutoXsrfEnabled() {
|
|
660
664
|
return this.enableAutoXsrf;
|
|
661
665
|
}
|
|
662
666
|
/**
|
|
663
667
|
* Enable or disable automatic addition of anti-CSRF headers
|
|
668
|
+
* When enabled, X-Requested-With: XMLHttpRequest will be added to all requests.
|
|
669
|
+
*
|
|
670
|
+
* @param enable - Whether to enable this feature
|
|
671
|
+
* @returns The config instance for chaining
|
|
664
672
|
*/
|
|
665
673
|
setEnableAntiCsrf(enable) {
|
|
666
674
|
this.enableAntiCsrf = enable;
|
|
667
675
|
return this;
|
|
668
676
|
}
|
|
669
677
|
/**
|
|
670
|
-
* Check if anti-CSRF is enabled
|
|
678
|
+
* Check if anti-CSRF protection is enabled
|
|
679
|
+
*
|
|
680
|
+
* @returns True if anti-CSRF protection is enabled
|
|
671
681
|
*/
|
|
672
682
|
isAntiCsrfEnabled() {
|
|
673
683
|
return this.enableAntiCsrf;
|
|
674
684
|
}
|
|
675
685
|
/**
|
|
676
|
-
*
|
|
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
|
+
* });
|
|
677
697
|
*/
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.
|
|
681
|
-
|
|
682
|
-
this.xsrfCookieName = "XSRF-TOKEN";
|
|
683
|
-
this.xsrfHeaderName = "X-XSRF-TOKEN";
|
|
684
|
-
this.enableAutoXsrf = true;
|
|
685
|
-
return this;
|
|
698
|
+
addRequestInterceptor(interceptor) {
|
|
699
|
+
const id = this.nextInterceptorId++;
|
|
700
|
+
this.requestInterceptors.push({ id, interceptor });
|
|
701
|
+
return id;
|
|
686
702
|
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
class CookieUtils {
|
|
690
703
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
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
|
+
* });
|
|
694
715
|
*/
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (typeof valueOrOptions === "string") {
|
|
700
|
-
value = valueOrOptions;
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
// Extract value from options object without validation
|
|
704
|
-
value = valueOrOptions.value;
|
|
705
|
-
}
|
|
706
|
-
// Add the cookie to the request
|
|
707
|
-
cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
|
|
708
|
-
});
|
|
709
|
-
return cookiePairs.join("; ");
|
|
716
|
+
addResponseInterceptor(interceptor) {
|
|
717
|
+
const id = this.nextInterceptorId++;
|
|
718
|
+
this.responseInterceptors.push({ id, interceptor });
|
|
719
|
+
return id;
|
|
710
720
|
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Utility class for CSRF token management
|
|
715
|
-
*/
|
|
716
|
-
class CsrfUtils {
|
|
717
721
|
/**
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
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
|
+
* });
|
|
721
733
|
*/
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const meta = document.querySelector(`meta[name="${metaName}"]`);
|
|
727
|
-
return (meta === null || meta === void 0 ? void 0 : meta.getAttribute("content")) || null;
|
|
734
|
+
addErrorInterceptor(interceptor) {
|
|
735
|
+
const id = this.nextInterceptorId++;
|
|
736
|
+
this.errorInterceptors.push({ id, interceptor });
|
|
737
|
+
return id;
|
|
728
738
|
}
|
|
729
739
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
* @
|
|
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);
|
|
733
746
|
*/
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
* @
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Get all global error interceptors (in registration order)
|
|
799
|
+
* @internal
|
|
800
|
+
*/
|
|
801
|
+
getErrorInterceptors() {
|
|
802
|
+
return this.errorInterceptors.map(item => item.interceptor);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Reset all configuration options to their default values
|
|
806
|
+
*
|
|
807
|
+
* @returns The config instance for chaining
|
|
808
|
+
*
|
|
809
|
+
* @example
|
|
810
|
+
* Config.getInstance().reset();
|
|
811
|
+
*/
|
|
812
|
+
reset() {
|
|
813
|
+
this.csrfToken = null;
|
|
814
|
+
this.csrfHeaderName = "X-CSRF-Token";
|
|
815
|
+
this.enableAntiCsrf = true;
|
|
816
|
+
this.xsrfCookieName = "XSRF-TOKEN";
|
|
817
|
+
this.xsrfHeaderName = "X-XSRF-TOKEN";
|
|
818
|
+
this.enableAutoXsrf = true;
|
|
819
|
+
this.clearInterceptors();
|
|
820
|
+
return this;
|
|
777
821
|
}
|
|
778
822
|
}
|
|
779
823
|
|
|
780
|
-
|
|
824
|
+
/**
|
|
825
|
+
* Base class with common functionality for all request types
|
|
826
|
+
* Provides the core request building and execution capabilities.
|
|
827
|
+
*/
|
|
781
828
|
class BaseRequest {
|
|
782
|
-
constructor() {
|
|
829
|
+
constructor(url) {
|
|
783
830
|
this.requestOptions = {
|
|
784
831
|
headers: {},
|
|
785
832
|
};
|
|
786
833
|
this.queryParams = new URLSearchParams();
|
|
787
834
|
this.autoApplyCsrfProtection = true;
|
|
788
|
-
|
|
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
|
+
}
|
|
789
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* Add multiple HTTP headers to the request
|
|
879
|
+
*
|
|
880
|
+
* @param headers - Key-value pairs of header names and values
|
|
881
|
+
* @returns The request instance for chaining
|
|
882
|
+
*
|
|
883
|
+
* @example
|
|
884
|
+
* request.withHeaders({
|
|
885
|
+
* 'Accept': 'application/json',
|
|
886
|
+
* 'X-Custom-Header': 'value'
|
|
887
|
+
* });
|
|
888
|
+
*/
|
|
790
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
|
+
});
|
|
791
897
|
this.requestOptions.headers = {
|
|
792
|
-
...this.
|
|
793
|
-
...
|
|
898
|
+
...this.getHeadersRecord(),
|
|
899
|
+
...filteredHeaders,
|
|
794
900
|
};
|
|
795
901
|
return this;
|
|
796
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Add a single HTTP header to the request
|
|
905
|
+
*
|
|
906
|
+
* @param key - The header name
|
|
907
|
+
* @param value - The header value
|
|
908
|
+
* @returns The request instance for chaining
|
|
909
|
+
*
|
|
910
|
+
* @example
|
|
911
|
+
* request.withHeader('Accept', 'application/json');
|
|
912
|
+
*/
|
|
913
|
+
withHeader(key, value) {
|
|
914
|
+
// Ignore null and undefined values
|
|
915
|
+
if (value !== null && value !== undefined) {
|
|
916
|
+
return this.withHeaders({ [key]: value });
|
|
917
|
+
}
|
|
918
|
+
return this;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Set a timeout for the request
|
|
922
|
+
* If the request takes longer than the specified timeout, it will be aborted.
|
|
923
|
+
*
|
|
924
|
+
* @param timeout - The timeout in milliseconds
|
|
925
|
+
* @returns The request instance for chaining
|
|
926
|
+
* @throws Error if timeout is not a positive number
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* request.withTimeout(5000); // 5 seconds timeout
|
|
930
|
+
*/
|
|
797
931
|
withTimeout(timeout) {
|
|
798
932
|
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
799
933
|
throw new Error("Timeout must be a positive number");
|
|
800
934
|
this.requestOptions.timeout = timeout;
|
|
801
935
|
return this;
|
|
802
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Configure automatic retry behavior for failed requests
|
|
939
|
+
*
|
|
940
|
+
* @param retries - Number of retry attempts before failing
|
|
941
|
+
* @returns The request instance for chaining
|
|
942
|
+
* @throws Error if retries is not a non-negative integer
|
|
943
|
+
*
|
|
944
|
+
* @example
|
|
945
|
+
* request.withRetries(3); // Retry up to 3 times
|
|
946
|
+
*/
|
|
803
947
|
withRetries(retries) {
|
|
804
948
|
if (!Number.isInteger(retries) || retries < 0)
|
|
805
949
|
throw new Error("Retry count must be a non-negative integer");
|
|
806
950
|
this.requestOptions.retries = retries;
|
|
807
951
|
return this;
|
|
808
952
|
}
|
|
953
|
+
/**
|
|
954
|
+
* Set a callback to be invoked before each retry attempt
|
|
955
|
+
* Useful for implementing backoff strategies or logging retry attempts.
|
|
956
|
+
*
|
|
957
|
+
* @param callback - Function to call before retrying
|
|
958
|
+
* @returns The request instance for chaining
|
|
959
|
+
*
|
|
960
|
+
* @example
|
|
961
|
+
* request.onRetry(({ attempt, error }) => {
|
|
962
|
+
* console.log(`Retry attempt ${attempt} after error: ${error.message}`);
|
|
963
|
+
* return new Promise(resolve => setTimeout(resolve, attempt * 1000));
|
|
964
|
+
* });
|
|
965
|
+
*/
|
|
809
966
|
onRetry(callback) {
|
|
810
967
|
this.requestOptions.onRetry = callback;
|
|
811
968
|
return this;
|
|
812
969
|
}
|
|
813
970
|
/**
|
|
814
|
-
* Sets
|
|
815
|
-
* @
|
|
816
|
-
*
|
|
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()
|
|
817
976
|
*/
|
|
818
|
-
withCredentials(
|
|
819
|
-
this.
|
|
820
|
-
|
|
977
|
+
get withCredentials() {
|
|
978
|
+
return this.createFluentSetter("credentials", {
|
|
979
|
+
INCLUDE: CredentialsPolicy.INCLUDE,
|
|
980
|
+
OMIT: CredentialsPolicy.OMIT,
|
|
981
|
+
SAME_ORIGIN: CredentialsPolicy.SAME_ORIGIN,
|
|
982
|
+
});
|
|
821
983
|
}
|
|
822
984
|
/**
|
|
823
985
|
* Allows providing an external AbortController to cancel the request
|
|
@@ -838,22 +1000,37 @@ class BaseRequest {
|
|
|
838
1000
|
return this;
|
|
839
1001
|
}
|
|
840
1002
|
/**
|
|
841
|
-
* Sets
|
|
842
|
-
* @
|
|
843
|
-
*
|
|
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()
|
|
844
1008
|
*/
|
|
845
|
-
withReferrerPolicy(
|
|
846
|
-
this.
|
|
847
|
-
|
|
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
|
+
});
|
|
848
1020
|
}
|
|
849
1021
|
/**
|
|
850
|
-
* Sets
|
|
851
|
-
* @
|
|
852
|
-
*
|
|
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()
|
|
853
1027
|
*/
|
|
854
|
-
withRedirect(
|
|
855
|
-
this.
|
|
856
|
-
|
|
1028
|
+
get withRedirect() {
|
|
1029
|
+
return this.createFluentSetter("redirect", {
|
|
1030
|
+
FOLLOW: RedirectMode.FOLLOW,
|
|
1031
|
+
ERROR: RedirectMode.ERROR,
|
|
1032
|
+
MANUAL: RedirectMode.MANUAL,
|
|
1033
|
+
});
|
|
857
1034
|
}
|
|
858
1035
|
/**
|
|
859
1036
|
* Sets the keepalive flag for the request
|
|
@@ -865,13 +1042,18 @@ class BaseRequest {
|
|
|
865
1042
|
return this;
|
|
866
1043
|
}
|
|
867
1044
|
/**
|
|
868
|
-
* Sets
|
|
869
|
-
* @
|
|
870
|
-
*
|
|
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()
|
|
871
1050
|
*/
|
|
872
|
-
withPriority(
|
|
873
|
-
this.
|
|
874
|
-
|
|
1051
|
+
get withPriority() {
|
|
1052
|
+
return this.createFluentSetter("priority", {
|
|
1053
|
+
HIGH: RequestPriority.HIGH,
|
|
1054
|
+
LOW: RequestPriority.LOW,
|
|
1055
|
+
AUTO: RequestPriority.AUTO,
|
|
1056
|
+
});
|
|
875
1057
|
}
|
|
876
1058
|
/**
|
|
877
1059
|
* Adds query parameters to the request URL
|
|
@@ -909,13 +1091,19 @@ class BaseRequest {
|
|
|
909
1091
|
return this;
|
|
910
1092
|
}
|
|
911
1093
|
/**
|
|
912
|
-
* Sets
|
|
913
|
-
* @
|
|
914
|
-
*
|
|
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()
|
|
915
1099
|
*/
|
|
916
|
-
withMode(
|
|
917
|
-
this.
|
|
918
|
-
|
|
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
|
+
});
|
|
919
1107
|
}
|
|
920
1108
|
/**
|
|
921
1109
|
* Shorthand for setting the Content-Type header
|
|
@@ -965,7 +1153,7 @@ class BaseRequest {
|
|
|
965
1153
|
if (typeof Buffer !== "undefined")
|
|
966
1154
|
return Buffer.from(str).toString("base64");
|
|
967
1155
|
// Fallback (should never happen in modern environments)
|
|
968
|
-
throw new Error("Base64 encoding is not supported
|
|
1156
|
+
throw new Error("Base64 encoding is not supported");
|
|
969
1157
|
}
|
|
970
1158
|
/**
|
|
971
1159
|
* Sets the bearer token for authentication
|
|
@@ -975,13 +1163,23 @@ class BaseRequest {
|
|
|
975
1163
|
withBearerToken(token) {
|
|
976
1164
|
return this.withAuthorization(`Bearer ${token}`);
|
|
977
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
|
+
}
|
|
978
1176
|
/**
|
|
979
1177
|
* Helper function to check for header presence in a case-insensitive way
|
|
980
|
-
* @param headers Headers object
|
|
981
1178
|
* @param headerName Header name to check
|
|
982
1179
|
* @returns Boolean indicating if the header exists (case-insensitive)
|
|
983
1180
|
*/
|
|
984
|
-
hasHeader(
|
|
1181
|
+
hasHeader(headerName) {
|
|
1182
|
+
const headers = this.getHeadersRecord();
|
|
985
1183
|
return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
|
|
986
1184
|
}
|
|
987
1185
|
/**
|
|
@@ -990,12 +1188,11 @@ class BaseRequest {
|
|
|
990
1188
|
* @returns The instance for chaining
|
|
991
1189
|
*/
|
|
992
1190
|
withCookies(cookies) {
|
|
993
|
-
const cookieEntries = Object.entries(cookies);
|
|
1191
|
+
const cookieEntries = Object.entries(cookies || {});
|
|
994
1192
|
if (cookieEntries.length === 0) {
|
|
995
1193
|
return this;
|
|
996
1194
|
}
|
|
997
|
-
|
|
998
|
-
const currentHeaders = this.requestOptions.headers || {};
|
|
1195
|
+
const currentHeaders = this.getHeadersRecord();
|
|
999
1196
|
// Get all existing cookie headers with different cases
|
|
1000
1197
|
const cookieValues = [];
|
|
1001
1198
|
const cookieHeaderKeys = [];
|
|
@@ -1064,110 +1261,178 @@ class BaseRequest {
|
|
|
1064
1261
|
});
|
|
1065
1262
|
}
|
|
1066
1263
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1264
|
+
* Add a request interceptor for this specific request
|
|
1265
|
+
* Request interceptors can modify the request configuration or return an early response
|
|
1266
|
+
*
|
|
1267
|
+
* @param interceptor - The request interceptor function
|
|
1268
|
+
* @returns The instance for chaining
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* request.withRequestInterceptor((config) => {
|
|
1272
|
+
* config.headers['X-Custom'] = 'value';
|
|
1273
|
+
* return config;
|
|
1274
|
+
* });
|
|
1275
|
+
*/
|
|
1276
|
+
withRequestInterceptor(interceptor) {
|
|
1277
|
+
this.requestInterceptors.push(interceptor);
|
|
1278
|
+
return this;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Add a response interceptor for this specific request
|
|
1282
|
+
* Response interceptors can transform the response
|
|
1283
|
+
*
|
|
1284
|
+
* @param interceptor - The response interceptor function
|
|
1285
|
+
* @returns The instance for chaining
|
|
1286
|
+
*
|
|
1287
|
+
* @example
|
|
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
|
|
1069
1302
|
* @returns The instance for chaining
|
|
1303
|
+
*
|
|
1304
|
+
* @example
|
|
1305
|
+
* request.withErrorInterceptor((error) => {
|
|
1306
|
+
* console.error('Request failed:', error);
|
|
1307
|
+
* throw error;
|
|
1308
|
+
* });
|
|
1070
1309
|
*/
|
|
1071
|
-
|
|
1072
|
-
this.
|
|
1073
|
-
this.cacheManager = new CacheManager(options);
|
|
1310
|
+
withErrorInterceptor(interceptor) {
|
|
1311
|
+
this.errorInterceptors.push(interceptor);
|
|
1074
1312
|
return this;
|
|
1075
1313
|
}
|
|
1076
1314
|
/**
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
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[]>();
|
|
1079
1340
|
*/
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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);
|
|
1394
|
+
*/
|
|
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) {
|
|
1088
1418
|
this.withHeaders({
|
|
1089
|
-
|
|
1419
|
+
[csrfHeaderName]: globalToken,
|
|
1090
1420
|
});
|
|
1091
1421
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
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);
|
|
1099
1429
|
if (!hasLocalToken) {
|
|
1100
1430
|
this.withHeaders({
|
|
1101
|
-
[
|
|
1431
|
+
[xsrfHeaderName]: xsrfToken,
|
|
1102
1432
|
});
|
|
1103
1433
|
}
|
|
1104
1434
|
}
|
|
1105
|
-
// check for XSRF token in cookies and send it as a header
|
|
1106
|
-
if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
|
|
1107
|
-
const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
|
|
1108
|
-
if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
|
|
1109
|
-
const headers = this.requestOptions.headers;
|
|
1110
|
-
const xsrfHeaderName = config.getXsrfHeaderName();
|
|
1111
|
-
const hasLocalToken = this.hasHeader(headers, "X-XSRF-TOKEN") || this.hasHeader(headers, xsrfHeaderName);
|
|
1112
|
-
if (!hasLocalToken) {
|
|
1113
|
-
this.withHeaders({
|
|
1114
|
-
[xsrfHeaderName]: xsrfToken,
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
// Cast the request options to RequestInit to ensure compatibility
|
|
1121
|
-
const fetchOptions = {
|
|
1122
|
-
...this.requestOptions,
|
|
1123
|
-
method: this.method,
|
|
1124
|
-
};
|
|
1125
|
-
// Create the base promise - handle caching if enabled
|
|
1126
|
-
let basePromise;
|
|
1127
|
-
if (this.cacheEnabled && this.cacheManager) {
|
|
1128
|
-
const cacheKey = this.cacheManager.generateKey(url, this.method, this.requestOptions.headers, this.requestOptions.body);
|
|
1129
|
-
basePromise = this.executeWithCache(url, fetchOptions, cacheKey);
|
|
1130
|
-
}
|
|
1131
|
-
else {
|
|
1132
|
-
basePromise = !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
|
|
1133
1435
|
}
|
|
1134
|
-
const responsePromise = basePromise;
|
|
1135
|
-
// Attach convenience methods that begin processing immediately
|
|
1136
|
-
responsePromise.getJson = async () => {
|
|
1137
|
-
return basePromise.then(response => response.getJson());
|
|
1138
|
-
};
|
|
1139
|
-
responsePromise.getText = async () => {
|
|
1140
|
-
return basePromise.then(response => response.getText());
|
|
1141
|
-
};
|
|
1142
|
-
responsePromise.getBlob = async () => {
|
|
1143
|
-
return basePromise.then(response => response.getBlob());
|
|
1144
|
-
};
|
|
1145
|
-
responsePromise.getArrayBuffer = async () => {
|
|
1146
|
-
return basePromise.then(response => response.getArrayBuffer());
|
|
1147
|
-
};
|
|
1148
|
-
responsePromise.getBody = async () => {
|
|
1149
|
-
return basePromise.then(response => response.getBody());
|
|
1150
|
-
};
|
|
1151
|
-
responsePromise.getData = async (selector) => {
|
|
1152
|
-
try {
|
|
1153
|
-
const data = await basePromise.then(response => response.getJson());
|
|
1154
|
-
// If no selector is provided, return the raw JSON data
|
|
1155
|
-
if (!selector)
|
|
1156
|
-
return data;
|
|
1157
|
-
// Apply the selector if provided
|
|
1158
|
-
return selector(data);
|
|
1159
|
-
}
|
|
1160
|
-
catch (error) {
|
|
1161
|
-
if (error instanceof Error) {
|
|
1162
|
-
// If selector fails, enhance the error with the original data
|
|
1163
|
-
const enhancedError = new Error(`Data selector failed: ${error.message}. Original data: ${JSON.stringify(await basePromise.then(response => response.getJson()), null, 2)}`);
|
|
1164
|
-
enhancedError.stack = error.stack;
|
|
1165
|
-
throw enhancedError;
|
|
1166
|
-
}
|
|
1167
|
-
throw error;
|
|
1168
|
-
}
|
|
1169
|
-
};
|
|
1170
|
-
return responsePromise;
|
|
1171
1436
|
}
|
|
1172
1437
|
/**
|
|
1173
1438
|
* Formats the URL with any query parameters
|
|
@@ -1199,136 +1464,295 @@ class BaseRequest {
|
|
|
1199
1464
|
* @param url The formatted URL to send the request to
|
|
1200
1465
|
* @param fetchOptions The fetch options to use
|
|
1201
1466
|
* @returns A wrapped response object
|
|
1467
|
+
* @throws RequestError if the request fails after all retries
|
|
1202
1468
|
*/
|
|
1203
1469
|
async executeWithRetries(url, fetchOptions) {
|
|
1204
|
-
let attempt = 0;
|
|
1205
1470
|
const maxRetries = this.requestOptions.retries || 0;
|
|
1206
|
-
|
|
1471
|
+
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
1472
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1207
1473
|
try {
|
|
1208
1474
|
return await this.executeRequest(url, fetchOptions);
|
|
1209
1475
|
}
|
|
1210
1476
|
catch (error) {
|
|
1211
|
-
const requestError = error instanceof RequestError ? error : RequestError.networkError(url,
|
|
1212
|
-
|
|
1213
|
-
if (attempt >= maxRetries || requestError.timeoutError)
|
|
1477
|
+
const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, error instanceof Error ? error : new Error(String(error)));
|
|
1478
|
+
if (attempt >= maxRetries)
|
|
1214
1479
|
throw requestError;
|
|
1215
|
-
attempt++;
|
|
1216
1480
|
if (this.requestOptions.onRetry) {
|
|
1217
|
-
await this.requestOptions.onRetry({ attempt, error: requestError });
|
|
1481
|
+
await this.requestOptions.onRetry({ attempt: attempt + 1, error: requestError });
|
|
1218
1482
|
}
|
|
1219
1483
|
}
|
|
1220
|
-
}
|
|
1221
|
-
//
|
|
1222
|
-
throw new RequestError(`Max retries reached
|
|
1484
|
+
}
|
|
1485
|
+
// This should never happen but is needed for type safety
|
|
1486
|
+
throw new RequestError(`Max retries reached`, url, method);
|
|
1223
1487
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
let
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
// If
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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;
|
|
1248
1565
|
}
|
|
1249
1566
|
else {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
+
}
|
|
1257
1582
|
}
|
|
1258
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
|
+
}
|
|
1259
1613
|
else {
|
|
1614
|
+
timeoutController = new AbortController();
|
|
1260
1615
|
timeoutId = setTimeout(() => {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
},
|
|
1616
|
+
isTimeout = true;
|
|
1617
|
+
timeoutController.abort();
|
|
1618
|
+
}, timeoutMs);
|
|
1619
|
+
return timeoutController.signal;
|
|
1264
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;
|
|
1265
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);
|
|
1266
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
|
|
1267
1706
|
let response;
|
|
1268
1707
|
try {
|
|
1269
1708
|
response = await fetch(url, fetchOptions);
|
|
1270
1709
|
}
|
|
1271
1710
|
catch (error) {
|
|
1272
|
-
// Check if
|
|
1273
|
-
if (error instanceof DOMException && error.name === "AbortError"
|
|
1274
|
-
|
|
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);
|
|
1275
1726
|
}
|
|
1276
|
-
|
|
1277
|
-
throw RequestError.networkError(url, fetchOptions.method, error);
|
|
1727
|
+
throw RequestError.networkError(url, method, errorObj);
|
|
1278
1728
|
}
|
|
1279
1729
|
if (!response.ok) {
|
|
1280
|
-
throw RequestError.fromResponse(response, url,
|
|
1281
|
-
}
|
|
1282
|
-
// Return a wrapped response instead of parsing it
|
|
1283
|
-
return new ResponseWrapper(response);
|
|
1284
|
-
}
|
|
1285
|
-
finally {
|
|
1286
|
-
if (timeoutId) {
|
|
1287
|
-
clearTimeout(timeoutId);
|
|
1730
|
+
throw RequestError.fromResponse(response, url, method);
|
|
1288
1731
|
}
|
|
1732
|
+
const wrappedResponse = new ResponseWrapper(response, url, method);
|
|
1733
|
+
return await this.runResponseInterceptors(wrappedResponse);
|
|
1289
1734
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
* @param cacheKey The cache key for this request
|
|
1296
|
-
*/
|
|
1297
|
-
async executeWithCache(url, fetchOptions, cacheKey) {
|
|
1298
|
-
// Try to get from cache first
|
|
1299
|
-
if (this.cacheManager) {
|
|
1300
|
-
const cachedEntry = await this.cacheManager.get(cacheKey);
|
|
1301
|
-
if (cachedEntry) {
|
|
1302
|
-
// Create a synthetic Response object from cached data
|
|
1303
|
-
const cachedResponse = new Response(JSON.stringify(cachedEntry.value), {
|
|
1304
|
-
status: 200,
|
|
1305
|
-
headers: new Headers(cachedEntry.headers || {}),
|
|
1306
|
-
});
|
|
1307
|
-
return new ResponseWrapper(cachedResponse);
|
|
1735
|
+
catch (error) {
|
|
1736
|
+
// Convert to RequestError if needed
|
|
1737
|
+
let requestError;
|
|
1738
|
+
if (error instanceof RequestError) {
|
|
1739
|
+
requestError = error;
|
|
1308
1740
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
// Cache the successful response if caching is enabled
|
|
1313
|
-
// Only cache successful responses
|
|
1314
|
-
if (response.ok && this.cacheManager) {
|
|
1315
|
-
// Clone the response so we don't consume it
|
|
1316
|
-
try {
|
|
1317
|
-
const responseData = await response.getJson();
|
|
1318
|
-
// Extract headers to store in cache
|
|
1319
|
-
const headers = {};
|
|
1320
|
-
response.headers.forEach((value, key) => {
|
|
1321
|
-
headers[key] = value;
|
|
1322
|
-
});
|
|
1323
|
-
// Store in cache
|
|
1324
|
-
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);
|
|
1325
1744
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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;
|
|
1329
1750
|
}
|
|
1751
|
+
throw interceptorResult;
|
|
1752
|
+
}
|
|
1753
|
+
finally {
|
|
1754
|
+
abortSignal.cleanup();
|
|
1330
1755
|
}
|
|
1331
|
-
return response;
|
|
1332
1756
|
}
|
|
1333
1757
|
}
|
|
1334
1758
|
|
|
@@ -1336,6 +1760,9 @@ class BaseRequest {
|
|
|
1336
1760
|
* Base class for requests that can have a body (POST, PUT, PATCH)
|
|
1337
1761
|
*/
|
|
1338
1762
|
class BodyRequest extends BaseRequest {
|
|
1763
|
+
constructor(url) {
|
|
1764
|
+
super(url);
|
|
1765
|
+
}
|
|
1339
1766
|
/**
|
|
1340
1767
|
* Sets the body of the request
|
|
1341
1768
|
* @param body - The request body
|
|
@@ -1357,7 +1784,7 @@ class BodyRequest extends BaseRequest {
|
|
|
1357
1784
|
JSON.stringify(body);
|
|
1358
1785
|
}
|
|
1359
1786
|
catch (error) {
|
|
1360
|
-
throw new
|
|
1787
|
+
throw new RequestError(`Stringify failed: ${String(error)}`, this.url, this.method);
|
|
1361
1788
|
}
|
|
1362
1789
|
}
|
|
1363
1790
|
else {
|
|
@@ -1365,18 +1792,68 @@ class BodyRequest extends BaseRequest {
|
|
|
1365
1792
|
}
|
|
1366
1793
|
return this;
|
|
1367
1794
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
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() {
|
|
1370
1840
|
const headers = this.requestOptions.headers;
|
|
1371
|
-
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()) {
|
|
1372
1849
|
this.withContentType(contentType);
|
|
1373
1850
|
}
|
|
1374
1851
|
}
|
|
1375
1852
|
/**
|
|
1376
|
-
*
|
|
1853
|
+
* Execute the request and return the ResponseWrapper
|
|
1377
1854
|
* Overrides the base implementation to add body handling
|
|
1378
1855
|
*/
|
|
1379
|
-
|
|
1856
|
+
async getResponse() {
|
|
1380
1857
|
if (this.body !== undefined) {
|
|
1381
1858
|
// Remove previous body if it exists
|
|
1382
1859
|
if (this.requestOptions.body)
|
|
@@ -1389,100 +1866,222 @@ class BodyRequest extends BaseRequest {
|
|
|
1389
1866
|
this.requestOptions.body = this.body;
|
|
1390
1867
|
}
|
|
1391
1868
|
}
|
|
1392
|
-
return super.
|
|
1869
|
+
return super.getResponse();
|
|
1393
1870
|
}
|
|
1394
1871
|
}
|
|
1395
1872
|
|
|
1396
|
-
|
|
1873
|
+
/**
|
|
1874
|
+
* HTTP GET request implementation
|
|
1875
|
+
* Used for retrieving data from an API endpoint.
|
|
1876
|
+
*
|
|
1877
|
+
* @example
|
|
1878
|
+
* const request = new GetRequest('/api/users')
|
|
1879
|
+
* .withQueryParams({ id: '123' });
|
|
1880
|
+
* const data = await request.getData();
|
|
1881
|
+
*/
|
|
1397
1882
|
class GetRequest extends BaseRequest {
|
|
1398
|
-
constructor() {
|
|
1399
|
-
super(
|
|
1883
|
+
constructor(url) {
|
|
1884
|
+
super(url);
|
|
1400
1885
|
this.method = HttpMethod.GET;
|
|
1401
1886
|
}
|
|
1402
1887
|
}
|
|
1888
|
+
/**
|
|
1889
|
+
* HTTP HEAD request implementation
|
|
1890
|
+
* Similar to GET but returns only headers without a response body.
|
|
1891
|
+
*
|
|
1892
|
+
* @example
|
|
1893
|
+
* const request = new HeadRequest('/api/users/123');
|
|
1894
|
+
* const response = await request.getResponse();
|
|
1895
|
+
*/
|
|
1403
1896
|
class HeadRequest extends BaseRequest {
|
|
1404
|
-
constructor() {
|
|
1405
|
-
super(
|
|
1897
|
+
constructor(url) {
|
|
1898
|
+
super(url);
|
|
1406
1899
|
this.method = HttpMethod.HEAD;
|
|
1407
1900
|
}
|
|
1408
1901
|
}
|
|
1902
|
+
/**
|
|
1903
|
+
* HTTP OPTIONS request implementation
|
|
1904
|
+
* Used to describe the communication options for the target resource.
|
|
1905
|
+
*
|
|
1906
|
+
* @example
|
|
1907
|
+
* const request = new OptionsRequest('/api/users');
|
|
1908
|
+
* const response = await request.getResponse();
|
|
1909
|
+
*/
|
|
1409
1910
|
class OptionsRequest extends BaseRequest {
|
|
1410
|
-
constructor() {
|
|
1411
|
-
super(
|
|
1911
|
+
constructor(url) {
|
|
1912
|
+
super(url);
|
|
1412
1913
|
this.method = HttpMethod.OPTIONS;
|
|
1413
1914
|
}
|
|
1414
1915
|
}
|
|
1916
|
+
/**
|
|
1917
|
+
* HTTP DELETE request implementation
|
|
1918
|
+
* Used to delete a resource on the server.
|
|
1919
|
+
*
|
|
1920
|
+
* @example
|
|
1921
|
+
* const request = new DeleteRequest('/api/users/123');
|
|
1922
|
+
* await request.getData();
|
|
1923
|
+
*/
|
|
1415
1924
|
class DeleteRequest extends BaseRequest {
|
|
1416
|
-
constructor() {
|
|
1417
|
-
super(
|
|
1925
|
+
constructor(url) {
|
|
1926
|
+
super(url);
|
|
1418
1927
|
this.method = HttpMethod.DELETE;
|
|
1419
1928
|
}
|
|
1420
1929
|
}
|
|
1421
|
-
|
|
1930
|
+
/**
|
|
1931
|
+
* HTTP POST request implementation
|
|
1932
|
+
* Used to create a new resource or submit data for processing.
|
|
1933
|
+
*
|
|
1934
|
+
* @example
|
|
1935
|
+
* const request = new PostRequest('/api/users')
|
|
1936
|
+
* .withJson({ name: 'John', email: 'john@example.com' });
|
|
1937
|
+
* const data = await request.getData();
|
|
1938
|
+
*/
|
|
1422
1939
|
class PostRequest extends BodyRequest {
|
|
1423
|
-
constructor() {
|
|
1424
|
-
super(
|
|
1940
|
+
constructor(url) {
|
|
1941
|
+
super(url);
|
|
1425
1942
|
this.method = HttpMethod.POST;
|
|
1426
1943
|
}
|
|
1427
1944
|
}
|
|
1945
|
+
/**
|
|
1946
|
+
* HTTP PUT request implementation
|
|
1947
|
+
* Used to replace or update an existing resource.
|
|
1948
|
+
*
|
|
1949
|
+
* @example
|
|
1950
|
+
* const request = new PutRequest('/api/users/123')
|
|
1951
|
+
* .withJson({ id: '123', name: 'John', email: 'john@example.com' });
|
|
1952
|
+
* const data = await request.getData();
|
|
1953
|
+
*/
|
|
1428
1954
|
class PutRequest extends BodyRequest {
|
|
1429
|
-
constructor() {
|
|
1430
|
-
super(
|
|
1955
|
+
constructor(url) {
|
|
1956
|
+
super(url);
|
|
1431
1957
|
this.method = HttpMethod.PUT;
|
|
1432
1958
|
}
|
|
1433
1959
|
}
|
|
1960
|
+
/**
|
|
1961
|
+
* HTTP PATCH request implementation
|
|
1962
|
+
* Used to apply partial modifications to a resource.
|
|
1963
|
+
*
|
|
1964
|
+
* @example
|
|
1965
|
+
* const request = new PatchRequest('/api/users/123')
|
|
1966
|
+
* .withJson({ email: 'new.email@example.com' });
|
|
1967
|
+
* const data = await request.getData();
|
|
1968
|
+
*/
|
|
1434
1969
|
class PatchRequest extends BodyRequest {
|
|
1435
|
-
constructor() {
|
|
1436
|
-
super(
|
|
1970
|
+
constructor(url) {
|
|
1971
|
+
super(url);
|
|
1437
1972
|
this.method = HttpMethod.PATCH;
|
|
1438
1973
|
}
|
|
1439
1974
|
}
|
|
1440
1975
|
|
|
1441
1976
|
/**
|
|
1442
1977
|
* Create a GET request
|
|
1978
|
+
* Used for retrieving resources from the server.
|
|
1979
|
+
*
|
|
1980
|
+
* @param url - The URL to send the request to
|
|
1981
|
+
* @returns A new GET request instance
|
|
1982
|
+
*
|
|
1983
|
+
* @example
|
|
1984
|
+
* const request = get('/api/users')
|
|
1985
|
+
* .withQueryParams({ limit: 10 });
|
|
1986
|
+
* const users = await request.getData();
|
|
1443
1987
|
*/
|
|
1444
|
-
function get() {
|
|
1445
|
-
return new GetRequest();
|
|
1988
|
+
function get(url) {
|
|
1989
|
+
return new GetRequest(url);
|
|
1446
1990
|
}
|
|
1447
1991
|
/**
|
|
1448
1992
|
* Create a POST request
|
|
1993
|
+
* Used for creating new resources or submitting data.
|
|
1994
|
+
*
|
|
1995
|
+
* @param url - The URL to send the request to
|
|
1996
|
+
* @returns A new POST request instance
|
|
1997
|
+
*
|
|
1998
|
+
* @example
|
|
1999
|
+
* const request = post('/api/users')
|
|
2000
|
+
* .withJson({ name: 'John', email: 'john@example.com' });
|
|
2001
|
+
* const newUser = await request.getData();
|
|
1449
2002
|
*/
|
|
1450
|
-
function post() {
|
|
1451
|
-
return new PostRequest();
|
|
2003
|
+
function post(url) {
|
|
2004
|
+
return new PostRequest(url);
|
|
1452
2005
|
}
|
|
1453
2006
|
/**
|
|
1454
2007
|
* Create a PUT request
|
|
2008
|
+
* Used for replacing or updating an existing resource.
|
|
2009
|
+
*
|
|
2010
|
+
* @param url - The URL to send the request to
|
|
2011
|
+
* @returns A new PUT request instance
|
|
2012
|
+
*
|
|
2013
|
+
* @example
|
|
2014
|
+
* const request = put('/api/users/123')
|
|
2015
|
+
* .withJson({ name: 'John Updated', email: 'john@example.com' });
|
|
2016
|
+
* const updatedUser = await request.getData();
|
|
1455
2017
|
*/
|
|
1456
|
-
function put() {
|
|
1457
|
-
return new PutRequest();
|
|
2018
|
+
function put(url) {
|
|
2019
|
+
return new PutRequest(url);
|
|
1458
2020
|
}
|
|
1459
2021
|
/**
|
|
1460
2022
|
* Create a DELETE request
|
|
1461
|
-
*
|
|
2023
|
+
* Used for removing resources from the server.
|
|
2024
|
+
*
|
|
2025
|
+
* @param url - The URL to send the request to
|
|
2026
|
+
* @returns A new DELETE request instance
|
|
2027
|
+
*
|
|
2028
|
+
* @example
|
|
2029
|
+
* const request = del('/api/users/123');
|
|
2030
|
+
* await request.getData();
|
|
1462
2031
|
*/
|
|
1463
|
-
function del() {
|
|
1464
|
-
return new DeleteRequest();
|
|
2032
|
+
function del(url) {
|
|
2033
|
+
return new DeleteRequest(url);
|
|
1465
2034
|
}
|
|
1466
2035
|
/**
|
|
1467
2036
|
* Create a PATCH request
|
|
2037
|
+
* Used for applying partial modifications to a resource.
|
|
2038
|
+
*
|
|
2039
|
+
* @param url - The URL to send the request to
|
|
2040
|
+
* @returns A new PATCH request instance
|
|
2041
|
+
*
|
|
2042
|
+
* @example
|
|
2043
|
+
* const request = patch('/api/users/123')
|
|
2044
|
+
* .withJson({ status: 'active' });
|
|
2045
|
+
* const patchedUser = await request.getData();
|
|
1468
2046
|
*/
|
|
1469
|
-
function patch() {
|
|
1470
|
-
return new PatchRequest();
|
|
2047
|
+
function patch(url) {
|
|
2048
|
+
return new PatchRequest(url);
|
|
1471
2049
|
}
|
|
1472
2050
|
/**
|
|
1473
2051
|
* Create a HEAD request
|
|
2052
|
+
* Similar to GET but returns only headers without a body.
|
|
2053
|
+
*
|
|
2054
|
+
* @param url - The URL to send the request to
|
|
2055
|
+
* @returns A new HEAD request instance
|
|
2056
|
+
*
|
|
2057
|
+
* @example
|
|
2058
|
+
* const request = head('/api/users/123');
|
|
2059
|
+
* const response = await request.getResponse();
|
|
2060
|
+
* console.log(response.headers.get('Last-Modified'));
|
|
1474
2061
|
*/
|
|
1475
|
-
function head() {
|
|
1476
|
-
return new HeadRequest();
|
|
2062
|
+
function head(url) {
|
|
2063
|
+
return new HeadRequest(url);
|
|
1477
2064
|
}
|
|
1478
2065
|
/**
|
|
1479
2066
|
* Create an OPTIONS request
|
|
2067
|
+
* Used to describe the communication options for a resource.
|
|
2068
|
+
*
|
|
2069
|
+
* @param url - The URL to send the request to
|
|
2070
|
+
* @returns A new OPTIONS request instance
|
|
2071
|
+
*
|
|
2072
|
+
* @example
|
|
2073
|
+
* const request = options('/api/users');
|
|
2074
|
+
* const response = await request.getResponse();
|
|
2075
|
+
* console.log(response.headers.get('Allow'));
|
|
1480
2076
|
*/
|
|
1481
|
-
function options() {
|
|
1482
|
-
return new OptionsRequest();
|
|
2077
|
+
function options(url) {
|
|
2078
|
+
return new OptionsRequest(url);
|
|
1483
2079
|
}
|
|
1484
2080
|
|
|
1485
|
-
|
|
2081
|
+
/**
|
|
2082
|
+
* Main API object for creating HTTP requests
|
|
2083
|
+
* Provides factory methods for all HTTP methods and access to global configuration.
|
|
2084
|
+
*/
|
|
1486
2085
|
const create = {
|
|
1487
2086
|
get,
|
|
1488
2087
|
post,
|
|
@@ -1491,8 +2090,8 @@ const create = {
|
|
|
1491
2090
|
patch,
|
|
1492
2091
|
head,
|
|
1493
2092
|
options,
|
|
1494
|
-
// Add configuration access
|
|
1495
2093
|
config: Config.getInstance(),
|
|
1496
2094
|
};
|
|
1497
2095
|
|
|
1498
|
-
export {
|
|
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
|