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