create-request 0.0.1-beta.9 → 1.1.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 +508 -303
- package/dist/library/BaseRequest.d.ts +301 -41
- package/dist/library/BodyRequest.d.ts +39 -4
- package/dist/library/RequestError.d.ts +5 -2
- package/dist/library/ResponseWrapper.d.ts +96 -13
- package/dist/library/enums.d.ts +0 -11
- package/dist/library/index.cjs +2201 -0
- package/dist/library/index.cjs.map +1 -0
- package/dist/library/index.d.ts +12 -12
- package/dist/library/index.esm.js +1416 -728
- 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 +47 -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 -1525
- 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,90 @@ 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, graphQLOptions) {
|
|
138
180
|
this.bodyUsed = false;
|
|
139
181
|
this.response = response;
|
|
182
|
+
this.url = url;
|
|
183
|
+
this.method = method;
|
|
184
|
+
if (graphQLOptions) {
|
|
185
|
+
this.graphQLOptions = {
|
|
186
|
+
throwOnError: graphQLOptions.throwOnError,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
140
189
|
}
|
|
141
190
|
// Wrapper properties
|
|
142
191
|
get status() {
|
|
@@ -154,29 +203,109 @@ class ResponseWrapper {
|
|
|
154
203
|
get raw() {
|
|
155
204
|
return this.response;
|
|
156
205
|
}
|
|
157
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Check for GraphQL errors and throw if throwOnError is enabled
|
|
208
|
+
* @param data - The parsed JSON data
|
|
209
|
+
* @throws RequestError if GraphQL response contains errors and throwOnError is enabled
|
|
210
|
+
*/
|
|
211
|
+
checkGraphQLErrors(data) {
|
|
212
|
+
var _a;
|
|
213
|
+
if (!((_a = this.graphQLOptions) === null || _a === void 0 ? void 0 : _a.throwOnError) || typeof data !== "object" || data === null)
|
|
214
|
+
return;
|
|
215
|
+
const responseData = data;
|
|
216
|
+
if (!Array.isArray(responseData.errors) || responseData.errors.length === 0)
|
|
217
|
+
return;
|
|
218
|
+
const errors = responseData.errors;
|
|
219
|
+
const errorMessages = errors.map(x => typeof x === "string" ? x : x && typeof x === "object" && "message" in x ? String(x.message || "Unknown error") : String(x));
|
|
220
|
+
const errorMessage = errorMessages.join(", ");
|
|
221
|
+
throw new RequestError(`GraphQL request failed with errors: ${errorMessage}`, this.url || "", this.method || "", {
|
|
222
|
+
status: this.response.status,
|
|
223
|
+
response: this.response,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Parse the response body as JSON
|
|
228
|
+
* Caches the result for subsequent calls.
|
|
229
|
+
* If GraphQL options are set with throwOnError=true, will check for GraphQL errors and throw.
|
|
230
|
+
*
|
|
231
|
+
* @returns The parsed JSON data
|
|
232
|
+
* @throws {RequestError} When the request fails, JSON parsing fails, GraphQL errors occur (if throwOnError enabled), or body is already consumed
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* const data = await response.getJson();
|
|
236
|
+
* console.log(data.items);
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* // Error handling - errors are always RequestError
|
|
240
|
+
* try {
|
|
241
|
+
* const data = await response.getJson();
|
|
242
|
+
* } catch (error) {
|
|
243
|
+
* if (error instanceof RequestError) {
|
|
244
|
+
* console.log(error.status, error.url, error.method);
|
|
245
|
+
* }
|
|
246
|
+
* }
|
|
247
|
+
*/
|
|
158
248
|
async getJson() {
|
|
159
249
|
if (this.jsonCache !== undefined) {
|
|
250
|
+
this.checkGraphQLErrors(this.jsonCache);
|
|
160
251
|
return this.jsonCache;
|
|
161
252
|
}
|
|
162
253
|
if (this.bodyUsed) {
|
|
163
254
|
// If we already have text, try to parse it as JSON
|
|
164
255
|
if (this.textCache !== undefined) {
|
|
165
256
|
try {
|
|
166
|
-
|
|
167
|
-
this.jsonCache =
|
|
168
|
-
|
|
257
|
+
const parsed = JSON.parse(this.textCache);
|
|
258
|
+
this.jsonCache = parsed;
|
|
259
|
+
this.checkGraphQLErrors(parsed);
|
|
260
|
+
return parsed;
|
|
169
261
|
}
|
|
170
|
-
catch (
|
|
171
|
-
throw
|
|
262
|
+
catch (error) {
|
|
263
|
+
// Re-throw RequestErrors from checkGraphQLErrors without wrapping
|
|
264
|
+
if (error instanceof RequestError) {
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
268
|
+
throw new RequestError(`Invalid JSON: ${errorMessage}`, this.url || "", this.method || "", {
|
|
269
|
+
status: this.response.status,
|
|
270
|
+
response: this.response,
|
|
271
|
+
});
|
|
172
272
|
}
|
|
173
273
|
}
|
|
174
|
-
throw new
|
|
274
|
+
throw new RequestError("Body already consumed", this.url || "", this.method || "", {
|
|
275
|
+
status: this.response.status,
|
|
276
|
+
response: this.response,
|
|
277
|
+
});
|
|
175
278
|
}
|
|
176
279
|
this.bodyUsed = true;
|
|
177
|
-
|
|
178
|
-
|
|
280
|
+
try {
|
|
281
|
+
const parsed = await this.response.json();
|
|
282
|
+
this.jsonCache = parsed;
|
|
283
|
+
this.checkGraphQLErrors(parsed);
|
|
284
|
+
return parsed;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
// Re-throw RequestErrors from checkGraphQLErrors without wrapping
|
|
288
|
+
if (error instanceof RequestError) {
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
292
|
+
throw new RequestError(`Invalid JSON: ${errorMessage}`, this.url || "", this.method || "", {
|
|
293
|
+
status: this.response.status,
|
|
294
|
+
response: this.response,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
179
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Get the response body as text
|
|
300
|
+
* Caches the result for subsequent calls. Attempts to convert from JSON or Blob
|
|
301
|
+
* if the body has already been consumed in those formats.
|
|
302
|
+
*
|
|
303
|
+
* @returns The response text
|
|
304
|
+
* @throws {RequestError} When reading fails or the response has already been consumed in an incompatible format
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* const text = await response.getText();
|
|
308
|
+
*/
|
|
180
309
|
async getText() {
|
|
181
310
|
if (this.textCache !== undefined) {
|
|
182
311
|
return this.textCache;
|
|
@@ -189,15 +318,48 @@ class ResponseWrapper {
|
|
|
189
318
|
}
|
|
190
319
|
// If we have blob, convert to text
|
|
191
320
|
if (this.blobCache !== undefined) {
|
|
192
|
-
|
|
193
|
-
|
|
321
|
+
try {
|
|
322
|
+
this.textCache = await this.blobCache.text();
|
|
323
|
+
return this.textCache;
|
|
324
|
+
}
|
|
325
|
+
catch (e) {
|
|
326
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
327
|
+
throw new RequestError(`Cannot convert to text: ${errorMessage}`, this.url || "", this.method || "", {
|
|
328
|
+
status: this.response.status,
|
|
329
|
+
response: this.response,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
194
332
|
}
|
|
195
|
-
throw new
|
|
333
|
+
throw new RequestError("Cannot convert to text", this.url || "", this.method || "", {
|
|
334
|
+
status: this.response.status,
|
|
335
|
+
response: this.response,
|
|
336
|
+
});
|
|
196
337
|
}
|
|
197
338
|
this.bodyUsed = true;
|
|
198
|
-
|
|
199
|
-
|
|
339
|
+
try {
|
|
340
|
+
this.textCache = await this.response.text();
|
|
341
|
+
return this.textCache;
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
345
|
+
throw new RequestError(`Read failed: ${errorMessage}`, this.url || "", this.method || "", {
|
|
346
|
+
status: this.response.status,
|
|
347
|
+
response: this.response,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
200
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Get the response body as a Blob
|
|
353
|
+
* Caches the result for subsequent calls. Attempts to convert from text
|
|
354
|
+
* if the body has already been consumed as text.
|
|
355
|
+
*
|
|
356
|
+
* @returns The response as a Blob
|
|
357
|
+
* @throws {RequestError} When reading fails or the response has already been consumed in an incompatible format
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* const blob = await response.getBlob();
|
|
361
|
+
* const url = URL.createObjectURL(blob);
|
|
362
|
+
*/
|
|
201
363
|
async getBlob() {
|
|
202
364
|
if (this.blobCache !== undefined) {
|
|
203
365
|
return this.blobCache;
|
|
@@ -208,367 +370,191 @@ class ResponseWrapper {
|
|
|
208
370
|
this.blobCache = new Blob([this.textCache], { type: "text/plain" });
|
|
209
371
|
return this.blobCache;
|
|
210
372
|
}
|
|
211
|
-
throw new
|
|
373
|
+
throw new RequestError("Cannot convert to Blob", this.url || "", this.method || "", {
|
|
374
|
+
status: this.response.status,
|
|
375
|
+
response: this.response,
|
|
376
|
+
});
|
|
212
377
|
}
|
|
213
378
|
this.bodyUsed = true;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
async getArrayBuffer() {
|
|
218
|
-
if (this.arrayBufferCache !== undefined) {
|
|
219
|
-
return this.arrayBufferCache;
|
|
379
|
+
try {
|
|
380
|
+
this.blobCache = await this.response.blob();
|
|
381
|
+
return this.blobCache;
|
|
220
382
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
this.arrayBufferCache = uint8Array.buffer;
|
|
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;
|
|
234
|
-
}
|
|
235
|
-
throw new Error("Response body has already been consumed in a format that cannot be converted to ArrayBuffer");
|
|
383
|
+
catch (e) {
|
|
384
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
385
|
+
throw new RequestError(`Read failed: ${errorMessage}`, this.url || "", this.method || "", {
|
|
386
|
+
status: this.response.status,
|
|
387
|
+
response: this.response,
|
|
388
|
+
});
|
|
236
389
|
}
|
|
237
|
-
this.bodyUsed = true;
|
|
238
|
-
this.arrayBufferCache = await this.response.arrayBuffer();
|
|
239
|
-
return this.arrayBufferCache;
|
|
240
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Get the raw response body as a ReadableStream
|
|
393
|
+
* This method consumes the body and should only be called once.
|
|
394
|
+
*
|
|
395
|
+
* @returns The response body as a ReadableStream or null
|
|
396
|
+
* @throws {RequestError} When the response body has already been consumed
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* const stream = response.getBody();
|
|
400
|
+
* if (stream) {
|
|
401
|
+
* const reader = stream.getReader();
|
|
402
|
+
* // Process the stream
|
|
403
|
+
* }
|
|
404
|
+
*/
|
|
241
405
|
getBody() {
|
|
242
406
|
if (this.bodyUsed) {
|
|
243
|
-
throw new
|
|
407
|
+
throw new RequestError("Body already consumed", this.url || "", this.method || "", {
|
|
408
|
+
status: this.response.status,
|
|
409
|
+
response: this.response,
|
|
410
|
+
});
|
|
244
411
|
}
|
|
245
412
|
this.bodyUsed = true;
|
|
246
413
|
return this.response.body;
|
|
247
414
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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);
|
|
415
|
+
/**
|
|
416
|
+
* Extract specific data using a selector function
|
|
417
|
+
* If no selector is provided, returns the full JSON response.
|
|
418
|
+
*
|
|
419
|
+
* @param selector - Optional function to extract and transform data
|
|
420
|
+
* @returns A promise that resolves to the selected data
|
|
421
|
+
* @throws {RequestError} When the request fails, JSON parsing fails, or the selector throws an error
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* // Get full response
|
|
425
|
+
* const data = await response.getData();
|
|
426
|
+
*
|
|
427
|
+
* // Extract specific data
|
|
428
|
+
* const users = await response.getData(data => data.results.users);
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* // Error handling - errors are always RequestError
|
|
432
|
+
* try {
|
|
433
|
+
* const data = await response.getData();
|
|
434
|
+
* } catch (error) {
|
|
435
|
+
* if (error instanceof RequestError) {
|
|
436
|
+
* console.log(error.status, error.url, error.method);
|
|
437
|
+
* }
|
|
438
|
+
* }
|
|
439
|
+
*/
|
|
440
|
+
async getData(selector) {
|
|
441
|
+
try {
|
|
442
|
+
const data = await this.getJson();
|
|
443
|
+
// If no selector is provided, return the raw JSON data
|
|
444
|
+
if (!selector)
|
|
445
|
+
return data;
|
|
446
|
+
// Apply the selector if provided
|
|
447
|
+
return selector(data);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
// If it's already a RequestError, re-throw it
|
|
451
|
+
if (error instanceof RequestError) {
|
|
452
|
+
throw error;
|
|
312
453
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
catch (e) {
|
|
321
|
-
console.error("Failed to set cache item even after clearing cache");
|
|
322
|
-
}
|
|
454
|
+
// Enhance selector errors with context
|
|
455
|
+
if (selector) {
|
|
456
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
457
|
+
throw new RequestError(`Data selector failed: ${errorMessage}`, this.url || "", this.method || "", {
|
|
458
|
+
status: this.response.status,
|
|
459
|
+
response: this.response,
|
|
460
|
+
});
|
|
323
461
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
delete(key) {
|
|
331
|
-
if (typeof localStorage === "undefined")
|
|
332
|
-
return;
|
|
333
|
-
localStorage.removeItem(key);
|
|
334
|
-
},
|
|
335
|
-
clear() {
|
|
336
|
-
if (typeof localStorage === "undefined")
|
|
337
|
-
return;
|
|
338
|
-
localStorage.clear();
|
|
339
|
-
},
|
|
340
|
-
};
|
|
462
|
+
// If we get here and it's not a RequestError, wrap it
|
|
463
|
+
// This should rarely happen as getJson() should throw RequestError
|
|
464
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
465
|
+
throw RequestError.networkError(this.url || "", this.method || "", errorObj);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
341
468
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (typeof
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
sessionStorage.setItem(key, value);
|
|
469
|
+
|
|
470
|
+
class CookieUtils {
|
|
471
|
+
/**
|
|
472
|
+
* Formats cookies for a request
|
|
473
|
+
* @param cookies Object containing cookie name-value pairs or cookie options
|
|
474
|
+
* @returns Formatted cookie string for the Cookie header
|
|
475
|
+
*/
|
|
476
|
+
static formatRequestCookies(cookies) {
|
|
477
|
+
const cookiePairs = [];
|
|
478
|
+
Object.entries(cookies).forEach(([name, valueOrOptions]) => {
|
|
479
|
+
let value;
|
|
480
|
+
if (typeof valueOrOptions === "string") {
|
|
481
|
+
value = valueOrOptions;
|
|
357
482
|
}
|
|
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
|
-
}
|
|
483
|
+
else {
|
|
484
|
+
// Extract value from options object without validation
|
|
485
|
+
value = valueOrOptions.value;
|
|
368
486
|
}
|
|
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
|
-
};
|
|
487
|
+
// Add the cookie to the request
|
|
488
|
+
cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
|
|
489
|
+
});
|
|
490
|
+
return cookiePairs.join("; ");
|
|
426
491
|
}
|
|
427
|
-
// Default to memory storage
|
|
428
|
-
console.warn("Unsupported storage type, falling back to memory storage");
|
|
429
|
-
return createMemoryStorage();
|
|
430
492
|
}
|
|
431
493
|
|
|
432
494
|
/**
|
|
433
|
-
*
|
|
495
|
+
* Utility class for CSRF token management
|
|
434
496
|
*/
|
|
435
|
-
class
|
|
436
|
-
constructor(options = {}) {
|
|
437
|
-
this.entryCount = 0;
|
|
438
|
-
this.totalSize = 0;
|
|
439
|
-
this.options = options;
|
|
440
|
-
this.storage = createStorageProvider(options);
|
|
441
|
-
}
|
|
497
|
+
class CsrfUtils {
|
|
442
498
|
/**
|
|
443
|
-
*
|
|
499
|
+
* Extracts CSRF token from a meta tag in the document head
|
|
500
|
+
* @param metaName The name attribute of the meta tag (default: "csrf-token")
|
|
501
|
+
* @returns The CSRF token or null if not found
|
|
444
502
|
*/
|
|
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
|
-
}
|
|
503
|
+
static getTokenFromMeta(metaName = "csrf-token") {
|
|
504
|
+
if (typeof document === "undefined") {
|
|
505
|
+
return null;
|
|
482
506
|
}
|
|
483
|
-
|
|
484
|
-
return
|
|
507
|
+
const meta = document.querySelector(`meta[name="${metaName}"]`);
|
|
508
|
+
return (meta === null || meta === void 0 ? void 0 : meta.getAttribute("content")) || null;
|
|
485
509
|
}
|
|
486
510
|
/**
|
|
487
|
-
*
|
|
511
|
+
* Extracts CSRF token from a cookie
|
|
512
|
+
* @param cookieName The name of the cookie containing the CSRF token
|
|
513
|
+
* @returns The CSRF token or null if not found
|
|
488
514
|
*/
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (!value)
|
|
515
|
+
static getTokenFromCookie(cookieName = "csrf-token") {
|
|
516
|
+
if (typeof document === "undefined") {
|
|
492
517
|
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
518
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
519
|
+
const cookies = document.cookie.split(";");
|
|
520
|
+
for (const cookie of cookies) {
|
|
521
|
+
const [name, value] = cookie.trim().split("=");
|
|
522
|
+
if (name === cookieName) {
|
|
523
|
+
return decodeURIComponent(value);
|
|
524
|
+
}
|
|
506
525
|
}
|
|
526
|
+
return null;
|
|
507
527
|
}
|
|
508
528
|
/**
|
|
509
|
-
*
|
|
529
|
+
* Validates if the provided string is a potential CSRF token
|
|
530
|
+
* Checks if the token meets security requirements
|
|
531
|
+
* @param token The token to validate
|
|
532
|
+
* @returns Whether the token is valid
|
|
510
533
|
*/
|
|
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;
|
|
534
|
+
static isValidToken(token) {
|
|
535
|
+
// Basic checks for null/undefined and type
|
|
536
|
+
if (typeof token !== "string") {
|
|
537
|
+
return false;
|
|
539
538
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
539
|
+
if (token.length < 8)
|
|
540
|
+
return false;
|
|
541
|
+
// If token is longer than 10 chars, perform additional security checks
|
|
542
|
+
if (token.length > 10) {
|
|
543
|
+
// Check for valid character set (alphanumeric & common token symbols)
|
|
544
|
+
const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
|
|
545
|
+
if (!validTokenRegex.test(token)) {
|
|
546
|
+
return false;
|
|
548
547
|
}
|
|
548
|
+
// For longer tokens, check for sufficient entropy (at least 2 character types)
|
|
549
|
+
const hasUpperCase = /[A-Z]/.test(token);
|
|
550
|
+
const hasLowerCase = /[a-z]/.test(token);
|
|
551
|
+
const hasNumbers = /[0-9]/.test(token);
|
|
552
|
+
const hasSpecials = /[-_=+/.]/.test(token);
|
|
553
|
+
const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
|
|
554
|
+
return characterTypesCount >= 2;
|
|
549
555
|
}
|
|
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());
|
|
556
|
+
// For shorter tokens (8-10 chars), just return true if we reached here
|
|
557
|
+
return true;
|
|
572
558
|
}
|
|
573
559
|
}
|
|
574
560
|
|
|
@@ -584,9 +570,20 @@ class Config {
|
|
|
584
570
|
this.csrfToken = null;
|
|
585
571
|
this.enableAutoXsrf = true;
|
|
586
572
|
this.enableAntiCsrf = true; // X-Requested-With header
|
|
573
|
+
// Interceptor configuration
|
|
574
|
+
this.requestInterceptors = [];
|
|
575
|
+
this.responseInterceptors = [];
|
|
576
|
+
this.errorInterceptors = [];
|
|
577
|
+
this.nextInterceptorId = 1;
|
|
587
578
|
}
|
|
588
579
|
/**
|
|
589
|
-
* Get the singleton instance
|
|
580
|
+
* Get the singleton instance of the Config class
|
|
581
|
+
*
|
|
582
|
+
* @returns The global configuration instance
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* const config = Config.getInstance();
|
|
586
|
+
* config.setCsrfToken('token123');
|
|
590
587
|
*/
|
|
591
588
|
static getInstance() {
|
|
592
589
|
if (!Config.instance) {
|
|
@@ -596,231 +593,432 @@ class Config {
|
|
|
596
593
|
}
|
|
597
594
|
/**
|
|
598
595
|
* Set a global CSRF token to be used for all requests
|
|
596
|
+
*
|
|
597
|
+
* @param token - The CSRF token value
|
|
598
|
+
* @returns The config instance for chaining
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* Config.getInstance().setCsrfToken('myToken123');
|
|
599
602
|
*/
|
|
600
603
|
setCsrfToken(token) {
|
|
601
604
|
this.csrfToken = token;
|
|
602
605
|
return this;
|
|
603
606
|
}
|
|
604
607
|
/**
|
|
605
|
-
* Get the global CSRF token
|
|
608
|
+
* Get the global CSRF token that will be automatically applied to requests
|
|
609
|
+
*
|
|
610
|
+
* @returns The current CSRF token or null if not set
|
|
606
611
|
*/
|
|
607
612
|
getCsrfToken() {
|
|
608
613
|
return this.csrfToken;
|
|
609
614
|
}
|
|
610
615
|
/**
|
|
611
|
-
* Set the CSRF header name
|
|
616
|
+
* Set the CSRF header name used when sending the token
|
|
617
|
+
*
|
|
618
|
+
* @param name - The header name to use
|
|
619
|
+
* @returns The config instance for chaining
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* Config.getInstance().setCsrfHeaderName('X-My-CSRF-Token');
|
|
612
623
|
*/
|
|
613
624
|
setCsrfHeaderName(name) {
|
|
614
625
|
this.csrfHeaderName = name;
|
|
615
626
|
return this;
|
|
616
627
|
}
|
|
617
628
|
/**
|
|
618
|
-
* Get the CSRF header name
|
|
629
|
+
* Get the configured CSRF header name
|
|
630
|
+
*
|
|
631
|
+
* @returns The current CSRF header name
|
|
619
632
|
*/
|
|
620
633
|
getCsrfHeaderName() {
|
|
621
634
|
return this.csrfHeaderName;
|
|
622
635
|
}
|
|
623
636
|
/**
|
|
624
|
-
* Set the XSRF cookie name to look for
|
|
637
|
+
* Set the XSRF cookie name to look for when extracting tokens from cookies
|
|
638
|
+
*
|
|
639
|
+
* @param name - The cookie name to look for
|
|
640
|
+
* @returns The config instance for chaining
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* Config.getInstance().setXsrfCookieName('MY-XSRF-COOKIE');
|
|
625
644
|
*/
|
|
626
645
|
setXsrfCookieName(name) {
|
|
627
646
|
this.xsrfCookieName = name;
|
|
628
647
|
return this;
|
|
629
648
|
}
|
|
630
649
|
/**
|
|
631
|
-
* Get the XSRF cookie name
|
|
650
|
+
* Get the configured XSRF cookie name
|
|
651
|
+
*
|
|
652
|
+
* @returns The current XSRF cookie name
|
|
632
653
|
*/
|
|
633
654
|
getXsrfCookieName() {
|
|
634
655
|
return this.xsrfCookieName;
|
|
635
656
|
}
|
|
636
657
|
/**
|
|
637
|
-
* Set the XSRF header name
|
|
658
|
+
* Set the XSRF header name for sending tokens extracted from cookies
|
|
659
|
+
*
|
|
660
|
+
* @param name - The header name to use
|
|
661
|
+
* @returns The config instance for chaining
|
|
638
662
|
*/
|
|
639
663
|
setXsrfHeaderName(name) {
|
|
640
664
|
this.xsrfHeaderName = name;
|
|
641
665
|
return this;
|
|
642
666
|
}
|
|
643
667
|
/**
|
|
644
|
-
* Get the XSRF header name
|
|
668
|
+
* Get the configured XSRF header name
|
|
669
|
+
*
|
|
670
|
+
* @returns The current XSRF header name
|
|
645
671
|
*/
|
|
646
672
|
getXsrfHeaderName() {
|
|
647
673
|
return this.xsrfHeaderName;
|
|
648
674
|
}
|
|
649
675
|
/**
|
|
650
|
-
* Enable or disable automatic XSRF
|
|
676
|
+
* Enable or disable automatic extraction of XSRF tokens from cookies
|
|
677
|
+
* When enabled, the library will look for XSRF tokens in cookies and
|
|
678
|
+
* automatically add them to request headers.
|
|
679
|
+
*
|
|
680
|
+
* @param enable - Whether to enable this feature
|
|
681
|
+
* @returns The config instance for chaining
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* Config.getInstance().setEnableAutoXsrf(false); // Disable XSRF extraction
|
|
651
685
|
*/
|
|
652
686
|
setEnableAutoXsrf(enable) {
|
|
653
687
|
this.enableAutoXsrf = enable;
|
|
654
688
|
return this;
|
|
655
689
|
}
|
|
656
690
|
/**
|
|
657
|
-
* Check if automatic XSRF is enabled
|
|
691
|
+
* Check if automatic XSRF token extraction is enabled
|
|
692
|
+
*
|
|
693
|
+
* @returns True if automatic XSRF is enabled
|
|
658
694
|
*/
|
|
659
695
|
isAutoXsrfEnabled() {
|
|
660
696
|
return this.enableAutoXsrf;
|
|
661
697
|
}
|
|
662
698
|
/**
|
|
663
699
|
* Enable or disable automatic addition of anti-CSRF headers
|
|
700
|
+
* When enabled, X-Requested-With: XMLHttpRequest will be added to all requests.
|
|
701
|
+
*
|
|
702
|
+
* @param enable - Whether to enable this feature
|
|
703
|
+
* @returns The config instance for chaining
|
|
664
704
|
*/
|
|
665
705
|
setEnableAntiCsrf(enable) {
|
|
666
706
|
this.enableAntiCsrf = enable;
|
|
667
707
|
return this;
|
|
668
708
|
}
|
|
669
709
|
/**
|
|
670
|
-
* Check if anti-CSRF is enabled
|
|
710
|
+
* Check if anti-CSRF protection is enabled
|
|
711
|
+
*
|
|
712
|
+
* @returns True if anti-CSRF protection is enabled
|
|
671
713
|
*/
|
|
672
714
|
isAntiCsrfEnabled() {
|
|
673
715
|
return this.enableAntiCsrf;
|
|
674
716
|
}
|
|
675
717
|
/**
|
|
676
|
-
*
|
|
718
|
+
* Add a global request interceptor
|
|
719
|
+
* Request interceptors can modify the request configuration or return an early response
|
|
720
|
+
*
|
|
721
|
+
* @param interceptor - The request interceptor function
|
|
722
|
+
* @returns The interceptor ID for later removal
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* const id = Config.getInstance().addRequestInterceptor((config) => {
|
|
726
|
+
* config.headers['X-Custom'] = 'value';
|
|
727
|
+
* return config;
|
|
728
|
+
* });
|
|
677
729
|
*/
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.
|
|
681
|
-
|
|
682
|
-
this.xsrfCookieName = "XSRF-TOKEN";
|
|
683
|
-
this.xsrfHeaderName = "X-XSRF-TOKEN";
|
|
684
|
-
this.enableAutoXsrf = true;
|
|
685
|
-
return this;
|
|
730
|
+
addRequestInterceptor(interceptor) {
|
|
731
|
+
const id = this.nextInterceptorId++;
|
|
732
|
+
this.requestInterceptors.push({ id, interceptor });
|
|
733
|
+
return id;
|
|
686
734
|
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
class CookieUtils {
|
|
690
735
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
736
|
+
* Add a global response interceptor
|
|
737
|
+
* Response interceptors can transform the response
|
|
738
|
+
*
|
|
739
|
+
* @param interceptor - The response interceptor function
|
|
740
|
+
* @returns The interceptor ID for later removal
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* const id = Config.getInstance().addResponseInterceptor((response) => {
|
|
744
|
+
* console.log('Response received:', response.status);
|
|
745
|
+
* return response;
|
|
746
|
+
* });
|
|
694
747
|
*/
|
|
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("; ");
|
|
748
|
+
addResponseInterceptor(interceptor) {
|
|
749
|
+
const id = this.nextInterceptorId++;
|
|
750
|
+
this.responseInterceptors.push({ id, interceptor });
|
|
751
|
+
return id;
|
|
710
752
|
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Utility class for CSRF token management
|
|
715
|
-
*/
|
|
716
|
-
class CsrfUtils {
|
|
717
753
|
/**
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
754
|
+
* Add a global error interceptor
|
|
755
|
+
* Error interceptors can handle or transform errors
|
|
756
|
+
*
|
|
757
|
+
* @param interceptor - The error interceptor function
|
|
758
|
+
* @returns The interceptor ID for later removal
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* const id = Config.getInstance().addErrorInterceptor((error) => {
|
|
762
|
+
* console.error('Request failed:', error);
|
|
763
|
+
* throw error;
|
|
764
|
+
* });
|
|
721
765
|
*/
|
|
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;
|
|
766
|
+
addErrorInterceptor(interceptor) {
|
|
767
|
+
const id = this.nextInterceptorId++;
|
|
768
|
+
this.errorInterceptors.push({ id, interceptor });
|
|
769
|
+
return id;
|
|
728
770
|
}
|
|
729
771
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
* @
|
|
772
|
+
* Remove a request interceptor by its ID
|
|
773
|
+
*
|
|
774
|
+
* @param id - The interceptor ID returned from addRequestInterceptor
|
|
775
|
+
*
|
|
776
|
+
* @example
|
|
777
|
+
* Config.getInstance().removeRequestInterceptor(id);
|
|
733
778
|
*/
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
const cookies = document.cookie.split(";");
|
|
739
|
-
for (const cookie of cookies) {
|
|
740
|
-
const [name, value] = cookie.trim().split("=");
|
|
741
|
-
if (name === cookieName) {
|
|
742
|
-
return decodeURIComponent(value);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return null;
|
|
779
|
+
removeRequestInterceptor(id) {
|
|
780
|
+
this.requestInterceptors = this.requestInterceptors.filter(item => item.id !== id);
|
|
746
781
|
}
|
|
747
782
|
/**
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
* @param
|
|
751
|
-
*
|
|
783
|
+
* Remove a response interceptor by its ID
|
|
784
|
+
*
|
|
785
|
+
* @param id - The interceptor ID returned from addResponseInterceptor
|
|
786
|
+
*
|
|
787
|
+
* @example
|
|
788
|
+
* Config.getInstance().removeResponseInterceptor(id);
|
|
752
789
|
*/
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
790
|
+
removeResponseInterceptor(id) {
|
|
791
|
+
this.responseInterceptors = this.responseInterceptors.filter(item => item.id !== id);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Remove an error interceptor by its ID
|
|
795
|
+
*
|
|
796
|
+
* @param id - The interceptor ID returned from addErrorInterceptor
|
|
797
|
+
*
|
|
798
|
+
* @example
|
|
799
|
+
* Config.getInstance().removeErrorInterceptor(id);
|
|
800
|
+
*/
|
|
801
|
+
removeErrorInterceptor(id) {
|
|
802
|
+
this.errorInterceptors = this.errorInterceptors.filter(item => item.id !== id);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Clear all interceptors (request, response, and error)
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* Config.getInstance().clearInterceptors();
|
|
809
|
+
*/
|
|
810
|
+
clearInterceptors() {
|
|
811
|
+
this.requestInterceptors = [];
|
|
812
|
+
this.responseInterceptors = [];
|
|
813
|
+
this.errorInterceptors = [];
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Get all global request interceptors (in registration order)
|
|
817
|
+
* @internal
|
|
818
|
+
*/
|
|
819
|
+
getRequestInterceptors() {
|
|
820
|
+
return this.requestInterceptors.map(item => item.interceptor);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Get all global response interceptors (in registration order)
|
|
824
|
+
* @internal
|
|
825
|
+
*/
|
|
826
|
+
getResponseInterceptors() {
|
|
827
|
+
return this.responseInterceptors.map(item => item.interceptor);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Get all global error interceptors (in registration order)
|
|
831
|
+
* @internal
|
|
832
|
+
*/
|
|
833
|
+
getErrorInterceptors() {
|
|
834
|
+
return this.errorInterceptors.map(item => item.interceptor);
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Reset all configuration options to their default values
|
|
838
|
+
*
|
|
839
|
+
* @returns The config instance for chaining
|
|
840
|
+
*
|
|
841
|
+
* @example
|
|
842
|
+
* Config.getInstance().reset();
|
|
843
|
+
*/
|
|
844
|
+
reset() {
|
|
845
|
+
this.csrfToken = null;
|
|
846
|
+
this.csrfHeaderName = "X-CSRF-Token";
|
|
847
|
+
this.enableAntiCsrf = true;
|
|
848
|
+
this.xsrfCookieName = "XSRF-TOKEN";
|
|
849
|
+
this.xsrfHeaderName = "X-XSRF-TOKEN";
|
|
850
|
+
this.enableAutoXsrf = true;
|
|
851
|
+
this.clearInterceptors();
|
|
852
|
+
return this;
|
|
777
853
|
}
|
|
778
854
|
}
|
|
779
855
|
|
|
780
|
-
|
|
856
|
+
/**
|
|
857
|
+
* Base class with common functionality for all request types
|
|
858
|
+
* Provides the core request building and execution capabilities.
|
|
859
|
+
*/
|
|
781
860
|
class BaseRequest {
|
|
782
|
-
constructor() {
|
|
861
|
+
constructor(url) {
|
|
783
862
|
this.requestOptions = {
|
|
784
863
|
headers: {},
|
|
785
864
|
};
|
|
786
865
|
this.queryParams = new URLSearchParams();
|
|
787
866
|
this.autoApplyCsrfProtection = true;
|
|
788
|
-
|
|
867
|
+
// Per-request interceptors
|
|
868
|
+
this.requestInterceptors = [];
|
|
869
|
+
this.responseInterceptors = [];
|
|
870
|
+
this.errorInterceptors = [];
|
|
871
|
+
this.url = url;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Get GraphQL options if set (only for BodyRequest subclasses)
|
|
875
|
+
* @returns GraphQL options or undefined
|
|
876
|
+
*/
|
|
877
|
+
getGraphQLOptions() {
|
|
878
|
+
return undefined;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Creates a fluent API for setting enum-based options
|
|
882
|
+
* Combines direct setter with convenience methods
|
|
883
|
+
*/
|
|
884
|
+
createFluentSetter(optionName, options) {
|
|
885
|
+
const fluent = {};
|
|
886
|
+
// Create convenience methods for each enum value
|
|
887
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
888
|
+
fluent[key] = () => {
|
|
889
|
+
this.requestOptions[optionName] = value;
|
|
890
|
+
return this;
|
|
891
|
+
};
|
|
892
|
+
});
|
|
893
|
+
// Create the callable setter
|
|
894
|
+
const callable = (value) => {
|
|
895
|
+
this.requestOptions[optionName] = value;
|
|
896
|
+
return this;
|
|
897
|
+
};
|
|
898
|
+
return Object.assign(callable, fluent);
|
|
899
|
+
}
|
|
900
|
+
validateUrl(url) {
|
|
901
|
+
if (!(url === null || url === void 0 ? void 0 : url.trim()))
|
|
902
|
+
throw new RequestError("URL cannot be empty", url, this.method);
|
|
903
|
+
if (url.includes("\0") || url.includes("\r") || url.includes("\n")) {
|
|
904
|
+
throw new RequestError("Invalid URL (control chars)", url, this.method);
|
|
905
|
+
}
|
|
906
|
+
const trimmed = url.trim();
|
|
907
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
908
|
+
try {
|
|
909
|
+
new URL(trimmed);
|
|
910
|
+
}
|
|
911
|
+
catch (_a) {
|
|
912
|
+
throw new RequestError(`Invalid URL: ${trimmed}`, trimmed, this.method);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
789
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Add multiple HTTP headers to the request
|
|
918
|
+
*
|
|
919
|
+
* @param headers - Key-value pairs of header names and values
|
|
920
|
+
* @returns The request instance for chaining
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* request.withHeaders({
|
|
924
|
+
* 'Accept': 'application/json',
|
|
925
|
+
* 'X-Custom-Header': 'value'
|
|
926
|
+
* });
|
|
927
|
+
*/
|
|
790
928
|
withHeaders(headers) {
|
|
929
|
+
// Filter out null and undefined values
|
|
930
|
+
const filteredHeaders = {};
|
|
931
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
932
|
+
if (value !== null && value !== undefined) {
|
|
933
|
+
filteredHeaders[key] = value;
|
|
934
|
+
}
|
|
935
|
+
});
|
|
791
936
|
this.requestOptions.headers = {
|
|
792
|
-
...this.
|
|
793
|
-
...
|
|
937
|
+
...this.getHeadersRecord(),
|
|
938
|
+
...filteredHeaders,
|
|
794
939
|
};
|
|
795
940
|
return this;
|
|
796
941
|
}
|
|
942
|
+
/**
|
|
943
|
+
* Add a single HTTP header to the request
|
|
944
|
+
*
|
|
945
|
+
* @param key - The header name
|
|
946
|
+
* @param value - The header value
|
|
947
|
+
* @returns The request instance for chaining
|
|
948
|
+
*
|
|
949
|
+
* @example
|
|
950
|
+
* request.withHeader('Accept', 'application/json');
|
|
951
|
+
*/
|
|
797
952
|
withHeader(key, value) {
|
|
798
|
-
|
|
953
|
+
// Ignore null and undefined values
|
|
954
|
+
if (value !== null && value !== undefined) {
|
|
955
|
+
return this.withHeaders({ [key]: value });
|
|
956
|
+
}
|
|
957
|
+
return this;
|
|
799
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* Set a timeout for the request
|
|
961
|
+
* If the request takes longer than the specified timeout, it will be aborted.
|
|
962
|
+
*
|
|
963
|
+
* @param timeout - The timeout in milliseconds
|
|
964
|
+
* @returns The request instance for chaining
|
|
965
|
+
* @throws RequestError if timeout is not a positive number
|
|
966
|
+
*
|
|
967
|
+
* @example
|
|
968
|
+
* request.withTimeout(5000); // 5 seconds timeout
|
|
969
|
+
*/
|
|
800
970
|
withTimeout(timeout) {
|
|
801
971
|
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
802
|
-
throw new
|
|
972
|
+
throw new RequestError("Timeout must be a positive number", this.url, this.method);
|
|
803
973
|
this.requestOptions.timeout = timeout;
|
|
804
974
|
return this;
|
|
805
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Configure automatic retry behavior for failed requests
|
|
978
|
+
*
|
|
979
|
+
* @param retries - Number of retry attempts before failing
|
|
980
|
+
* @returns The request instance for chaining
|
|
981
|
+
* @throws RequestError if retries is not a non-negative integer
|
|
982
|
+
*
|
|
983
|
+
* @example
|
|
984
|
+
* request.withRetries(3); // Retry up to 3 times
|
|
985
|
+
*/
|
|
806
986
|
withRetries(retries) {
|
|
807
987
|
if (!Number.isInteger(retries) || retries < 0)
|
|
808
|
-
throw new
|
|
988
|
+
throw new RequestError("Retry count must be a non-negative integer", this.url, this.method);
|
|
809
989
|
this.requestOptions.retries = retries;
|
|
810
990
|
return this;
|
|
811
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Set a callback to be invoked before each retry attempt
|
|
994
|
+
* Useful for implementing backoff strategies or logging retry attempts.
|
|
995
|
+
*
|
|
996
|
+
* @param callback - Function to call before retrying
|
|
997
|
+
* @returns The request instance for chaining
|
|
998
|
+
*
|
|
999
|
+
* @example
|
|
1000
|
+
* request.onRetry(({ attempt, error }) => {
|
|
1001
|
+
* console.log(`Retry attempt ${attempt} after error: ${error.message}`);
|
|
1002
|
+
* return new Promise(resolve => setTimeout(resolve, attempt * 1000));
|
|
1003
|
+
* });
|
|
1004
|
+
*/
|
|
812
1005
|
onRetry(callback) {
|
|
813
1006
|
this.requestOptions.onRetry = callback;
|
|
814
1007
|
return this;
|
|
815
1008
|
}
|
|
816
1009
|
/**
|
|
817
|
-
* Sets
|
|
818
|
-
* @
|
|
819
|
-
*
|
|
1010
|
+
* Sets credentials policy - supports both direct call and fluent API
|
|
1011
|
+
* @example
|
|
1012
|
+
* request.withCredentials("include")
|
|
1013
|
+
* request.withCredentials(CredentialsPolicy.INCLUDE)
|
|
1014
|
+
* request.withCredentials.INCLUDE()
|
|
820
1015
|
*/
|
|
821
|
-
withCredentials(
|
|
822
|
-
this.
|
|
823
|
-
|
|
1016
|
+
get withCredentials() {
|
|
1017
|
+
return this.createFluentSetter("credentials", {
|
|
1018
|
+
INCLUDE: CredentialsPolicy.INCLUDE,
|
|
1019
|
+
OMIT: CredentialsPolicy.OMIT,
|
|
1020
|
+
SAME_ORIGIN: CredentialsPolicy.SAME_ORIGIN,
|
|
1021
|
+
});
|
|
824
1022
|
}
|
|
825
1023
|
/**
|
|
826
1024
|
* Allows providing an external AbortController to cancel the request
|
|
@@ -841,22 +1039,37 @@ class BaseRequest {
|
|
|
841
1039
|
return this;
|
|
842
1040
|
}
|
|
843
1041
|
/**
|
|
844
|
-
* Sets
|
|
845
|
-
* @
|
|
846
|
-
*
|
|
1042
|
+
* Sets referrer policy - supports both direct call and fluent API
|
|
1043
|
+
* @example
|
|
1044
|
+
* request.withReferrerPolicy("no-referrer")
|
|
1045
|
+
* request.withReferrerPolicy(ReferrerPolicy.NO_REFERRER)
|
|
1046
|
+
* request.withReferrerPolicy.NO_REFERRER()
|
|
847
1047
|
*/
|
|
848
|
-
withReferrerPolicy(
|
|
849
|
-
this.
|
|
850
|
-
|
|
1048
|
+
get withReferrerPolicy() {
|
|
1049
|
+
return this.createFluentSetter("referrerPolicy", {
|
|
1050
|
+
ORIGIN: ReferrerPolicy.ORIGIN,
|
|
1051
|
+
UNSAFE_URL: ReferrerPolicy.UNSAFE_URL,
|
|
1052
|
+
SAME_ORIGIN: ReferrerPolicy.SAME_ORIGIN,
|
|
1053
|
+
NO_REFERRER: ReferrerPolicy.NO_REFERRER,
|
|
1054
|
+
STRICT_ORIGIN: ReferrerPolicy.STRICT_ORIGIN,
|
|
1055
|
+
ORIGIN_WHEN_CROSS_ORIGIN: ReferrerPolicy.ORIGIN_WHEN_CROSS_ORIGIN,
|
|
1056
|
+
NO_REFERRER_WHEN_DOWNGRADE: ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE,
|
|
1057
|
+
STRICT_ORIGIN_WHEN_CROSS_ORIGIN: ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
|
|
1058
|
+
});
|
|
851
1059
|
}
|
|
852
1060
|
/**
|
|
853
|
-
* Sets
|
|
854
|
-
* @
|
|
855
|
-
*
|
|
1061
|
+
* Sets redirect mode - supports both direct call and fluent API
|
|
1062
|
+
* @example
|
|
1063
|
+
* request.withRedirect("follow")
|
|
1064
|
+
* request.withRedirect(RedirectMode.FOLLOW)
|
|
1065
|
+
* request.withRedirect.FOLLOW()
|
|
856
1066
|
*/
|
|
857
|
-
withRedirect(
|
|
858
|
-
this.
|
|
859
|
-
|
|
1067
|
+
get withRedirect() {
|
|
1068
|
+
return this.createFluentSetter("redirect", {
|
|
1069
|
+
FOLLOW: RedirectMode.FOLLOW,
|
|
1070
|
+
ERROR: RedirectMode.ERROR,
|
|
1071
|
+
MANUAL: RedirectMode.MANUAL,
|
|
1072
|
+
});
|
|
860
1073
|
}
|
|
861
1074
|
/**
|
|
862
1075
|
* Sets the keepalive flag for the request
|
|
@@ -868,13 +1081,18 @@ class BaseRequest {
|
|
|
868
1081
|
return this;
|
|
869
1082
|
}
|
|
870
1083
|
/**
|
|
871
|
-
* Sets
|
|
872
|
-
* @
|
|
873
|
-
*
|
|
1084
|
+
* Sets request priority - supports both direct call and fluent API
|
|
1085
|
+
* @example
|
|
1086
|
+
* request.withPriority("high")
|
|
1087
|
+
* request.withPriority(RequestPriority.HIGH)
|
|
1088
|
+
* request.withPriority.HIGH()
|
|
874
1089
|
*/
|
|
875
|
-
withPriority(
|
|
876
|
-
this.
|
|
877
|
-
|
|
1090
|
+
get withPriority() {
|
|
1091
|
+
return this.createFluentSetter("priority", {
|
|
1092
|
+
HIGH: RequestPriority.HIGH,
|
|
1093
|
+
LOW: RequestPriority.LOW,
|
|
1094
|
+
AUTO: RequestPriority.AUTO,
|
|
1095
|
+
});
|
|
878
1096
|
}
|
|
879
1097
|
/**
|
|
880
1098
|
* Adds query parameters to the request URL
|
|
@@ -912,13 +1130,19 @@ class BaseRequest {
|
|
|
912
1130
|
return this;
|
|
913
1131
|
}
|
|
914
1132
|
/**
|
|
915
|
-
* Sets
|
|
916
|
-
* @
|
|
917
|
-
*
|
|
1133
|
+
* Sets request mode - supports both direct call and fluent API
|
|
1134
|
+
* @example
|
|
1135
|
+
* request.withMode("cors")
|
|
1136
|
+
* request.withMode(RequestMode.CORS)
|
|
1137
|
+
* request.withMode.CORS()
|
|
918
1138
|
*/
|
|
919
|
-
withMode(
|
|
920
|
-
this.
|
|
921
|
-
|
|
1139
|
+
get withMode() {
|
|
1140
|
+
return this.createFluentSetter("mode", {
|
|
1141
|
+
CORS: RequestMode.CORS,
|
|
1142
|
+
NO_CORS: RequestMode.NO_CORS,
|
|
1143
|
+
SAME_ORIGIN: RequestMode.SAME_ORIGIN,
|
|
1144
|
+
NAVIGATE: RequestMode.NAVIGATE,
|
|
1145
|
+
});
|
|
922
1146
|
}
|
|
923
1147
|
/**
|
|
924
1148
|
* Shorthand for setting the Content-Type header
|
|
@@ -968,7 +1192,7 @@ class BaseRequest {
|
|
|
968
1192
|
if (typeof Buffer !== "undefined")
|
|
969
1193
|
return Buffer.from(str).toString("base64");
|
|
970
1194
|
// Fallback (should never happen in modern environments)
|
|
971
|
-
throw new
|
|
1195
|
+
throw new RequestError("Base64 encoding is not supported", this.url, this.method);
|
|
972
1196
|
}
|
|
973
1197
|
/**
|
|
974
1198
|
* Sets the bearer token for authentication
|
|
@@ -978,13 +1202,23 @@ class BaseRequest {
|
|
|
978
1202
|
withBearerToken(token) {
|
|
979
1203
|
return this.withAuthorization(`Bearer ${token}`);
|
|
980
1204
|
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Safely get headers as a Record<string, string>
|
|
1207
|
+
* @returns The headers object
|
|
1208
|
+
*/
|
|
1209
|
+
getHeadersRecord() {
|
|
1210
|
+
if (typeof this.requestOptions.headers === "object" && this.requestOptions.headers !== null) {
|
|
1211
|
+
return this.requestOptions.headers;
|
|
1212
|
+
}
|
|
1213
|
+
return {};
|
|
1214
|
+
}
|
|
981
1215
|
/**
|
|
982
1216
|
* Helper function to check for header presence in a case-insensitive way
|
|
983
|
-
* @param headers Headers object
|
|
984
1217
|
* @param headerName Header name to check
|
|
985
1218
|
* @returns Boolean indicating if the header exists (case-insensitive)
|
|
986
1219
|
*/
|
|
987
|
-
hasHeader(
|
|
1220
|
+
hasHeader(headerName) {
|
|
1221
|
+
const headers = this.getHeadersRecord();
|
|
988
1222
|
return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
|
|
989
1223
|
}
|
|
990
1224
|
/**
|
|
@@ -993,12 +1227,11 @@ class BaseRequest {
|
|
|
993
1227
|
* @returns The instance for chaining
|
|
994
1228
|
*/
|
|
995
1229
|
withCookies(cookies) {
|
|
996
|
-
const cookieEntries = Object.entries(cookies);
|
|
1230
|
+
const cookieEntries = Object.entries(cookies || {});
|
|
997
1231
|
if (cookieEntries.length === 0) {
|
|
998
1232
|
return this;
|
|
999
1233
|
}
|
|
1000
|
-
|
|
1001
|
-
const currentHeaders = this.requestOptions.headers || {};
|
|
1234
|
+
const currentHeaders = this.getHeadersRecord();
|
|
1002
1235
|
// Get all existing cookie headers with different cases
|
|
1003
1236
|
const cookieValues = [];
|
|
1004
1237
|
const cookieHeaderKeys = [];
|
|
@@ -1067,110 +1300,200 @@ class BaseRequest {
|
|
|
1067
1300
|
});
|
|
1068
1301
|
}
|
|
1069
1302
|
/**
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1303
|
+
* Add a request interceptor for this specific request
|
|
1304
|
+
* Request interceptors can modify the request configuration or return an early response
|
|
1305
|
+
*
|
|
1306
|
+
* @param interceptor - The request interceptor function
|
|
1307
|
+
* @returns The instance for chaining
|
|
1308
|
+
*
|
|
1309
|
+
* @example
|
|
1310
|
+
* request.withRequestInterceptor((config) => {
|
|
1311
|
+
* config.headers['X-Custom'] = 'value';
|
|
1312
|
+
* return config;
|
|
1313
|
+
* });
|
|
1314
|
+
*/
|
|
1315
|
+
withRequestInterceptor(interceptor) {
|
|
1316
|
+
this.requestInterceptors.push(interceptor);
|
|
1317
|
+
return this;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Add a response interceptor for this specific request
|
|
1321
|
+
* Response interceptors can transform the response
|
|
1322
|
+
*
|
|
1323
|
+
* @param interceptor - The response interceptor function
|
|
1324
|
+
* @returns The instance for chaining
|
|
1325
|
+
*
|
|
1326
|
+
* @example
|
|
1327
|
+
* request.withResponseInterceptor((response) => {
|
|
1328
|
+
* console.log('Status:', response.status);
|
|
1329
|
+
* return response;
|
|
1330
|
+
* });
|
|
1331
|
+
*/
|
|
1332
|
+
withResponseInterceptor(interceptor) {
|
|
1333
|
+
this.responseInterceptors.push(interceptor);
|
|
1334
|
+
return this;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Add an error interceptor for this specific request
|
|
1338
|
+
* Error interceptors can handle or transform errors
|
|
1339
|
+
*
|
|
1340
|
+
* @param interceptor - The error interceptor function
|
|
1072
1341
|
* @returns The instance for chaining
|
|
1342
|
+
*
|
|
1343
|
+
* @example
|
|
1344
|
+
* request.withErrorInterceptor((error) => {
|
|
1345
|
+
* console.error('Request failed:', error);
|
|
1346
|
+
* throw error;
|
|
1347
|
+
* });
|
|
1073
1348
|
*/
|
|
1074
|
-
|
|
1075
|
-
this.
|
|
1076
|
-
this.cacheManager = new CacheManager(options);
|
|
1349
|
+
withErrorInterceptor(interceptor) {
|
|
1350
|
+
this.errorInterceptors.push(interceptor);
|
|
1077
1351
|
return this;
|
|
1078
1352
|
}
|
|
1079
1353
|
/**
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1354
|
+
* Execute the request and return the ResponseWrapper
|
|
1355
|
+
* This is the base method for getting the full response.
|
|
1356
|
+
*
|
|
1357
|
+
* @returns A promise that resolves to the ResponseWrapper
|
|
1358
|
+
*
|
|
1359
|
+
* @example
|
|
1360
|
+
* const response = await request.getResponse();
|
|
1361
|
+
* console.log(response.status);
|
|
1362
|
+
*/
|
|
1363
|
+
async getResponse() {
|
|
1364
|
+
const url = this.formatUrlWithQueryParams(this.url);
|
|
1365
|
+
this.applyCsrfProtection();
|
|
1366
|
+
const fetchOptions = {
|
|
1367
|
+
...this.requestOptions,
|
|
1368
|
+
method: this.method,
|
|
1369
|
+
};
|
|
1370
|
+
return !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Execute the request and parse the response as JSON
|
|
1374
|
+
*
|
|
1375
|
+
* @returns A promise that resolves to the parsed JSON data
|
|
1376
|
+
* @throws {RequestError} When the request fails, JSON parsing fails, GraphQL errors occur (if throwOnError enabled), or body is already consumed
|
|
1377
|
+
*
|
|
1378
|
+
* @example
|
|
1379
|
+
* const users = await request.getJson<User[]>();
|
|
1380
|
+
*
|
|
1381
|
+
* @example
|
|
1382
|
+
* // Error handling - errors are always RequestError
|
|
1383
|
+
* try {
|
|
1384
|
+
* const data = await request.getJson();
|
|
1385
|
+
* } catch (error) {
|
|
1386
|
+
* if (error instanceof RequestError) {
|
|
1387
|
+
* console.log(error.status, error.url, error.method);
|
|
1388
|
+
* }
|
|
1389
|
+
* }
|
|
1390
|
+
*/
|
|
1391
|
+
async getJson() {
|
|
1392
|
+
const response = await this.getResponse();
|
|
1393
|
+
return response.getJson();
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Execute the request and get the response as text
|
|
1397
|
+
*
|
|
1398
|
+
* @returns A promise that resolves to the response text
|
|
1399
|
+
*
|
|
1400
|
+
* @example
|
|
1401
|
+
* const text = await request.getText();
|
|
1402
|
+
*/
|
|
1403
|
+
async getText() {
|
|
1404
|
+
const response = await this.getResponse();
|
|
1405
|
+
return response.getText();
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Execute the request and get the response as a Blob
|
|
1409
|
+
*
|
|
1410
|
+
* @returns A promise that resolves to the response Blob
|
|
1411
|
+
*
|
|
1412
|
+
* @example
|
|
1413
|
+
* const blob = await request.getBlob();
|
|
1414
|
+
*/
|
|
1415
|
+
async getBlob() {
|
|
1416
|
+
const response = await this.getResponse();
|
|
1417
|
+
return response.getBlob();
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Execute the request and get the response body as a ReadableStream
|
|
1421
|
+
*
|
|
1422
|
+
* @returns A promise that resolves to the response body stream
|
|
1423
|
+
*
|
|
1424
|
+
* @example
|
|
1425
|
+
* const stream = await request.getBody();
|
|
1426
|
+
*/
|
|
1427
|
+
async getBody() {
|
|
1428
|
+
const response = await this.getResponse();
|
|
1429
|
+
return response.getBody();
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Execute the request and extract specific data using a selector function
|
|
1433
|
+
* If no selector is provided, returns the full JSON response.
|
|
1434
|
+
*
|
|
1435
|
+
* @param selector - Optional function to extract and transform data
|
|
1436
|
+
* @returns A promise that resolves to the selected data
|
|
1437
|
+
* @throws {RequestError} When the request fails, JSON parsing fails, or the selector throws an error
|
|
1438
|
+
*
|
|
1439
|
+
* @example
|
|
1440
|
+
* // Get full response
|
|
1441
|
+
* const data = await request.getData();
|
|
1442
|
+
*
|
|
1443
|
+
* // Extract specific data
|
|
1444
|
+
* const users = await request.getData(data => data.results.users);
|
|
1445
|
+
*
|
|
1446
|
+
* @example
|
|
1447
|
+
* // Error handling - errors are always RequestError
|
|
1448
|
+
* try {
|
|
1449
|
+
* const data = await request.getData();
|
|
1450
|
+
* } catch (error) {
|
|
1451
|
+
* if (error instanceof RequestError) {
|
|
1452
|
+
* console.log(error.status, error.url, error.method);
|
|
1453
|
+
* }
|
|
1454
|
+
* }
|
|
1455
|
+
*/
|
|
1456
|
+
async getData(selector) {
|
|
1457
|
+
const response = await this.getResponse();
|
|
1458
|
+
return response.getData(selector);
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Apply CSRF protection headers based on configuration
|
|
1082
1462
|
*/
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1463
|
+
applyCsrfProtection() {
|
|
1464
|
+
if (!this.autoApplyCsrfProtection)
|
|
1465
|
+
return;
|
|
1466
|
+
const config = Config.getInstance();
|
|
1467
|
+
// Apply anti-CSRF headers if enabled
|
|
1468
|
+
if (config.isAntiCsrfEnabled()) {
|
|
1469
|
+
this.withHeaders({
|
|
1470
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
// Apply global CSRF token if set
|
|
1474
|
+
const globalToken = config.getCsrfToken();
|
|
1475
|
+
if (globalToken) {
|
|
1476
|
+
const csrfHeaderName = config.getCsrfHeaderName();
|
|
1477
|
+
const hasLocalToken = this.hasHeader("X-CSRF-Token") || this.hasHeader(csrfHeaderName);
|
|
1478
|
+
if (!hasLocalToken) {
|
|
1091
1479
|
this.withHeaders({
|
|
1092
|
-
|
|
1480
|
+
[csrfHeaderName]: globalToken,
|
|
1093
1481
|
});
|
|
1094
1482
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
const hasLocalToken = this.hasHeader(
|
|
1483
|
+
}
|
|
1484
|
+
// Check for XSRF token in cookies and send it as a header
|
|
1485
|
+
if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
|
|
1486
|
+
const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
|
|
1487
|
+
if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
|
|
1488
|
+
const xsrfHeaderName = config.getXsrfHeaderName();
|
|
1489
|
+
const hasLocalToken = this.hasHeader("X-XSRF-TOKEN") || this.hasHeader(xsrfHeaderName);
|
|
1102
1490
|
if (!hasLocalToken) {
|
|
1103
1491
|
this.withHeaders({
|
|
1104
|
-
[
|
|
1492
|
+
[xsrfHeaderName]: xsrfToken,
|
|
1105
1493
|
});
|
|
1106
1494
|
}
|
|
1107
1495
|
}
|
|
1108
|
-
// check for XSRF token in cookies and send it as a header
|
|
1109
|
-
if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
|
|
1110
|
-
const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
|
|
1111
|
-
if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
|
|
1112
|
-
const headers = this.requestOptions.headers;
|
|
1113
|
-
const xsrfHeaderName = config.getXsrfHeaderName();
|
|
1114
|
-
const hasLocalToken = this.hasHeader(headers, "X-XSRF-TOKEN") || this.hasHeader(headers, xsrfHeaderName);
|
|
1115
|
-
if (!hasLocalToken) {
|
|
1116
|
-
this.withHeaders({
|
|
1117
|
-
[xsrfHeaderName]: xsrfToken,
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
// Cast the request options to RequestInit to ensure compatibility
|
|
1124
|
-
const fetchOptions = {
|
|
1125
|
-
...this.requestOptions,
|
|
1126
|
-
method: this.method,
|
|
1127
|
-
};
|
|
1128
|
-
// Create the base promise - handle caching if enabled
|
|
1129
|
-
let basePromise;
|
|
1130
|
-
if (this.cacheEnabled && this.cacheManager) {
|
|
1131
|
-
const cacheKey = this.cacheManager.generateKey(url, this.method, this.requestOptions.headers, this.requestOptions.body);
|
|
1132
|
-
basePromise = this.executeWithCache(url, fetchOptions, cacheKey);
|
|
1133
1496
|
}
|
|
1134
|
-
else {
|
|
1135
|
-
basePromise = !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
|
|
1136
|
-
}
|
|
1137
|
-
const responsePromise = basePromise;
|
|
1138
|
-
// Attach convenience methods that begin processing immediately
|
|
1139
|
-
responsePromise.getJson = async () => {
|
|
1140
|
-
return basePromise.then(response => response.getJson());
|
|
1141
|
-
};
|
|
1142
|
-
responsePromise.getText = async () => {
|
|
1143
|
-
return basePromise.then(response => response.getText());
|
|
1144
|
-
};
|
|
1145
|
-
responsePromise.getBlob = async () => {
|
|
1146
|
-
return basePromise.then(response => response.getBlob());
|
|
1147
|
-
};
|
|
1148
|
-
responsePromise.getArrayBuffer = async () => {
|
|
1149
|
-
return basePromise.then(response => response.getArrayBuffer());
|
|
1150
|
-
};
|
|
1151
|
-
responsePromise.getBody = async () => {
|
|
1152
|
-
return basePromise.then(response => response.getBody());
|
|
1153
|
-
};
|
|
1154
|
-
responsePromise.getData = async (selector) => {
|
|
1155
|
-
try {
|
|
1156
|
-
const data = await basePromise.then(response => response.getJson());
|
|
1157
|
-
// If no selector is provided, return the raw JSON data
|
|
1158
|
-
if (!selector)
|
|
1159
|
-
return data;
|
|
1160
|
-
// Apply the selector if provided
|
|
1161
|
-
return selector(data);
|
|
1162
|
-
}
|
|
1163
|
-
catch (error) {
|
|
1164
|
-
if (error instanceof Error) {
|
|
1165
|
-
// If selector fails, enhance the error with the original data
|
|
1166
|
-
const enhancedError = new Error(`Data selector failed: ${error.message}. Original data: ${JSON.stringify(await basePromise.then(response => response.getJson()), null, 2)}`);
|
|
1167
|
-
enhancedError.stack = error.stack;
|
|
1168
|
-
throw enhancedError;
|
|
1169
|
-
}
|
|
1170
|
-
throw error;
|
|
1171
|
-
}
|
|
1172
|
-
};
|
|
1173
|
-
return responsePromise;
|
|
1174
1497
|
}
|
|
1175
1498
|
/**
|
|
1176
1499
|
* Formats the URL with any query parameters
|
|
@@ -1178,7 +1501,8 @@ class BaseRequest {
|
|
|
1178
1501
|
* @returns The URL with query parameters appended
|
|
1179
1502
|
*/
|
|
1180
1503
|
formatUrlWithQueryParams(url) {
|
|
1181
|
-
|
|
1504
|
+
const queryString = this.queryParams.toString();
|
|
1505
|
+
if (!queryString) {
|
|
1182
1506
|
return url;
|
|
1183
1507
|
}
|
|
1184
1508
|
try {
|
|
@@ -1194,7 +1518,7 @@ class BaseRequest {
|
|
|
1194
1518
|
// Handle relative URLs
|
|
1195
1519
|
const hasExistingParams = url.includes("?");
|
|
1196
1520
|
const separator = hasExistingParams ? "&" : "?";
|
|
1197
|
-
return `${url}${separator}${
|
|
1521
|
+
return `${url}${separator}${queryString}`;
|
|
1198
1522
|
}
|
|
1199
1523
|
}
|
|
1200
1524
|
/**
|
|
@@ -1202,136 +1526,298 @@ class BaseRequest {
|
|
|
1202
1526
|
* @param url The formatted URL to send the request to
|
|
1203
1527
|
* @param fetchOptions The fetch options to use
|
|
1204
1528
|
* @returns A wrapped response object
|
|
1529
|
+
* @throws RequestError if the request fails after all retries
|
|
1205
1530
|
*/
|
|
1206
1531
|
async executeWithRetries(url, fetchOptions) {
|
|
1207
|
-
let attempt = 0;
|
|
1208
1532
|
const maxRetries = this.requestOptions.retries || 0;
|
|
1209
|
-
|
|
1533
|
+
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
1534
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1210
1535
|
try {
|
|
1211
1536
|
return await this.executeRequest(url, fetchOptions);
|
|
1212
1537
|
}
|
|
1213
1538
|
catch (error) {
|
|
1214
|
-
const requestError = error instanceof RequestError ? error : RequestError.networkError(url,
|
|
1215
|
-
|
|
1216
|
-
if (attempt >= maxRetries || requestError.timeoutError)
|
|
1539
|
+
const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, error instanceof Error ? error : new Error(String(error)));
|
|
1540
|
+
if (attempt >= maxRetries)
|
|
1217
1541
|
throw requestError;
|
|
1218
|
-
attempt++;
|
|
1219
1542
|
if (this.requestOptions.onRetry) {
|
|
1220
|
-
await this.requestOptions.onRetry({ attempt, error: requestError });
|
|
1543
|
+
await this.requestOptions.onRetry({ attempt: attempt + 1, error: requestError });
|
|
1221
1544
|
}
|
|
1222
1545
|
}
|
|
1223
|
-
}
|
|
1224
|
-
//
|
|
1225
|
-
throw new RequestError(`Max retries reached
|
|
1546
|
+
}
|
|
1547
|
+
// This should never happen but is needed for type safety
|
|
1548
|
+
throw new RequestError(`Max retries reached`, url, method);
|
|
1226
1549
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
let
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
// If
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1550
|
+
/**
|
|
1551
|
+
* Run request interceptors in order: global interceptors first, then per-request
|
|
1552
|
+
* @param configParam - The request configuration
|
|
1553
|
+
* @returns Modified config or a Response to short-circuit
|
|
1554
|
+
*/
|
|
1555
|
+
async runRequestInterceptors(configParam) {
|
|
1556
|
+
const globalConfig = Config.getInstance();
|
|
1557
|
+
const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this.requestInterceptors];
|
|
1558
|
+
let currentConfig = configParam;
|
|
1559
|
+
for (let i = 0; i < allInterceptors.length; i++) {
|
|
1560
|
+
try {
|
|
1561
|
+
const result = await allInterceptors[i](currentConfig);
|
|
1562
|
+
// If interceptor returns a Response, short-circuit
|
|
1563
|
+
if (result instanceof Response) {
|
|
1564
|
+
return result;
|
|
1565
|
+
}
|
|
1566
|
+
currentConfig = result;
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1570
|
+
throw new RequestError(`Request interceptor failed: ${errorMessage}`, currentConfig.url, currentConfig.method);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return currentConfig;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Run response interceptors in reverse order: per-request interceptors first, then global in reverse
|
|
1577
|
+
* @param response - The response wrapper
|
|
1578
|
+
* @returns Modified response wrapper
|
|
1579
|
+
*/
|
|
1580
|
+
async runResponseInterceptors(response) {
|
|
1581
|
+
const globalConfig = Config.getInstance();
|
|
1582
|
+
const globalInterceptors = globalConfig.getResponseInterceptors();
|
|
1583
|
+
// Per-request in order, then global in reverse
|
|
1584
|
+
const allInterceptors = [...this.responseInterceptors, ...globalInterceptors.reverse()];
|
|
1585
|
+
let currentResponse = response;
|
|
1586
|
+
for (let i = 0; i < allInterceptors.length; i++) {
|
|
1587
|
+
try {
|
|
1588
|
+
currentResponse = await allInterceptors[i](currentResponse);
|
|
1589
|
+
}
|
|
1590
|
+
catch (error) {
|
|
1591
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1592
|
+
const url = currentResponse.url || "";
|
|
1593
|
+
const method = currentResponse.method || "";
|
|
1594
|
+
throw new RequestError(`Response interceptor failed: ${errorMessage}`, url, method);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return currentResponse;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Run error interceptors in reverse order: per-request interceptors first, then global in reverse
|
|
1601
|
+
* @param error - The error that occurred
|
|
1602
|
+
* @returns Modified error or a ResponseWrapper to recover
|
|
1603
|
+
*/
|
|
1604
|
+
async runErrorInterceptors(error) {
|
|
1605
|
+
const globalConfig = Config.getInstance();
|
|
1606
|
+
const globalInterceptors = globalConfig.getErrorInterceptors();
|
|
1607
|
+
// Per-request in order, then global in reverse
|
|
1608
|
+
const allInterceptors = [...this.errorInterceptors, ...globalInterceptors.reverse()];
|
|
1609
|
+
let currentError = error;
|
|
1610
|
+
for (let i = 0; i < allInterceptors.length; i++) {
|
|
1611
|
+
try {
|
|
1612
|
+
// If previous interceptor recovered with a response, stop processing
|
|
1613
|
+
if (currentError instanceof ResponseWrapper) {
|
|
1614
|
+
return currentError;
|
|
1615
|
+
}
|
|
1616
|
+
const result = await allInterceptors[i](currentError);
|
|
1617
|
+
currentError = result;
|
|
1618
|
+
}
|
|
1619
|
+
catch (interceptorError) {
|
|
1620
|
+
// If an error interceptor throws, that becomes the new error
|
|
1621
|
+
if (interceptorError instanceof RequestError) {
|
|
1622
|
+
currentError = interceptorError;
|
|
1251
1623
|
}
|
|
1252
1624
|
else {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1625
|
+
const errorMessage = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
|
|
1626
|
+
// Always wrap in RequestError when we have context
|
|
1627
|
+
if (currentError instanceof RequestError) {
|
|
1628
|
+
currentError = new RequestError(`Error interceptor ${i + 1} failed: ${errorMessage}`, currentError.url, currentError.method, {
|
|
1629
|
+
status: currentError.status,
|
|
1630
|
+
response: currentError.response,
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
else if (currentError instanceof ResponseWrapper && currentError.url && currentError.method) {
|
|
1634
|
+
// If it's a ResponseWrapper, we can still create a proper RequestError from its context
|
|
1635
|
+
currentError = new RequestError(`Error interceptor ${i + 1} failed: ${errorMessage}`, currentError.url, currentError.method, {
|
|
1636
|
+
status: currentError.status,
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
// Last resort: if we have no context at all, use the original error's context
|
|
1641
|
+
// This shouldn't happen in practice, but handle it gracefully
|
|
1642
|
+
const errorObj = interceptorError instanceof Error ? interceptorError : new Error(String(interceptorError));
|
|
1643
|
+
currentError = RequestError.networkError(error.url, error.method, errorObj);
|
|
1644
|
+
}
|
|
1260
1645
|
}
|
|
1261
1646
|
}
|
|
1647
|
+
}
|
|
1648
|
+
return currentError;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Helper to create an abort signal with timeout support
|
|
1652
|
+
* Handles various AbortSignal API levels gracefully
|
|
1653
|
+
*/
|
|
1654
|
+
createAbortSignal(timeoutMs, externalController) {
|
|
1655
|
+
let timeoutId;
|
|
1656
|
+
let timeoutController;
|
|
1657
|
+
let isTimeout = false;
|
|
1658
|
+
// No timeout - just use external controller
|
|
1659
|
+
if (!timeoutMs) {
|
|
1660
|
+
return {
|
|
1661
|
+
signal: externalController === null || externalController === void 0 ? void 0 : externalController.signal,
|
|
1662
|
+
cleanup: () => { },
|
|
1663
|
+
wasTimeout: () => false,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
// Check if AbortSignal.any() is available (modern browsers)
|
|
1667
|
+
const hasAbortSignalAny = typeof AbortSignal.any === "function";
|
|
1668
|
+
const hasAbortSignalTimeout = typeof AbortSignal.timeout === "function";
|
|
1669
|
+
// Create timeout signal
|
|
1670
|
+
const createTimeoutSignal = () => {
|
|
1671
|
+
if (hasAbortSignalTimeout) {
|
|
1672
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
1673
|
+
signal.addEventListener("abort", () => (isTimeout = true), { once: true });
|
|
1674
|
+
return signal;
|
|
1675
|
+
}
|
|
1262
1676
|
else {
|
|
1677
|
+
timeoutController = new AbortController();
|
|
1263
1678
|
timeoutId = setTimeout(() => {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
},
|
|
1679
|
+
isTimeout = true;
|
|
1680
|
+
timeoutController.abort();
|
|
1681
|
+
}, timeoutMs);
|
|
1682
|
+
return timeoutController.signal;
|
|
1267
1683
|
}
|
|
1684
|
+
};
|
|
1685
|
+
const timeoutSignal = createTimeoutSignal();
|
|
1686
|
+
// Combine signals if we have both timeout and external controller
|
|
1687
|
+
const finalSignal = externalController && hasAbortSignalAny
|
|
1688
|
+
? AbortSignal.any([externalController.signal, timeoutSignal])
|
|
1689
|
+
: externalController
|
|
1690
|
+
? this.combineSignalsManually(externalController.signal, timeoutSignal)
|
|
1691
|
+
: timeoutSignal;
|
|
1692
|
+
return {
|
|
1693
|
+
signal: finalSignal,
|
|
1694
|
+
cleanup: () => {
|
|
1695
|
+
if (timeoutId !== undefined)
|
|
1696
|
+
clearTimeout(timeoutId);
|
|
1697
|
+
},
|
|
1698
|
+
wasTimeout: () => isTimeout,
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Manually combine two abort signals for older environments
|
|
1703
|
+
* Returns the first signal and listens to the second
|
|
1704
|
+
*/
|
|
1705
|
+
combineSignalsManually(signal1, signal2) {
|
|
1706
|
+
// If either is already aborted, use that one
|
|
1707
|
+
if (signal1.aborted)
|
|
1708
|
+
return signal1;
|
|
1709
|
+
if (signal2.aborted)
|
|
1710
|
+
return signal2;
|
|
1711
|
+
// Use a controller to create a combined signal
|
|
1712
|
+
const controller = new AbortController();
|
|
1713
|
+
const abort = () => controller.abort();
|
|
1714
|
+
signal1.addEventListener("abort", abort, { once: true });
|
|
1715
|
+
signal2.addEventListener("abort", abort, { once: true });
|
|
1716
|
+
return controller.signal;
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Convert fetchOptions to RequestConfig with proper typing
|
|
1720
|
+
*/
|
|
1721
|
+
createRequestConfig(url, fetchOptions) {
|
|
1722
|
+
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
1723
|
+
const headers = this.getHeadersRecord();
|
|
1724
|
+
const extendedOptions = fetchOptions;
|
|
1725
|
+
return {
|
|
1726
|
+
url,
|
|
1727
|
+
method,
|
|
1728
|
+
headers,
|
|
1729
|
+
body: fetchOptions.body,
|
|
1730
|
+
signal: fetchOptions.signal || undefined,
|
|
1731
|
+
credentials: fetchOptions.credentials,
|
|
1732
|
+
mode: fetchOptions.mode,
|
|
1733
|
+
redirect: fetchOptions.redirect,
|
|
1734
|
+
referrer: fetchOptions.referrer,
|
|
1735
|
+
referrerPolicy: fetchOptions.referrerPolicy,
|
|
1736
|
+
keepalive: fetchOptions.keepalive,
|
|
1737
|
+
priority: extendedOptions.priority,
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Apply interceptor results back to fetchOptions
|
|
1742
|
+
*/
|
|
1743
|
+
applyRequestConfig(config, fetchOptions) {
|
|
1744
|
+
fetchOptions.headers = config.headers;
|
|
1745
|
+
if (config.body !== undefined) {
|
|
1746
|
+
fetchOptions.body = config.body;
|
|
1268
1747
|
}
|
|
1748
|
+
}
|
|
1749
|
+
async executeRequest(url, fetchOptions) {
|
|
1750
|
+
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
1751
|
+
// Setup abort signal with timeout
|
|
1752
|
+
const abortSignal = this.createAbortSignal(this.requestOptions.timeout, this.abortController);
|
|
1269
1753
|
try {
|
|
1754
|
+
// Run request interceptors before making the request
|
|
1755
|
+
const requestConfig = this.createRequestConfig(url, fetchOptions);
|
|
1756
|
+
const interceptorResult = await this.runRequestInterceptors(requestConfig);
|
|
1757
|
+
// If interceptor returned a Response, short-circuit and wrap it
|
|
1758
|
+
if (interceptorResult instanceof Response) {
|
|
1759
|
+
const graphQLOptions = this.getGraphQLOptions();
|
|
1760
|
+
const wrappedResponse = new ResponseWrapper(interceptorResult, this.url, this.method, graphQLOptions);
|
|
1761
|
+
return await this.runResponseInterceptors(wrappedResponse);
|
|
1762
|
+
}
|
|
1763
|
+
// Update fetchOptions with interceptor modifications
|
|
1764
|
+
url = interceptorResult.url;
|
|
1765
|
+
this.validateUrl(url);
|
|
1766
|
+
this.applyRequestConfig(interceptorResult, fetchOptions);
|
|
1767
|
+
// Set the combined abort signal
|
|
1768
|
+
fetchOptions.signal = abortSignal.signal;
|
|
1769
|
+
// Execute fetch
|
|
1270
1770
|
let response;
|
|
1271
1771
|
try {
|
|
1272
1772
|
response = await fetch(url, fetchOptions);
|
|
1273
1773
|
}
|
|
1274
1774
|
catch (error) {
|
|
1275
|
-
// Check if
|
|
1276
|
-
if (error instanceof DOMException && error.name === "AbortError"
|
|
1277
|
-
|
|
1775
|
+
// Check if it's an abort error (DOMException in browsers, or AbortSignal abort)
|
|
1776
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
1777
|
+
if (abortSignal.wasTimeout()) {
|
|
1778
|
+
throw RequestError.timeout(url, method, this.requestOptions.timeout);
|
|
1779
|
+
}
|
|
1780
|
+
throw RequestError.abortError(url, method);
|
|
1278
1781
|
}
|
|
1279
|
-
//
|
|
1280
|
-
|
|
1782
|
+
// Check for Node.js/undici TimeoutError
|
|
1783
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
1784
|
+
const errorName = errorObj.name;
|
|
1785
|
+
const errorMessage = errorObj.message.toLowerCase();
|
|
1786
|
+
// Detect timeout errors from Node.js/undici (TimeoutError)
|
|
1787
|
+
const isTimeoutError = errorName === "TimeoutError" || errorMessage.includes("timeout") || errorMessage.includes("aborted due to timeout") || abortSignal.wasTimeout();
|
|
1788
|
+
if (isTimeoutError && this.requestOptions.timeout) {
|
|
1789
|
+
throw RequestError.timeout(url, method, this.requestOptions.timeout);
|
|
1790
|
+
}
|
|
1791
|
+
throw RequestError.networkError(url, method, errorObj);
|
|
1281
1792
|
}
|
|
1282
1793
|
if (!response.ok) {
|
|
1283
|
-
throw RequestError.fromResponse(response, url,
|
|
1284
|
-
}
|
|
1285
|
-
// Return a wrapped response instead of parsing it
|
|
1286
|
-
return new ResponseWrapper(response);
|
|
1287
|
-
}
|
|
1288
|
-
finally {
|
|
1289
|
-
if (timeoutId) {
|
|
1290
|
-
clearTimeout(timeoutId);
|
|
1794
|
+
throw RequestError.fromResponse(response, url, method);
|
|
1291
1795
|
}
|
|
1796
|
+
const graphQLOptions = this.getGraphQLOptions();
|
|
1797
|
+
const wrappedResponse = new ResponseWrapper(response, url, method, graphQLOptions);
|
|
1798
|
+
return await this.runResponseInterceptors(wrappedResponse);
|
|
1292
1799
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
* @param cacheKey The cache key for this request
|
|
1299
|
-
*/
|
|
1300
|
-
async executeWithCache(url, fetchOptions, cacheKey) {
|
|
1301
|
-
// Try to get from cache first
|
|
1302
|
-
if (this.cacheManager) {
|
|
1303
|
-
const cachedEntry = await this.cacheManager.get(cacheKey);
|
|
1304
|
-
if (cachedEntry) {
|
|
1305
|
-
// Create a synthetic Response object from cached data
|
|
1306
|
-
const cachedResponse = new Response(JSON.stringify(cachedEntry.value), {
|
|
1307
|
-
status: 200,
|
|
1308
|
-
headers: new Headers(cachedEntry.headers || {}),
|
|
1309
|
-
});
|
|
1310
|
-
return new ResponseWrapper(cachedResponse);
|
|
1800
|
+
catch (error) {
|
|
1801
|
+
// Convert to RequestError if needed
|
|
1802
|
+
let requestError;
|
|
1803
|
+
if (error instanceof RequestError) {
|
|
1804
|
+
requestError = error;
|
|
1311
1805
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
// Cache the successful response if caching is enabled
|
|
1316
|
-
// Only cache successful responses
|
|
1317
|
-
if (response.ok && this.cacheManager) {
|
|
1318
|
-
// Clone the response so we don't consume it
|
|
1319
|
-
try {
|
|
1320
|
-
const responseData = await response.getJson();
|
|
1321
|
-
// Extract headers to store in cache
|
|
1322
|
-
const headers = {};
|
|
1323
|
-
response.headers.forEach((value, key) => {
|
|
1324
|
-
headers[key] = value;
|
|
1325
|
-
});
|
|
1326
|
-
// Store in cache
|
|
1327
|
-
await this.cacheManager.set(cacheKey, responseData, headers);
|
|
1806
|
+
else {
|
|
1807
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
1808
|
+
requestError = RequestError.networkError(url, method, errorObj);
|
|
1328
1809
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1810
|
+
// Run error interceptors
|
|
1811
|
+
const interceptorResult = await this.runErrorInterceptors(requestError);
|
|
1812
|
+
// If error interceptor returned a ResponseWrapper, recover from error
|
|
1813
|
+
if (interceptorResult instanceof ResponseWrapper) {
|
|
1814
|
+
return interceptorResult;
|
|
1332
1815
|
}
|
|
1816
|
+
throw interceptorResult;
|
|
1817
|
+
}
|
|
1818
|
+
finally {
|
|
1819
|
+
abortSignal.cleanup();
|
|
1333
1820
|
}
|
|
1334
|
-
return response;
|
|
1335
1821
|
}
|
|
1336
1822
|
}
|
|
1337
1823
|
|
|
@@ -1339,6 +1825,10 @@ class BaseRequest {
|
|
|
1339
1825
|
* Base class for requests that can have a body (POST, PUT, PATCH)
|
|
1340
1826
|
*/
|
|
1341
1827
|
class BodyRequest extends BaseRequest {
|
|
1828
|
+
constructor(url) {
|
|
1829
|
+
super(url);
|
|
1830
|
+
this.graphQLOptions = undefined;
|
|
1831
|
+
}
|
|
1342
1832
|
/**
|
|
1343
1833
|
* Sets the body of the request
|
|
1344
1834
|
* @param body - The request body
|
|
@@ -1360,7 +1850,8 @@ class BodyRequest extends BaseRequest {
|
|
|
1360
1850
|
JSON.stringify(body);
|
|
1361
1851
|
}
|
|
1362
1852
|
catch (error) {
|
|
1363
|
-
|
|
1853
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1854
|
+
throw new RequestError(`JSON stringify failed: ${errorMessage}`, this.url, this.method);
|
|
1364
1855
|
}
|
|
1365
1856
|
}
|
|
1366
1857
|
else {
|
|
@@ -1368,18 +1859,93 @@ class BodyRequest extends BaseRequest {
|
|
|
1368
1859
|
}
|
|
1369
1860
|
return this;
|
|
1370
1861
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1862
|
+
/**
|
|
1863
|
+
* Sets a GraphQL query or mutation as the request body
|
|
1864
|
+
* Automatically formats the body as JSON and sets Content-Type to application/json
|
|
1865
|
+
*
|
|
1866
|
+
* @param query - The GraphQL query or mutation string
|
|
1867
|
+
* @param variables - Optional variables object to pass with the query
|
|
1868
|
+
* @param options - Optional GraphQL-specific options
|
|
1869
|
+
* @returns The request instance for chaining
|
|
1870
|
+
*
|
|
1871
|
+
* @example
|
|
1872
|
+
* const request = new PostRequest('/graphql')
|
|
1873
|
+
* .withGraphQL('query { user(id: $id) { name email } }', { id: '123' });
|
|
1874
|
+
*
|
|
1875
|
+
* @example
|
|
1876
|
+
* const request = new PostRequest('/graphql')
|
|
1877
|
+
* .withGraphQL('mutation { createUser(name: $name) { id } }', { name: 'John' });
|
|
1878
|
+
*
|
|
1879
|
+
* @example
|
|
1880
|
+
* // Throw an error if the GraphQL response contains errors
|
|
1881
|
+
* const request = new PostRequest('/graphql')
|
|
1882
|
+
* .withGraphQL('query { user { id } }', undefined, { throwOnError: true });
|
|
1883
|
+
*/
|
|
1884
|
+
withGraphQL(query, variables, options) {
|
|
1885
|
+
if (typeof query !== "string" || query.length === 0) {
|
|
1886
|
+
throw new RequestError("Invalid GraphQL query", this.url, this.method);
|
|
1887
|
+
}
|
|
1888
|
+
const graphQLBody = {
|
|
1889
|
+
query: query,
|
|
1890
|
+
};
|
|
1891
|
+
if (variables !== undefined) {
|
|
1892
|
+
if (typeof variables !== "object" || variables === null || Array.isArray(variables)) {
|
|
1893
|
+
throw new RequestError("Invalid GraphQL variables", this.url, this.method);
|
|
1894
|
+
}
|
|
1895
|
+
graphQLBody.variables = variables;
|
|
1896
|
+
}
|
|
1897
|
+
// Store GraphQL options if provided
|
|
1898
|
+
if (options !== undefined) {
|
|
1899
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
1900
|
+
throw new RequestError("Invalid GraphQL options", this.url, this.method);
|
|
1901
|
+
}
|
|
1902
|
+
// Store only the known GraphQL options properties
|
|
1903
|
+
const opts = options;
|
|
1904
|
+
this.graphQLOptions = {
|
|
1905
|
+
throwOnError: typeof opts.throwOnError === "boolean" ? opts.throwOnError : undefined,
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
// Validate JSON is stringifiable early
|
|
1909
|
+
try {
|
|
1910
|
+
JSON.stringify(graphQLBody);
|
|
1911
|
+
}
|
|
1912
|
+
catch (error) {
|
|
1913
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1914
|
+
throw new RequestError(`JSON stringify failed: ${errorMessage}`, this.url, this.method);
|
|
1915
|
+
}
|
|
1916
|
+
this.body = graphQLBody;
|
|
1917
|
+
this.bodyType = BodyType.JSON;
|
|
1918
|
+
this.setContentTypeIfNeeded("application/json");
|
|
1919
|
+
return this;
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Check if Content-Type header is already set (case-insensitive)
|
|
1923
|
+
*/
|
|
1924
|
+
hasContentType() {
|
|
1373
1925
|
const headers = this.requestOptions.headers;
|
|
1374
|
-
if (
|
|
1926
|
+
if (typeof headers === "object" && headers !== null) {
|
|
1927
|
+
const headersObj = headers;
|
|
1928
|
+
return Object.keys(headersObj).some(header => header.toLowerCase() === "content-type");
|
|
1929
|
+
}
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
setContentTypeIfNeeded(contentType) {
|
|
1933
|
+
if (!this.hasContentType()) {
|
|
1375
1934
|
this.withContentType(contentType);
|
|
1376
1935
|
}
|
|
1377
1936
|
}
|
|
1378
1937
|
/**
|
|
1379
|
-
*
|
|
1938
|
+
* Get the GraphQL options if set
|
|
1939
|
+
* @returns The GraphQL options or undefined
|
|
1940
|
+
*/
|
|
1941
|
+
getGraphQLOptions() {
|
|
1942
|
+
return this.graphQLOptions;
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Execute the request and return the ResponseWrapper
|
|
1380
1946
|
* Overrides the base implementation to add body handling
|
|
1381
1947
|
*/
|
|
1382
|
-
|
|
1948
|
+
async getResponse() {
|
|
1383
1949
|
if (this.body !== undefined) {
|
|
1384
1950
|
// Remove previous body if it exists
|
|
1385
1951
|
if (this.requestOptions.body)
|
|
@@ -1392,100 +1958,222 @@ class BodyRequest extends BaseRequest {
|
|
|
1392
1958
|
this.requestOptions.body = this.body;
|
|
1393
1959
|
}
|
|
1394
1960
|
}
|
|
1395
|
-
return super.
|
|
1961
|
+
return super.getResponse();
|
|
1396
1962
|
}
|
|
1397
1963
|
}
|
|
1398
1964
|
|
|
1399
|
-
|
|
1965
|
+
/**
|
|
1966
|
+
* HTTP GET request implementation
|
|
1967
|
+
* Used for retrieving data from an API endpoint.
|
|
1968
|
+
*
|
|
1969
|
+
* @example
|
|
1970
|
+
* const request = new GetRequest('/api/users')
|
|
1971
|
+
* .withQueryParams({ id: '123' });
|
|
1972
|
+
* const data = await request.getData();
|
|
1973
|
+
*/
|
|
1400
1974
|
class GetRequest extends BaseRequest {
|
|
1401
|
-
constructor() {
|
|
1402
|
-
super(
|
|
1975
|
+
constructor(url) {
|
|
1976
|
+
super(url);
|
|
1403
1977
|
this.method = HttpMethod.GET;
|
|
1404
1978
|
}
|
|
1405
1979
|
}
|
|
1980
|
+
/**
|
|
1981
|
+
* HTTP HEAD request implementation
|
|
1982
|
+
* Similar to GET but returns only headers without a response body.
|
|
1983
|
+
*
|
|
1984
|
+
* @example
|
|
1985
|
+
* const request = new HeadRequest('/api/users/123');
|
|
1986
|
+
* const response = await request.getResponse();
|
|
1987
|
+
*/
|
|
1406
1988
|
class HeadRequest extends BaseRequest {
|
|
1407
|
-
constructor() {
|
|
1408
|
-
super(
|
|
1989
|
+
constructor(url) {
|
|
1990
|
+
super(url);
|
|
1409
1991
|
this.method = HttpMethod.HEAD;
|
|
1410
1992
|
}
|
|
1411
1993
|
}
|
|
1994
|
+
/**
|
|
1995
|
+
* HTTP OPTIONS request implementation
|
|
1996
|
+
* Used to describe the communication options for the target resource.
|
|
1997
|
+
*
|
|
1998
|
+
* @example
|
|
1999
|
+
* const request = new OptionsRequest('/api/users');
|
|
2000
|
+
* const response = await request.getResponse();
|
|
2001
|
+
*/
|
|
1412
2002
|
class OptionsRequest extends BaseRequest {
|
|
1413
|
-
constructor() {
|
|
1414
|
-
super(
|
|
2003
|
+
constructor(url) {
|
|
2004
|
+
super(url);
|
|
1415
2005
|
this.method = HttpMethod.OPTIONS;
|
|
1416
2006
|
}
|
|
1417
2007
|
}
|
|
2008
|
+
/**
|
|
2009
|
+
* HTTP DELETE request implementation
|
|
2010
|
+
* Used to delete a resource on the server.
|
|
2011
|
+
*
|
|
2012
|
+
* @example
|
|
2013
|
+
* const request = new DeleteRequest('/api/users/123');
|
|
2014
|
+
* await request.getData();
|
|
2015
|
+
*/
|
|
1418
2016
|
class DeleteRequest extends BaseRequest {
|
|
1419
|
-
constructor() {
|
|
1420
|
-
super(
|
|
2017
|
+
constructor(url) {
|
|
2018
|
+
super(url);
|
|
1421
2019
|
this.method = HttpMethod.DELETE;
|
|
1422
2020
|
}
|
|
1423
2021
|
}
|
|
1424
|
-
|
|
2022
|
+
/**
|
|
2023
|
+
* HTTP POST request implementation
|
|
2024
|
+
* Used to create a new resource or submit data for processing.
|
|
2025
|
+
*
|
|
2026
|
+
* @example
|
|
2027
|
+
* const request = new PostRequest('/api/users')
|
|
2028
|
+
* .withJson({ name: 'John', email: 'john@example.com' });
|
|
2029
|
+
* const data = await request.getData();
|
|
2030
|
+
*/
|
|
1425
2031
|
class PostRequest extends BodyRequest {
|
|
1426
|
-
constructor() {
|
|
1427
|
-
super(
|
|
2032
|
+
constructor(url) {
|
|
2033
|
+
super(url);
|
|
1428
2034
|
this.method = HttpMethod.POST;
|
|
1429
2035
|
}
|
|
1430
2036
|
}
|
|
2037
|
+
/**
|
|
2038
|
+
* HTTP PUT request implementation
|
|
2039
|
+
* Used to replace or update an existing resource.
|
|
2040
|
+
*
|
|
2041
|
+
* @example
|
|
2042
|
+
* const request = new PutRequest('/api/users/123')
|
|
2043
|
+
* .withJson({ id: '123', name: 'John', email: 'john@example.com' });
|
|
2044
|
+
* const data = await request.getData();
|
|
2045
|
+
*/
|
|
1431
2046
|
class PutRequest extends BodyRequest {
|
|
1432
|
-
constructor() {
|
|
1433
|
-
super(
|
|
2047
|
+
constructor(url) {
|
|
2048
|
+
super(url);
|
|
1434
2049
|
this.method = HttpMethod.PUT;
|
|
1435
2050
|
}
|
|
1436
2051
|
}
|
|
2052
|
+
/**
|
|
2053
|
+
* HTTP PATCH request implementation
|
|
2054
|
+
* Used to apply partial modifications to a resource.
|
|
2055
|
+
*
|
|
2056
|
+
* @example
|
|
2057
|
+
* const request = new PatchRequest('/api/users/123')
|
|
2058
|
+
* .withJson({ email: 'new.email@example.com' });
|
|
2059
|
+
* const data = await request.getData();
|
|
2060
|
+
*/
|
|
1437
2061
|
class PatchRequest extends BodyRequest {
|
|
1438
|
-
constructor() {
|
|
1439
|
-
super(
|
|
2062
|
+
constructor(url) {
|
|
2063
|
+
super(url);
|
|
1440
2064
|
this.method = HttpMethod.PATCH;
|
|
1441
2065
|
}
|
|
1442
2066
|
}
|
|
1443
2067
|
|
|
1444
2068
|
/**
|
|
1445
2069
|
* Create a GET request
|
|
2070
|
+
* Used for retrieving resources from the server.
|
|
2071
|
+
*
|
|
2072
|
+
* @param url - The URL to send the request to
|
|
2073
|
+
* @returns A new GET request instance
|
|
2074
|
+
*
|
|
2075
|
+
* @example
|
|
2076
|
+
* const request = get('/api/users')
|
|
2077
|
+
* .withQueryParams({ limit: 10 });
|
|
2078
|
+
* const users = await request.getData();
|
|
1446
2079
|
*/
|
|
1447
|
-
function get() {
|
|
1448
|
-
return new GetRequest();
|
|
2080
|
+
function get(url) {
|
|
2081
|
+
return new GetRequest(url);
|
|
1449
2082
|
}
|
|
1450
2083
|
/**
|
|
1451
2084
|
* Create a POST request
|
|
2085
|
+
* Used for creating new resources or submitting data.
|
|
2086
|
+
*
|
|
2087
|
+
* @param url - The URL to send the request to
|
|
2088
|
+
* @returns A new POST request instance
|
|
2089
|
+
*
|
|
2090
|
+
* @example
|
|
2091
|
+
* const request = post('/api/users')
|
|
2092
|
+
* .withJson({ name: 'John', email: 'john@example.com' });
|
|
2093
|
+
* const newUser = await request.getData();
|
|
1452
2094
|
*/
|
|
1453
|
-
function post() {
|
|
1454
|
-
return new PostRequest();
|
|
2095
|
+
function post(url) {
|
|
2096
|
+
return new PostRequest(url);
|
|
1455
2097
|
}
|
|
1456
2098
|
/**
|
|
1457
2099
|
* Create a PUT request
|
|
2100
|
+
* Used for replacing or updating an existing resource.
|
|
2101
|
+
*
|
|
2102
|
+
* @param url - The URL to send the request to
|
|
2103
|
+
* @returns A new PUT request instance
|
|
2104
|
+
*
|
|
2105
|
+
* @example
|
|
2106
|
+
* const request = put('/api/users/123')
|
|
2107
|
+
* .withJson({ name: 'John Updated', email: 'john@example.com' });
|
|
2108
|
+
* const updatedUser = await request.getData();
|
|
1458
2109
|
*/
|
|
1459
|
-
function put() {
|
|
1460
|
-
return new PutRequest();
|
|
2110
|
+
function put(url) {
|
|
2111
|
+
return new PutRequest(url);
|
|
1461
2112
|
}
|
|
1462
2113
|
/**
|
|
1463
2114
|
* Create a DELETE request
|
|
1464
|
-
*
|
|
2115
|
+
* Used for removing resources from the server.
|
|
2116
|
+
*
|
|
2117
|
+
* @param url - The URL to send the request to
|
|
2118
|
+
* @returns A new DELETE request instance
|
|
2119
|
+
*
|
|
2120
|
+
* @example
|
|
2121
|
+
* const request = del('/api/users/123');
|
|
2122
|
+
* await request.getData();
|
|
1465
2123
|
*/
|
|
1466
|
-
function del() {
|
|
1467
|
-
return new DeleteRequest();
|
|
2124
|
+
function del(url) {
|
|
2125
|
+
return new DeleteRequest(url);
|
|
1468
2126
|
}
|
|
1469
2127
|
/**
|
|
1470
2128
|
* Create a PATCH request
|
|
2129
|
+
* Used for applying partial modifications to a resource.
|
|
2130
|
+
*
|
|
2131
|
+
* @param url - The URL to send the request to
|
|
2132
|
+
* @returns A new PATCH request instance
|
|
2133
|
+
*
|
|
2134
|
+
* @example
|
|
2135
|
+
* const request = patch('/api/users/123')
|
|
2136
|
+
* .withJson({ status: 'active' });
|
|
2137
|
+
* const patchedUser = await request.getData();
|
|
1471
2138
|
*/
|
|
1472
|
-
function patch() {
|
|
1473
|
-
return new PatchRequest();
|
|
2139
|
+
function patch(url) {
|
|
2140
|
+
return new PatchRequest(url);
|
|
1474
2141
|
}
|
|
1475
2142
|
/**
|
|
1476
2143
|
* Create a HEAD request
|
|
2144
|
+
* Similar to GET but returns only headers without a body.
|
|
2145
|
+
*
|
|
2146
|
+
* @param url - The URL to send the request to
|
|
2147
|
+
* @returns A new HEAD request instance
|
|
2148
|
+
*
|
|
2149
|
+
* @example
|
|
2150
|
+
* const request = head('/api/users/123');
|
|
2151
|
+
* const response = await request.getResponse();
|
|
2152
|
+
* console.log(response.headers.get('Last-Modified'));
|
|
1477
2153
|
*/
|
|
1478
|
-
function head() {
|
|
1479
|
-
return new HeadRequest();
|
|
2154
|
+
function head(url) {
|
|
2155
|
+
return new HeadRequest(url);
|
|
1480
2156
|
}
|
|
1481
2157
|
/**
|
|
1482
2158
|
* Create an OPTIONS request
|
|
2159
|
+
* Used to describe the communication options for a resource.
|
|
2160
|
+
*
|
|
2161
|
+
* @param url - The URL to send the request to
|
|
2162
|
+
* @returns A new OPTIONS request instance
|
|
2163
|
+
*
|
|
2164
|
+
* @example
|
|
2165
|
+
* const request = options('/api/users');
|
|
2166
|
+
* const response = await request.getResponse();
|
|
2167
|
+
* console.log(response.headers.get('Allow'));
|
|
1483
2168
|
*/
|
|
1484
|
-
function options() {
|
|
1485
|
-
return new OptionsRequest();
|
|
2169
|
+
function options(url) {
|
|
2170
|
+
return new OptionsRequest(url);
|
|
1486
2171
|
}
|
|
1487
2172
|
|
|
1488
|
-
|
|
2173
|
+
/**
|
|
2174
|
+
* Main API object for creating HTTP requests
|
|
2175
|
+
* Provides factory methods for all HTTP methods and access to global configuration.
|
|
2176
|
+
*/
|
|
1489
2177
|
const create = {
|
|
1490
2178
|
get,
|
|
1491
2179
|
post,
|
|
@@ -1494,8 +2182,8 @@ const create = {
|
|
|
1494
2182
|
patch,
|
|
1495
2183
|
head,
|
|
1496
2184
|
options,
|
|
1497
|
-
// Add configuration access
|
|
1498
2185
|
config: Config.getInstance(),
|
|
1499
2186
|
};
|
|
1500
2187
|
|
|
1501
|
-
export {
|
|
2188
|
+
export { CookieUtils, CredentialsPolicy, DeleteRequest, GetRequest, HeadRequest, HttpMethod, OptionsRequest, PatchRequest, PostRequest, PutRequest, RedirectMode, ReferrerPolicy, RequestError, RequestMode, RequestPriority, ResponseWrapper, SameSitePolicy, create as default };
|
|
2189
|
+
//# sourceMappingURL=index.esm.js.map
|