create-request 1.5.4 → 1.6.1
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 +82 -1
- package/dist/library/BaseRequest.d.ts +58 -27
- package/dist/library/BodyRequest.d.ts +6 -7
- package/dist/library/RequestError.d.ts +64 -3
- package/dist/library/ResponseWrapper.d.ts +19 -8
- package/dist/library/apiBuilder.d.ts +20 -1
- package/dist/library/enums.d.ts +67 -58
- package/dist/library/index.cjs +537 -589
- package/dist/library/index.cjs.map +1 -1
- package/dist/library/index.d.ts +1 -1
- package/dist/library/index.esm.js +529 -589
- package/dist/library/index.esm.js.map +1 -1
- package/dist/library/index.esm.min.js +1 -1
- package/dist/library/index.esm.min.js.map +1 -1
- package/dist/library/index.min.cjs +1 -1
- package/dist/library/index.min.cjs.map +1 -1
- package/dist/library/requestMethods.d.ts +7 -14
- package/dist/library/types.d.ts +15 -0
- package/dist/library/utils/Config.d.ts +11 -11
- package/package.json +4 -3
package/dist/library/index.cjs
CHANGED
|
@@ -3,98 +3,99 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Enum for HTTP methods
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
})(exports.HttpMethod || (exports.HttpMethod = {}));
|
|
6
|
+
const HttpMethod = {
|
|
7
|
+
GET: "GET",
|
|
8
|
+
PUT: "PUT",
|
|
9
|
+
POST: "POST",
|
|
10
|
+
HEAD: "HEAD",
|
|
11
|
+
PATCH: "PATCH",
|
|
12
|
+
DELETE: "DELETE",
|
|
13
|
+
OPTIONS: "OPTIONS",
|
|
14
|
+
};
|
|
16
15
|
/**
|
|
17
16
|
* Enum for request priorities
|
|
18
17
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})(exports.RequestPriority || (exports.RequestPriority = {}));
|
|
18
|
+
const RequestPriority = {
|
|
19
|
+
LOW: "low",
|
|
20
|
+
HIGH: "high",
|
|
21
|
+
AUTO: "auto",
|
|
22
|
+
};
|
|
25
23
|
/**
|
|
26
24
|
* Enum for credentials policies
|
|
27
25
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})(exports.CredentialsPolicy || (exports.CredentialsPolicy = {}));
|
|
26
|
+
const CredentialsPolicy = {
|
|
27
|
+
OMIT: "omit",
|
|
28
|
+
INCLUDE: "include",
|
|
29
|
+
SAME_ORIGIN: "same-origin",
|
|
30
|
+
};
|
|
34
31
|
/**
|
|
35
32
|
* Enum for request modes
|
|
36
33
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
})(exports.RequestMode || (exports.RequestMode = {}));
|
|
34
|
+
const RequestMode = {
|
|
35
|
+
CORS: "cors",
|
|
36
|
+
NO_CORS: "no-cors",
|
|
37
|
+
SAME_ORIGIN: "same-origin",
|
|
38
|
+
NAVIGATE: "navigate",
|
|
39
|
+
};
|
|
44
40
|
/**
|
|
45
41
|
* Enum for redirect modes
|
|
46
42
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
})(exports.RedirectMode || (exports.RedirectMode = {}));
|
|
43
|
+
const RedirectMode = {
|
|
44
|
+
ERROR: "error",
|
|
45
|
+
FOLLOW: "follow",
|
|
46
|
+
MANUAL: "manual",
|
|
47
|
+
};
|
|
53
48
|
/**
|
|
54
49
|
* Enum for cookie SameSite policies
|
|
55
50
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})(exports.SameSitePolicy || (exports.SameSitePolicy = {}));
|
|
51
|
+
const SameSitePolicy = {
|
|
52
|
+
LAX: "Lax",
|
|
53
|
+
NONE: "None",
|
|
54
|
+
STRICT: "Strict",
|
|
55
|
+
};
|
|
62
56
|
/**
|
|
63
57
|
* Enum for body types
|
|
64
58
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
})(BodyType || (BodyType = {}));
|
|
59
|
+
const BodyType = {
|
|
60
|
+
JSON: "json",
|
|
61
|
+
STRING: "string",
|
|
62
|
+
BINARY: "binary",
|
|
63
|
+
};
|
|
71
64
|
/**
|
|
72
65
|
* Referrer policies for fetch requests
|
|
73
66
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
})(exports.ReferrerPolicy || (exports.ReferrerPolicy = {}));
|
|
67
|
+
const ReferrerPolicy = {
|
|
68
|
+
ORIGIN: "origin",
|
|
69
|
+
UNSAFE_URL: "unsafe-url",
|
|
70
|
+
SAME_ORIGIN: "same-origin",
|
|
71
|
+
NO_REFERRER: "no-referrer",
|
|
72
|
+
STRICT_ORIGIN: "strict-origin",
|
|
73
|
+
ORIGIN_WHEN_CROSS_ORIGIN: "origin-when-cross-origin",
|
|
74
|
+
NO_REFERRER_WHEN_DOWNGRADE: "no-referrer-when-downgrade",
|
|
75
|
+
STRICT_ORIGIN_WHEN_CROSS_ORIGIN: "strict-origin-when-cross-origin",
|
|
76
|
+
};
|
|
85
77
|
/**
|
|
86
78
|
* Cache modes for fetch requests
|
|
87
79
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
})(exports.CacheMode || (exports.CacheMode = {}));
|
|
80
|
+
const CacheMode = {
|
|
81
|
+
RELOAD: "reload",
|
|
82
|
+
DEFAULT: "default",
|
|
83
|
+
NO_CACHE: "no-cache",
|
|
84
|
+
NO_STORE: "no-store",
|
|
85
|
+
FORCE_CACHE: "force-cache",
|
|
86
|
+
ONLY_IF_CACHED: "only-if-cached",
|
|
87
|
+
};
|
|
97
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Extract a message from an unknown thrown value
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
const errorMessage = (e) => (e instanceof Error ? e.message : String(e));
|
|
94
|
+
/**
|
|
95
|
+
* Coerce an unknown thrown value to an Error
|
|
96
|
+
* @internal
|
|
97
|
+
*/
|
|
98
|
+
const toError = (e) => (e instanceof Error ? e : new Error(String(e)));
|
|
98
99
|
/**
|
|
99
100
|
* Error class for HTTP request failures.
|
|
100
101
|
* Extends the standard Error class with additional context about the failed request.
|
|
@@ -108,6 +109,8 @@ exports.CacheMode = void 0;
|
|
|
108
109
|
* console.log(`URL: ${error.url}`);
|
|
109
110
|
* console.log(`Method: ${error.method}`);
|
|
110
111
|
* console.log(`Status: ${error.status}`);
|
|
112
|
+
* console.log(`Body: ${error.body}`); // Raw response body (if available)
|
|
113
|
+
* console.log(error.getJson()); // Body parsed as JSON (or undefined)
|
|
111
114
|
* console.log(`Is timeout: ${error.isTimeout}`);
|
|
112
115
|
* console.log(`Is aborted: ${error.isAborted}`);
|
|
113
116
|
* }
|
|
@@ -118,6 +121,12 @@ class RequestError extends Error {
|
|
|
118
121
|
status;
|
|
119
122
|
/** The Response object if the request received a response before failing */
|
|
120
123
|
response;
|
|
124
|
+
/**
|
|
125
|
+
* The raw response body as text, if a response was received and its body could be read.
|
|
126
|
+
* `undefined` for errors without a response (network errors, timeouts, aborts)
|
|
127
|
+
* or when the body could not be read.
|
|
128
|
+
*/
|
|
129
|
+
body;
|
|
121
130
|
/** The URL that was requested */
|
|
122
131
|
url;
|
|
123
132
|
/** The HTTP method that was used (e.g., 'GET', 'POST') */
|
|
@@ -126,6 +135,8 @@ class RequestError extends Error {
|
|
|
126
135
|
isTimeout;
|
|
127
136
|
/** Whether the request was aborted (cancelled) */
|
|
128
137
|
isAborted;
|
|
138
|
+
/** Cached result of parsing `body` as JSON (lazily populated by getJson) */
|
|
139
|
+
_parsed;
|
|
129
140
|
/**
|
|
130
141
|
* Creates a new RequestError instance.
|
|
131
142
|
*
|
|
@@ -135,6 +146,7 @@ class RequestError extends Error {
|
|
|
135
146
|
* @param options - Additional error context
|
|
136
147
|
* @param options.status - HTTP status code if available
|
|
137
148
|
* @param options.response - The Response object if available
|
|
149
|
+
* @param options.body - The raw response body as text, if available
|
|
138
150
|
* @param options.isTimeout - Whether this was a timeout error
|
|
139
151
|
* @param options.isAborted - Whether the request was aborted
|
|
140
152
|
* @param options.cause - The underlying error that caused this error
|
|
@@ -146,6 +158,7 @@ class RequestError extends Error {
|
|
|
146
158
|
this.method = method;
|
|
147
159
|
this.status = options.status;
|
|
148
160
|
this.response = options.response;
|
|
161
|
+
this.body = options.body;
|
|
149
162
|
this.isTimeout = !!options.isTimeout;
|
|
150
163
|
this.isAborted = !!options.isAborted;
|
|
151
164
|
// For better stack traces in modern environments
|
|
@@ -155,6 +168,62 @@ class RequestError extends Error {
|
|
|
155
168
|
// Maintains proper prototype chain for instanceof checks
|
|
156
169
|
Object.setPrototypeOf(this, RequestError.prototype);
|
|
157
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Parses the captured response body (`body`) as JSON.
|
|
173
|
+
* The result is cached, so repeated calls don't re-parse.
|
|
174
|
+
* This method never throws - it returns `undefined` when there is no body
|
|
175
|
+
* or the body is not valid JSON, making it safe to use in error handlers.
|
|
176
|
+
*
|
|
177
|
+
* @returns The parsed JSON body, or `undefined` if no body was captured or it isn't valid JSON
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* try {
|
|
182
|
+
* await create.post('/api/users').withBody(user).getJson();
|
|
183
|
+
* } catch (error) {
|
|
184
|
+
* if (error instanceof RequestError) {
|
|
185
|
+
* const details = error.getJson<{ message: string; code: string }>();
|
|
186
|
+
* console.log(details?.message ?? error.body ?? error.message);
|
|
187
|
+
* }
|
|
188
|
+
* }
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
getJson() {
|
|
192
|
+
if (this._parsed === undefined && this.body) {
|
|
193
|
+
try {
|
|
194
|
+
this._parsed = JSON.parse(this.body);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Body is not valid JSON - leave parsedBody undefined
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return this._parsed;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Safely reads the body of a Response as text without consuming it.
|
|
204
|
+
* The response is cloned before reading, so the original body remains readable.
|
|
205
|
+
* Never throws - returns `undefined` if the body is unavailable or cannot be read
|
|
206
|
+
* (e.g., already consumed, locked stream, or read failure).
|
|
207
|
+
*
|
|
208
|
+
* @param response - The Response to read the body from
|
|
209
|
+
* @returns The body as text, or `undefined` if it could not be read
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* const body = await RequestError.captureBody(response);
|
|
214
|
+
* throw RequestError.fromResponse(response, url, 'GET', body);
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
static async captureBody(response) {
|
|
218
|
+
try {
|
|
219
|
+
if (!response.bodyUsed)
|
|
220
|
+
return await response.clone().text();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Body could not be read (e.g., locked stream or read failure)
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
158
227
|
/**
|
|
159
228
|
* Creates a RequestError for a timeout failure.
|
|
160
229
|
*
|
|
@@ -180,20 +249,23 @@ class RequestError extends Error {
|
|
|
180
249
|
* @param response - The Response object from the failed request
|
|
181
250
|
* @param url - The URL that was requested
|
|
182
251
|
* @param method - The HTTP method that was used
|
|
183
|
-
* @
|
|
252
|
+
* @param body - The response body as text, if already read (see {@link RequestError.captureBody})
|
|
253
|
+
* @returns A RequestError with the status code, response object, and body (if provided)
|
|
184
254
|
*
|
|
185
255
|
* @example
|
|
186
256
|
* ```typescript
|
|
187
257
|
* const response = await fetch('/api/users');
|
|
188
258
|
* if (!response.ok) {
|
|
189
|
-
*
|
|
259
|
+
* const body = await RequestError.captureBody(response);
|
|
260
|
+
* throw RequestError.fromResponse(response, '/api/users', 'GET', body);
|
|
190
261
|
* }
|
|
191
262
|
* ```
|
|
192
263
|
*/
|
|
193
|
-
static fromResponse(response, url, method) {
|
|
264
|
+
static fromResponse(response, url, method, body) {
|
|
194
265
|
return new RequestError(`HTTP ${response.status}`, url, method, {
|
|
195
266
|
status: response.status,
|
|
196
267
|
response,
|
|
268
|
+
body,
|
|
197
269
|
});
|
|
198
270
|
}
|
|
199
271
|
/**
|
|
@@ -221,55 +293,29 @@ class RequestError extends Error {
|
|
|
221
293
|
let message = originalError.message;
|
|
222
294
|
// Check for Node.js error codes (e.g., from undici/dns errors)
|
|
223
295
|
const errorCode = originalError.code;
|
|
224
|
-
const errorName = originalError.name;
|
|
225
296
|
const stack = originalError.stack || "";
|
|
226
|
-
const errorMessageLower = message.toLowerCase();
|
|
227
297
|
// Check for timeout errors (Node.js/undici TimeoutError)
|
|
228
298
|
// Note: While explicit timeouts set via withTimeout() are handled in BaseRequest,
|
|
229
|
-
// this detection serves as a safety net for
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const isTimeoutError = errorName === "TimeoutError" ||
|
|
234
|
-
errorMessageLower.includes("timeout") ||
|
|
235
|
-
errorMessageLower.includes("aborted due to timeout") ||
|
|
299
|
+
// this detection serves as a safety net for timeout errors from external
|
|
300
|
+
// AbortControllers, other runtimes, and network-level timeouts (ETIMEDOUT).
|
|
301
|
+
const isTimeoutError = originalError.name === "TimeoutError" ||
|
|
302
|
+
message.toLowerCase().includes("timeout") ||
|
|
236
303
|
errorCode === "ETIMEDOUT" ||
|
|
237
304
|
stack.includes("TimeoutError") ||
|
|
238
305
|
stack.includes("timeout");
|
|
239
306
|
// If the error message is generic "fetch failed", provide more context
|
|
240
307
|
if (message === "fetch failed" || message === "Failed to fetch") {
|
|
241
308
|
// Check for DNS resolution errors
|
|
242
|
-
const isDnsError = errorCode === "ENOTFOUND" ||
|
|
243
|
-
errorCode === "EAI_AGAIN" ||
|
|
244
|
-
errorCode === "EAI_NODATA" ||
|
|
245
|
-
stack.includes("getaddrinfo") ||
|
|
246
|
-
stack.includes("ENOTFOUND") ||
|
|
247
|
-
stack.includes("EAI_AGAIN");
|
|
309
|
+
const isDnsError = errorCode === "ENOTFOUND" || errorCode === "EAI_AGAIN" || errorCode === "EAI_NODATA" || /getaddrinfo|ENOTFOUND|EAI_AGAIN/.test(stack);
|
|
248
310
|
// Check for connection errors (but not timeout errors)
|
|
249
311
|
const isConnectionError = !isTimeoutError && (errorCode === "ECONNREFUSED" || errorCode === "ECONNRESET" || stack.includes("ECONNREFUSED") || stack.includes("connect"));
|
|
250
|
-
|
|
251
|
-
message = `Timeout:${url}`;
|
|
252
|
-
}
|
|
253
|
-
else if (isDnsError) {
|
|
254
|
-
message = `DNS:${url}`;
|
|
255
|
-
}
|
|
256
|
-
else if (isConnectionError) {
|
|
257
|
-
message = `Conn:${url}`;
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
message = `Net:${url}`;
|
|
261
|
-
}
|
|
312
|
+
message = (isTimeoutError ? "Timeout:" : isDnsError ? "DNS:" : isConnectionError ? "Conn:" : "Net:") + url;
|
|
262
313
|
}
|
|
263
|
-
const error = new RequestError(message, url, method, {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// Create a proper RequestError stack trace, but append the original stack for debugging
|
|
267
|
-
// This way Node.js will show "RequestError: ..." instead of "TypeError: ..."
|
|
314
|
+
const error = new RequestError(message, url, method, isTimeoutError ? { isTimeout: true } : {});
|
|
315
|
+
// Create a proper RequestError stack trace, but append the original stack for
|
|
316
|
+
// debugging context, so Node.js shows "RequestError: ..." instead of "TypeError: ..."
|
|
268
317
|
if (originalError.stack) {
|
|
269
|
-
|
|
270
|
-
const currentStack = error.stack || "";
|
|
271
|
-
// Append the original error's stack as "Caused by:" for debugging context
|
|
272
|
-
error.stack = `${currentStack}\n\nCaused by: ${originalError.stack}`;
|
|
318
|
+
error.stack = `${error.stack || ""}\n\nCaused by: ${originalError.stack}`;
|
|
273
319
|
}
|
|
274
320
|
return error;
|
|
275
321
|
}
|
|
@@ -313,19 +359,19 @@ class ResponseWrapper {
|
|
|
313
359
|
url;
|
|
314
360
|
/** The HTTP method that was used (if available) */
|
|
315
361
|
method;
|
|
316
|
-
|
|
317
|
-
|
|
362
|
+
_res;
|
|
363
|
+
_gqlOpts;
|
|
318
364
|
// Cache the body as the last used method
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
365
|
+
_blob;
|
|
366
|
+
_text;
|
|
367
|
+
_json;
|
|
368
|
+
_buf;
|
|
323
369
|
constructor(response, url, method, graphQLOptions) {
|
|
324
|
-
this.
|
|
370
|
+
this._res = response;
|
|
325
371
|
this.url = url;
|
|
326
372
|
this.method = method;
|
|
327
373
|
if (graphQLOptions) {
|
|
328
|
-
this.
|
|
374
|
+
this._gqlOpts = {
|
|
329
375
|
throwOnError: graphQLOptions.throwOnError,
|
|
330
376
|
};
|
|
331
377
|
}
|
|
@@ -334,43 +380,65 @@ class ResponseWrapper {
|
|
|
334
380
|
* HTTP status code (e.g., 200, 404, 500)
|
|
335
381
|
*/
|
|
336
382
|
get status() {
|
|
337
|
-
return this.
|
|
383
|
+
return this._res.status;
|
|
338
384
|
}
|
|
339
385
|
/**
|
|
340
386
|
* HTTP status text (e.g., "OK", "Not Found", "Internal Server Error")
|
|
341
387
|
*/
|
|
342
388
|
get statusText() {
|
|
343
|
-
return this.
|
|
389
|
+
return this._res.statusText;
|
|
344
390
|
}
|
|
345
391
|
/**
|
|
346
392
|
* Response headers as a Headers object
|
|
347
393
|
*/
|
|
348
394
|
get headers() {
|
|
349
|
-
return this.
|
|
395
|
+
return this._res.headers;
|
|
350
396
|
}
|
|
351
397
|
/**
|
|
352
398
|
* Whether the response status is in the 200-299 range (successful)
|
|
353
399
|
*/
|
|
354
400
|
get ok() {
|
|
355
|
-
return this.
|
|
401
|
+
return this._res.ok;
|
|
356
402
|
}
|
|
357
403
|
/**
|
|
358
404
|
* The raw Response object from the fetch API.
|
|
359
405
|
* Use this if you need direct access to the underlying Response.
|
|
360
406
|
*/
|
|
361
407
|
get raw() {
|
|
362
|
-
return this.
|
|
408
|
+
return this._res;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Create a RequestError carrying this response's context
|
|
412
|
+
* @param message - The error message
|
|
413
|
+
* @param withBody - Whether to attach the cached body text to the error
|
|
414
|
+
*/
|
|
415
|
+
_err(message, withBody) {
|
|
416
|
+
return new RequestError(message, this.url || "", this.method || "", {
|
|
417
|
+
status: this._res.status,
|
|
418
|
+
response: this._res,
|
|
419
|
+
body: withBody ? this._text : undefined,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Read the response body via the given reader, wrapping failures in a RequestError
|
|
424
|
+
* @throws RequestError if the body has already been consumed or reading fails
|
|
425
|
+
*/
|
|
426
|
+
async _read(reader) {
|
|
427
|
+
this._checkUsed();
|
|
428
|
+
try {
|
|
429
|
+
return await reader();
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
throw this._err(`Read: ${errorMessage(e)}`);
|
|
433
|
+
}
|
|
363
434
|
}
|
|
364
435
|
/**
|
|
365
436
|
* Check if the response body has already been consumed and throw an error if so
|
|
366
437
|
* @throws RequestError if the body has already been consumed
|
|
367
438
|
*/
|
|
368
|
-
|
|
369
|
-
if (this.
|
|
370
|
-
throw
|
|
371
|
-
status: this.response.status,
|
|
372
|
-
response: this.response,
|
|
373
|
-
});
|
|
439
|
+
_checkUsed() {
|
|
440
|
+
if (this._res.bodyUsed) {
|
|
441
|
+
throw this._err("Body used");
|
|
374
442
|
}
|
|
375
443
|
}
|
|
376
444
|
/**
|
|
@@ -378,8 +446,8 @@ class ResponseWrapper {
|
|
|
378
446
|
* @param data - The parsed JSON data
|
|
379
447
|
* @throws RequestError if GraphQL response contains errors and throwOnError is enabled
|
|
380
448
|
*/
|
|
381
|
-
|
|
382
|
-
if (!this.
|
|
449
|
+
_checkGql(data) {
|
|
450
|
+
if (!this._gqlOpts?.throwOnError || typeof data !== "object" || data === null)
|
|
383
451
|
return;
|
|
384
452
|
const responseData = data;
|
|
385
453
|
if (!Array.isArray(responseData.errors) || responseData.errors.length === 0)
|
|
@@ -408,11 +476,7 @@ class ResponseWrapper {
|
|
|
408
476
|
}
|
|
409
477
|
return String(x);
|
|
410
478
|
});
|
|
411
|
-
|
|
412
|
-
throw new RequestError(`GQL: ${errorMessage}`, this.url || "", this.method || "", {
|
|
413
|
-
status: this.response.status,
|
|
414
|
-
response: this.response,
|
|
415
|
-
});
|
|
479
|
+
throw this._err(`GQL: ${errorMessages.join(", ")}`, true);
|
|
416
480
|
}
|
|
417
481
|
/**
|
|
418
482
|
* Parse the response body as JSON
|
|
@@ -441,38 +505,35 @@ class ResponseWrapper {
|
|
|
441
505
|
* }
|
|
442
506
|
*/
|
|
443
507
|
async getJson() {
|
|
444
|
-
if (this.
|
|
445
|
-
return this.
|
|
508
|
+
if (this._json !== undefined)
|
|
509
|
+
return this._json;
|
|
446
510
|
// Handle empty responses: 204 No Content or content-length: 0
|
|
447
|
-
const contentLength = this.
|
|
448
|
-
if (this.
|
|
449
|
-
this.
|
|
511
|
+
const contentLength = this._res.headers.get("content-length");
|
|
512
|
+
if (this._res.status === 204 || contentLength === "0") {
|
|
513
|
+
this._json = null;
|
|
450
514
|
return null;
|
|
451
515
|
}
|
|
452
|
-
this.
|
|
516
|
+
this._checkUsed();
|
|
453
517
|
try {
|
|
454
518
|
// Read as text first to handle empty bodies and cache for getText()
|
|
455
|
-
const text = await this.
|
|
456
|
-
this.
|
|
519
|
+
const text = await this._res.text();
|
|
520
|
+
this._text = text;
|
|
457
521
|
// Handle empty or whitespace-only responses
|
|
458
522
|
if (!text || text.trim() === "") {
|
|
459
|
-
this.
|
|
523
|
+
this._json = null;
|
|
460
524
|
return null;
|
|
461
525
|
}
|
|
462
526
|
// Parse the text as JSON
|
|
463
527
|
const parsed = JSON.parse(text);
|
|
464
|
-
this.
|
|
465
|
-
this.
|
|
528
|
+
this._json = parsed;
|
|
529
|
+
this._checkGql(parsed);
|
|
466
530
|
return parsed;
|
|
467
531
|
}
|
|
468
532
|
catch (error) {
|
|
469
533
|
if (error instanceof RequestError) {
|
|
470
534
|
throw error;
|
|
471
535
|
}
|
|
472
|
-
throw
|
|
473
|
-
status: this.response.status,
|
|
474
|
-
response: this.response,
|
|
475
|
-
});
|
|
536
|
+
throw this._err(`Bad JSON: ${errorMessage(error)}`, true);
|
|
476
537
|
}
|
|
477
538
|
}
|
|
478
539
|
/**
|
|
@@ -489,20 +550,9 @@ class ResponseWrapper {
|
|
|
489
550
|
* ```
|
|
490
551
|
*/
|
|
491
552
|
async getText() {
|
|
492
|
-
if (this.
|
|
493
|
-
return this.
|
|
494
|
-
this.
|
|
495
|
-
try {
|
|
496
|
-
const text = await this.response.text();
|
|
497
|
-
this.cachedText = text;
|
|
498
|
-
return text;
|
|
499
|
-
}
|
|
500
|
-
catch (e) {
|
|
501
|
-
throw new RequestError(`Read: ${e instanceof Error ? e.message : String(e)}`, this.url || "", this.method || "", {
|
|
502
|
-
status: this.response.status,
|
|
503
|
-
response: this.response,
|
|
504
|
-
});
|
|
505
|
-
}
|
|
553
|
+
if (this._text !== undefined)
|
|
554
|
+
return this._text;
|
|
555
|
+
return (this._text = await this._read(() => this._res.text()));
|
|
506
556
|
}
|
|
507
557
|
/**
|
|
508
558
|
* Get the response body as a Blob.
|
|
@@ -520,20 +570,9 @@ class ResponseWrapper {
|
|
|
520
570
|
* ```
|
|
521
571
|
*/
|
|
522
572
|
async getBlob() {
|
|
523
|
-
if (this.
|
|
524
|
-
return this.
|
|
525
|
-
this.
|
|
526
|
-
try {
|
|
527
|
-
const blob = await this.response.blob();
|
|
528
|
-
this.cachedBlob = blob;
|
|
529
|
-
return blob;
|
|
530
|
-
}
|
|
531
|
-
catch (e) {
|
|
532
|
-
throw new RequestError(`Read: ${e instanceof Error ? e.message : String(e)}`, this.url || "", this.method || "", {
|
|
533
|
-
status: this.response.status,
|
|
534
|
-
response: this.response,
|
|
535
|
-
});
|
|
536
|
-
}
|
|
573
|
+
if (this._blob !== undefined)
|
|
574
|
+
return this._blob;
|
|
575
|
+
return (this._blob = await this._read(() => this._res.blob()));
|
|
537
576
|
}
|
|
538
577
|
/**
|
|
539
578
|
* Get the response body as an ArrayBuffer.
|
|
@@ -551,21 +590,10 @@ class ResponseWrapper {
|
|
|
551
590
|
* ```
|
|
552
591
|
*/
|
|
553
592
|
async getArrayBuffer() {
|
|
554
|
-
if (this.
|
|
555
|
-
return this.
|
|
556
|
-
}
|
|
557
|
-
this.checkBodyNotConsumed();
|
|
558
|
-
try {
|
|
559
|
-
const arrayBuffer = await this.response.arrayBuffer();
|
|
560
|
-
this.cachedArrayBuffer = arrayBuffer;
|
|
561
|
-
return arrayBuffer;
|
|
562
|
-
}
|
|
563
|
-
catch (e) {
|
|
564
|
-
throw new RequestError(`Read: ${e instanceof Error ? e.message : String(e)}`, this.url || "", this.method || "", {
|
|
565
|
-
status: this.response.status,
|
|
566
|
-
response: this.response,
|
|
567
|
-
});
|
|
593
|
+
if (this._buf !== undefined) {
|
|
594
|
+
return this._buf;
|
|
568
595
|
}
|
|
596
|
+
return (this._buf = await this._read(() => this._res.arrayBuffer()));
|
|
569
597
|
}
|
|
570
598
|
/**
|
|
571
599
|
* Get the raw response body as a ReadableStream
|
|
@@ -583,8 +611,8 @@ class ResponseWrapper {
|
|
|
583
611
|
* }
|
|
584
612
|
*/
|
|
585
613
|
getBody() {
|
|
586
|
-
this.
|
|
587
|
-
return this.
|
|
614
|
+
this._checkUsed();
|
|
615
|
+
return this._res.body;
|
|
588
616
|
}
|
|
589
617
|
/**
|
|
590
618
|
* Extract specific data using a selector function
|
|
@@ -634,15 +662,11 @@ class ResponseWrapper {
|
|
|
634
662
|
}
|
|
635
663
|
// Enhance selector errors with context
|
|
636
664
|
if (selector) {
|
|
637
|
-
throw
|
|
638
|
-
status: this.response.status,
|
|
639
|
-
response: this.response,
|
|
640
|
-
});
|
|
665
|
+
throw this._err(`Selector: ${errorMessage(error)}`, true);
|
|
641
666
|
}
|
|
642
667
|
// If we get here and it's not a RequestError, wrap it
|
|
643
668
|
// This should rarely happen as getJson() should throw RequestError
|
|
644
|
-
|
|
645
|
-
throw RequestError.networkError(this.url || "", this.method || "", errorObj);
|
|
669
|
+
throw RequestError.networkError(this.url || "", this.method || "", toError(error));
|
|
646
670
|
}
|
|
647
671
|
}
|
|
648
672
|
}
|
|
@@ -654,20 +678,9 @@ class CookieUtils {
|
|
|
654
678
|
* @returns Formatted cookie string for the Cookie header
|
|
655
679
|
*/
|
|
656
680
|
static formatRequestCookies(cookies) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (typeof valueOrOptions === "string") {
|
|
661
|
-
value = valueOrOptions;
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
// Extract value from options object without validation
|
|
665
|
-
value = valueOrOptions.value;
|
|
666
|
-
}
|
|
667
|
-
// Add the cookie to the request
|
|
668
|
-
cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
|
|
669
|
-
});
|
|
670
|
-
return cookiePairs.join("; ");
|
|
681
|
+
return Object.entries(cookies)
|
|
682
|
+
.map(([name, valueOrOptions]) => `${encodeURIComponent(name)}=${encodeURIComponent(typeof valueOrOptions === "string" ? valueOrOptions : valueOrOptions.value)}`)
|
|
683
|
+
.join("; ");
|
|
671
684
|
}
|
|
672
685
|
}
|
|
673
686
|
|
|
@@ -721,17 +734,11 @@ class CsrfUtils {
|
|
|
721
734
|
// If token is longer than 10 chars, perform additional security checks
|
|
722
735
|
if (token.length > 10) {
|
|
723
736
|
// Check for valid character set (alphanumeric & common token symbols)
|
|
724
|
-
|
|
725
|
-
if (!validTokenRegex.test(token)) {
|
|
737
|
+
if (!/^[A-Za-z0-9\-_=+/.]+$/.test(token)) {
|
|
726
738
|
return false;
|
|
727
739
|
}
|
|
728
740
|
// For longer tokens, check for sufficient entropy (at least 2 character types)
|
|
729
|
-
|
|
730
|
-
const hasLowerCase = /[a-z]/.test(token);
|
|
731
|
-
const hasNumbers = /[0-9]/.test(token);
|
|
732
|
-
const hasSpecials = /[-_=+/.]/.test(token);
|
|
733
|
-
const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
|
|
734
|
-
return characterTypesCount >= 2;
|
|
741
|
+
return [/[A-Z]/, /[a-z]/, /[0-9]/, /[-_=+/.]/].filter(re => re.test(token)).length >= 2;
|
|
735
742
|
}
|
|
736
743
|
// For shorter tokens (8-10 chars), just return true if we reached here
|
|
737
744
|
return true;
|
|
@@ -742,19 +749,19 @@ class CsrfUtils {
|
|
|
742
749
|
* Global configuration for create-request
|
|
743
750
|
*/
|
|
744
751
|
class Config {
|
|
745
|
-
static
|
|
752
|
+
static _instance;
|
|
746
753
|
// CSRF configuration
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
754
|
+
_csrfHeader = "X-CSRF-Token";
|
|
755
|
+
_xsrfCookie = "XSRF-TOKEN";
|
|
756
|
+
_xsrfHeader = "X-XSRF-TOKEN";
|
|
757
|
+
_csrfToken = null;
|
|
758
|
+
_autoXsrf = true;
|
|
759
|
+
_antiCsrf = true; // X-Requested-With header
|
|
753
760
|
// Interceptor configuration
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
761
|
+
_reqI = [];
|
|
762
|
+
_resI = [];
|
|
763
|
+
_errI = [];
|
|
764
|
+
_nextId = 1;
|
|
758
765
|
constructor() { }
|
|
759
766
|
/**
|
|
760
767
|
* Get the singleton instance of the Config class
|
|
@@ -766,10 +773,10 @@ class Config {
|
|
|
766
773
|
* config.setCsrfToken('token123');
|
|
767
774
|
*/
|
|
768
775
|
static getInstance() {
|
|
769
|
-
if (!Config.
|
|
770
|
-
Config.
|
|
776
|
+
if (!Config._instance) {
|
|
777
|
+
Config._instance = new Config();
|
|
771
778
|
}
|
|
772
|
-
return Config.
|
|
779
|
+
return Config._instance;
|
|
773
780
|
}
|
|
774
781
|
/**
|
|
775
782
|
* Set a global CSRF token to be used for all requests
|
|
@@ -781,7 +788,7 @@ class Config {
|
|
|
781
788
|
* Config.getInstance().setCsrfToken('myToken123');
|
|
782
789
|
*/
|
|
783
790
|
setCsrfToken(token) {
|
|
784
|
-
this.
|
|
791
|
+
this._csrfToken = token;
|
|
785
792
|
return this;
|
|
786
793
|
}
|
|
787
794
|
/**
|
|
@@ -790,7 +797,7 @@ class Config {
|
|
|
790
797
|
* @returns The current CSRF token or null if not set
|
|
791
798
|
*/
|
|
792
799
|
getCsrfToken() {
|
|
793
|
-
return this.
|
|
800
|
+
return this._csrfToken;
|
|
794
801
|
}
|
|
795
802
|
/**
|
|
796
803
|
* Set the CSRF header name used when sending the token
|
|
@@ -802,7 +809,7 @@ class Config {
|
|
|
802
809
|
* Config.getInstance().setCsrfHeaderName('X-My-CSRF-Token');
|
|
803
810
|
*/
|
|
804
811
|
setCsrfHeaderName(name) {
|
|
805
|
-
this.
|
|
812
|
+
this._csrfHeader = name;
|
|
806
813
|
return this;
|
|
807
814
|
}
|
|
808
815
|
/**
|
|
@@ -811,7 +818,7 @@ class Config {
|
|
|
811
818
|
* @returns The current CSRF header name
|
|
812
819
|
*/
|
|
813
820
|
getCsrfHeaderName() {
|
|
814
|
-
return this.
|
|
821
|
+
return this._csrfHeader;
|
|
815
822
|
}
|
|
816
823
|
/**
|
|
817
824
|
* Set the XSRF cookie name to look for when extracting tokens from cookies
|
|
@@ -823,7 +830,7 @@ class Config {
|
|
|
823
830
|
* Config.getInstance().setXsrfCookieName('MY-XSRF-COOKIE');
|
|
824
831
|
*/
|
|
825
832
|
setXsrfCookieName(name) {
|
|
826
|
-
this.
|
|
833
|
+
this._xsrfCookie = name;
|
|
827
834
|
return this;
|
|
828
835
|
}
|
|
829
836
|
/**
|
|
@@ -832,7 +839,7 @@ class Config {
|
|
|
832
839
|
* @returns The current XSRF cookie name
|
|
833
840
|
*/
|
|
834
841
|
getXsrfCookieName() {
|
|
835
|
-
return this.
|
|
842
|
+
return this._xsrfCookie;
|
|
836
843
|
}
|
|
837
844
|
/**
|
|
838
845
|
* Set the XSRF header name for sending tokens extracted from cookies
|
|
@@ -841,7 +848,7 @@ class Config {
|
|
|
841
848
|
* @returns The config instance for chaining
|
|
842
849
|
*/
|
|
843
850
|
setXsrfHeaderName(name) {
|
|
844
|
-
this.
|
|
851
|
+
this._xsrfHeader = name;
|
|
845
852
|
return this;
|
|
846
853
|
}
|
|
847
854
|
/**
|
|
@@ -850,7 +857,7 @@ class Config {
|
|
|
850
857
|
* @returns The current XSRF header name
|
|
851
858
|
*/
|
|
852
859
|
getXsrfHeaderName() {
|
|
853
|
-
return this.
|
|
860
|
+
return this._xsrfHeader;
|
|
854
861
|
}
|
|
855
862
|
/**
|
|
856
863
|
* Enable or disable automatic extraction of XSRF tokens from cookies
|
|
@@ -864,7 +871,7 @@ class Config {
|
|
|
864
871
|
* Config.getInstance().setEnableAutoXsrf(false); // Disable XSRF extraction
|
|
865
872
|
*/
|
|
866
873
|
setEnableAutoXsrf(enable) {
|
|
867
|
-
this.
|
|
874
|
+
this._autoXsrf = enable;
|
|
868
875
|
return this;
|
|
869
876
|
}
|
|
870
877
|
/**
|
|
@@ -873,7 +880,7 @@ class Config {
|
|
|
873
880
|
* @returns True if automatic XSRF is enabled
|
|
874
881
|
*/
|
|
875
882
|
isAutoXsrfEnabled() {
|
|
876
|
-
return this.
|
|
883
|
+
return this._autoXsrf;
|
|
877
884
|
}
|
|
878
885
|
/**
|
|
879
886
|
* Enable or disable automatic addition of anti-CSRF headers
|
|
@@ -883,7 +890,7 @@ class Config {
|
|
|
883
890
|
* @returns The config instance for chaining
|
|
884
891
|
*/
|
|
885
892
|
setEnableAntiCsrf(enable) {
|
|
886
|
-
this.
|
|
893
|
+
this._antiCsrf = enable;
|
|
887
894
|
return this;
|
|
888
895
|
}
|
|
889
896
|
/**
|
|
@@ -892,7 +899,7 @@ class Config {
|
|
|
892
899
|
* @returns True if anti-CSRF protection is enabled
|
|
893
900
|
*/
|
|
894
901
|
isAntiCsrfEnabled() {
|
|
895
|
-
return this.
|
|
902
|
+
return this._antiCsrf;
|
|
896
903
|
}
|
|
897
904
|
/**
|
|
898
905
|
* Add a global request interceptor
|
|
@@ -908,8 +915,8 @@ class Config {
|
|
|
908
915
|
* });
|
|
909
916
|
*/
|
|
910
917
|
addRequestInterceptor(interceptor) {
|
|
911
|
-
const id = this.
|
|
912
|
-
this.
|
|
918
|
+
const id = this._nextId++;
|
|
919
|
+
this._reqI.push([id, interceptor]);
|
|
913
920
|
return id;
|
|
914
921
|
}
|
|
915
922
|
/**
|
|
@@ -926,8 +933,8 @@ class Config {
|
|
|
926
933
|
* });
|
|
927
934
|
*/
|
|
928
935
|
addResponseInterceptor(interceptor) {
|
|
929
|
-
const id = this.
|
|
930
|
-
this.
|
|
936
|
+
const id = this._nextId++;
|
|
937
|
+
this._resI.push([id, interceptor]);
|
|
931
938
|
return id;
|
|
932
939
|
}
|
|
933
940
|
/**
|
|
@@ -944,8 +951,8 @@ class Config {
|
|
|
944
951
|
* });
|
|
945
952
|
*/
|
|
946
953
|
addErrorInterceptor(interceptor) {
|
|
947
|
-
const id = this.
|
|
948
|
-
this.
|
|
954
|
+
const id = this._nextId++;
|
|
955
|
+
this._errI.push([id, interceptor]);
|
|
949
956
|
return id;
|
|
950
957
|
}
|
|
951
958
|
/**
|
|
@@ -957,7 +964,7 @@ class Config {
|
|
|
957
964
|
* Config.getInstance().removeRequestInterceptor(id);
|
|
958
965
|
*/
|
|
959
966
|
removeRequestInterceptor(id) {
|
|
960
|
-
this.
|
|
967
|
+
this._reqI = this._reqI.filter(item => item[0] !== id);
|
|
961
968
|
}
|
|
962
969
|
/**
|
|
963
970
|
* Remove a response interceptor by its ID
|
|
@@ -968,7 +975,7 @@ class Config {
|
|
|
968
975
|
* Config.getInstance().removeResponseInterceptor(id);
|
|
969
976
|
*/
|
|
970
977
|
removeResponseInterceptor(id) {
|
|
971
|
-
this.
|
|
978
|
+
this._resI = this._resI.filter(item => item[0] !== id);
|
|
972
979
|
}
|
|
973
980
|
/**
|
|
974
981
|
* Remove an error interceptor by its ID
|
|
@@ -979,7 +986,7 @@ class Config {
|
|
|
979
986
|
* Config.getInstance().removeErrorInterceptor(id);
|
|
980
987
|
*/
|
|
981
988
|
removeErrorInterceptor(id) {
|
|
982
|
-
this.
|
|
989
|
+
this._errI = this._errI.filter(item => item[0] !== id);
|
|
983
990
|
}
|
|
984
991
|
/**
|
|
985
992
|
* Clear all interceptors (request, response, and error)
|
|
@@ -988,30 +995,30 @@ class Config {
|
|
|
988
995
|
* Config.getInstance().clearInterceptors();
|
|
989
996
|
*/
|
|
990
997
|
clearInterceptors() {
|
|
991
|
-
this.
|
|
992
|
-
this.
|
|
993
|
-
this.
|
|
998
|
+
this._reqI = [];
|
|
999
|
+
this._resI = [];
|
|
1000
|
+
this._errI = [];
|
|
994
1001
|
}
|
|
995
1002
|
/**
|
|
996
1003
|
* Get all global request interceptors (in registration order)
|
|
997
1004
|
* @internal
|
|
998
1005
|
*/
|
|
999
1006
|
getRequestInterceptors() {
|
|
1000
|
-
return this.
|
|
1007
|
+
return this._reqI.map(item => item[1]);
|
|
1001
1008
|
}
|
|
1002
1009
|
/**
|
|
1003
1010
|
* Get all global response interceptors (in registration order)
|
|
1004
1011
|
* @internal
|
|
1005
1012
|
*/
|
|
1006
1013
|
getResponseInterceptors() {
|
|
1007
|
-
return this.
|
|
1014
|
+
return this._resI.map(item => item[1]);
|
|
1008
1015
|
}
|
|
1009
1016
|
/**
|
|
1010
1017
|
* Get all global error interceptors (in registration order)
|
|
1011
1018
|
* @internal
|
|
1012
1019
|
*/
|
|
1013
1020
|
getErrorInterceptors() {
|
|
1014
|
-
return this.
|
|
1021
|
+
return this._errI.map(item => item[1]);
|
|
1015
1022
|
}
|
|
1016
1023
|
/**
|
|
1017
1024
|
* Reset all configuration options to their default values
|
|
@@ -1022,12 +1029,12 @@ class Config {
|
|
|
1022
1029
|
* Config.getInstance().reset();
|
|
1023
1030
|
*/
|
|
1024
1031
|
reset() {
|
|
1025
|
-
this.
|
|
1026
|
-
this.
|
|
1027
|
-
this.
|
|
1028
|
-
this.
|
|
1029
|
-
this.
|
|
1030
|
-
this.
|
|
1032
|
+
this._csrfToken = null;
|
|
1033
|
+
this._csrfHeader = "X-CSRF-Token";
|
|
1034
|
+
this._antiCsrf = true;
|
|
1035
|
+
this._xsrfCookie = "XSRF-TOKEN";
|
|
1036
|
+
this._xsrfHeader = "X-XSRF-TOKEN";
|
|
1037
|
+
this._autoXsrf = true;
|
|
1031
1038
|
this.clearInterceptors();
|
|
1032
1039
|
return this;
|
|
1033
1040
|
}
|
|
@@ -1038,53 +1045,54 @@ class Config {
|
|
|
1038
1045
|
* Provides the core request building and execution capabilities.
|
|
1039
1046
|
*/
|
|
1040
1047
|
class BaseRequest {
|
|
1041
|
-
|
|
1042
|
-
|
|
1048
|
+
_url;
|
|
1049
|
+
_opts = {
|
|
1043
1050
|
headers: {},
|
|
1044
1051
|
};
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1052
|
+
_ctrl;
|
|
1053
|
+
_fetch;
|
|
1054
|
+
_query = new URLSearchParams();
|
|
1055
|
+
_autoCsrf = true;
|
|
1048
1056
|
// Per-request interceptors
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1057
|
+
_reqI = [];
|
|
1058
|
+
_resI = [];
|
|
1059
|
+
_errI = [];
|
|
1052
1060
|
constructor(url) {
|
|
1053
|
-
this.
|
|
1061
|
+
this._url = url;
|
|
1054
1062
|
}
|
|
1055
1063
|
/**
|
|
1056
1064
|
* Get GraphQL options if set (only for BodyRequest subclasses)
|
|
1057
1065
|
* @returns GraphQL options or undefined
|
|
1058
1066
|
*/
|
|
1059
|
-
|
|
1067
|
+
_gql() {
|
|
1060
1068
|
return undefined;
|
|
1061
1069
|
}
|
|
1062
1070
|
/**
|
|
1063
1071
|
* Creates a fluent API for setting enum-based options
|
|
1064
1072
|
* Combines direct setter with convenience methods
|
|
1065
1073
|
*/
|
|
1066
|
-
|
|
1074
|
+
_fluent(optionName, options) {
|
|
1067
1075
|
const fluent = {};
|
|
1068
1076
|
// Create convenience methods for each enum value
|
|
1069
1077
|
Object.entries(options).forEach(([key, value]) => {
|
|
1070
1078
|
fluent[key] = () => {
|
|
1071
|
-
this.
|
|
1079
|
+
this._opts[optionName] = value;
|
|
1072
1080
|
return this;
|
|
1073
1081
|
};
|
|
1074
1082
|
});
|
|
1075
1083
|
// Create the callable setter
|
|
1076
1084
|
const callable = (value) => {
|
|
1077
|
-
this.
|
|
1085
|
+
this._opts[optionName] = value;
|
|
1078
1086
|
return this;
|
|
1079
1087
|
};
|
|
1080
1088
|
return Object.assign(callable, fluent);
|
|
1081
1089
|
}
|
|
1082
|
-
|
|
1090
|
+
_validateUrl(url) {
|
|
1083
1091
|
const errorMessage = "Bad URL";
|
|
1084
1092
|
if (!url?.trim())
|
|
1085
|
-
throw new RequestError(errorMessage, url, this.
|
|
1093
|
+
throw new RequestError(errorMessage, url, this._method);
|
|
1086
1094
|
if (url.includes("\0") || url.includes("\r") || url.includes("\n")) {
|
|
1087
|
-
throw new RequestError(errorMessage, url, this.
|
|
1095
|
+
throw new RequestError(errorMessage, url, this._method);
|
|
1088
1096
|
}
|
|
1089
1097
|
const trimmed = url.trim();
|
|
1090
1098
|
if (/^https?:\/\//.test(trimmed)) {
|
|
@@ -1092,7 +1100,7 @@ class BaseRequest {
|
|
|
1092
1100
|
new URL(trimmed);
|
|
1093
1101
|
}
|
|
1094
1102
|
catch {
|
|
1095
|
-
throw new RequestError(errorMessage, trimmed, this.
|
|
1103
|
+
throw new RequestError(errorMessage, trimmed, this._method);
|
|
1096
1104
|
}
|
|
1097
1105
|
}
|
|
1098
1106
|
}
|
|
@@ -1110,16 +1118,12 @@ class BaseRequest {
|
|
|
1110
1118
|
*/
|
|
1111
1119
|
withHeaders(headers) {
|
|
1112
1120
|
// Filter out null and undefined values
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
if (value
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
this.requestOptions.headers = {
|
|
1120
|
-
...this.getHeadersRecord(),
|
|
1121
|
-
...filteredHeaders,
|
|
1122
|
-
};
|
|
1121
|
+
const merged = { ...this._headers() };
|
|
1122
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1123
|
+
if (value != null)
|
|
1124
|
+
merged[key] = value;
|
|
1125
|
+
}
|
|
1126
|
+
this._opts.headers = merged;
|
|
1123
1127
|
return this;
|
|
1124
1128
|
}
|
|
1125
1129
|
/**
|
|
@@ -1148,8 +1152,8 @@ class BaseRequest {
|
|
|
1148
1152
|
*/
|
|
1149
1153
|
withTimeout(timeout) {
|
|
1150
1154
|
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
1151
|
-
throw new RequestError("Bad timeout", this.
|
|
1152
|
-
this.
|
|
1155
|
+
throw new RequestError("Bad timeout", this._url, this._method);
|
|
1156
|
+
this._opts.timeout = timeout;
|
|
1153
1157
|
return this;
|
|
1154
1158
|
}
|
|
1155
1159
|
/**
|
|
@@ -1185,30 +1189,22 @@ class BaseRequest {
|
|
|
1185
1189
|
* });
|
|
1186
1190
|
*/
|
|
1187
1191
|
withRetries(retries) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
this.requestOptions.retries = retries;
|
|
1192
|
+
const isNumber = typeof retries === "number";
|
|
1193
|
+
const attempts = isNumber ? retries : retries.attempts;
|
|
1194
|
+
if (!Number.isInteger(attempts) || attempts < 0) {
|
|
1195
|
+
throw new RequestError(`Bad ${isNumber ? "retries" : "attempts"}: ${attempts}`, this._url, this._method);
|
|
1193
1196
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
if (
|
|
1197
|
-
|
|
1197
|
+
if (!isNumber && retries.delay !== undefined) {
|
|
1198
|
+
const delay = retries.delay;
|
|
1199
|
+
if (typeof delay === "number") {
|
|
1200
|
+
if (!Number.isFinite(delay) || delay < 0)
|
|
1201
|
+
throw new RequestError(`Bad delay: ${delay}`, this._url, this._method);
|
|
1198
1202
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
if (typeof retries.delay === "number") {
|
|
1202
|
-
if (!Number.isFinite(retries.delay) || retries.delay < 0) {
|
|
1203
|
-
throw new RequestError(`Bad delay: ${retries.delay}`, this.url, this.method);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
else if (typeof retries.delay !== "function") {
|
|
1207
|
-
throw new RequestError(`Bad delay: ${typeof retries.delay}`, this.url, this.method);
|
|
1208
|
-
}
|
|
1203
|
+
else if (typeof delay !== "function") {
|
|
1204
|
+
throw new RequestError(`Bad delay: ${typeof delay}`, this._url, this._method);
|
|
1209
1205
|
}
|
|
1210
|
-
this.requestOptions.retries = retries;
|
|
1211
1206
|
}
|
|
1207
|
+
this._opts.retries = retries;
|
|
1212
1208
|
return this;
|
|
1213
1209
|
}
|
|
1214
1210
|
/**
|
|
@@ -1225,7 +1221,7 @@ class BaseRequest {
|
|
|
1225
1221
|
* });
|
|
1226
1222
|
*/
|
|
1227
1223
|
onRetry(callback) {
|
|
1228
|
-
this.
|
|
1224
|
+
this._opts.onRetry = callback;
|
|
1229
1225
|
return this;
|
|
1230
1226
|
}
|
|
1231
1227
|
/**
|
|
@@ -1252,11 +1248,7 @@ class BaseRequest {
|
|
|
1252
1248
|
* request.withCredentials.INCLUDE()
|
|
1253
1249
|
*/
|
|
1254
1250
|
get withCredentials() {
|
|
1255
|
-
return this.
|
|
1256
|
-
INCLUDE: exports.CredentialsPolicy.INCLUDE,
|
|
1257
|
-
OMIT: exports.CredentialsPolicy.OMIT,
|
|
1258
|
-
SAME_ORIGIN: exports.CredentialsPolicy.SAME_ORIGIN,
|
|
1259
|
-
});
|
|
1251
|
+
return this._fluent("credentials", CredentialsPolicy);
|
|
1260
1252
|
}
|
|
1261
1253
|
/**
|
|
1262
1254
|
* Allows providing an external AbortController to cancel the request.
|
|
@@ -1285,7 +1277,42 @@ class BaseRequest {
|
|
|
1285
1277
|
* controller.abort();
|
|
1286
1278
|
*/
|
|
1287
1279
|
withAbortController(controller) {
|
|
1288
|
-
this.
|
|
1280
|
+
this._ctrl = controller;
|
|
1281
|
+
return this;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Sets a custom fetch implementation used to execute this request.
|
|
1285
|
+
* By default, requests use the global `fetch`. Injecting a custom function unlocks
|
|
1286
|
+
* testing without global mocks, custom undici dispatchers/agents in Node.js,
|
|
1287
|
+
* and framework-specific fetch extensions (e.g. Next.js caching options).
|
|
1288
|
+
*
|
|
1289
|
+
* The provided function receives the final URL and `RequestInit` (after interceptors)
|
|
1290
|
+
* and must return a `Promise<Response>`. It should honor `init.signal` so that
|
|
1291
|
+
* `withTimeout` and `withAbortController` keep working.
|
|
1292
|
+
*
|
|
1293
|
+
* @param fetchFn - A fetch-compatible function
|
|
1294
|
+
* @returns The request instance for chaining
|
|
1295
|
+
* @throws {RequestError} If fetchFn is not a function
|
|
1296
|
+
*
|
|
1297
|
+
* @example
|
|
1298
|
+
* // Testing: inject a stub instead of mocking the global fetch
|
|
1299
|
+
* const stubFetch: FetchFunction = async () => new Response('{"ok":true}');
|
|
1300
|
+
* const data = await create.get('/api/users').withFetch(stubFetch).getJson();
|
|
1301
|
+
*
|
|
1302
|
+
* @example
|
|
1303
|
+
* // Node.js: route through a custom undici agent (proxy, keep-alive tuning, ...)
|
|
1304
|
+
* import { fetch as undiciFetch, Agent } from 'undici';
|
|
1305
|
+
* const agent = new Agent({ keepAliveTimeout: 30_000 });
|
|
1306
|
+
* request.withFetch((url, init) => undiciFetch(url, { ...init, dispatcher: agent }));
|
|
1307
|
+
*
|
|
1308
|
+
* @example
|
|
1309
|
+
* // Next.js: pass caching hints through to the framework's patched fetch
|
|
1310
|
+
* request.withFetch((url, init) => fetch(url, { ...init, next: { revalidate: 60 } }));
|
|
1311
|
+
*/
|
|
1312
|
+
withFetch(fetchFn) {
|
|
1313
|
+
if (typeof fetchFn !== "function")
|
|
1314
|
+
throw new RequestError("Bad fetch", this._url, this._method);
|
|
1315
|
+
this._fetch = fetchFn;
|
|
1289
1316
|
return this;
|
|
1290
1317
|
}
|
|
1291
1318
|
/**
|
|
@@ -1307,7 +1334,7 @@ class BaseRequest {
|
|
|
1307
1334
|
* request.withReferrer("")
|
|
1308
1335
|
*/
|
|
1309
1336
|
withReferrer(referrer) {
|
|
1310
|
-
this.
|
|
1337
|
+
this._opts.referrer = referrer;
|
|
1311
1338
|
return this;
|
|
1312
1339
|
}
|
|
1313
1340
|
/**
|
|
@@ -1339,16 +1366,7 @@ class BaseRequest {
|
|
|
1339
1366
|
* request.withReferrerPolicy.NO_REFERRER()
|
|
1340
1367
|
*/
|
|
1341
1368
|
get withReferrerPolicy() {
|
|
1342
|
-
return this.
|
|
1343
|
-
ORIGIN: exports.ReferrerPolicy.ORIGIN,
|
|
1344
|
-
UNSAFE_URL: exports.ReferrerPolicy.UNSAFE_URL,
|
|
1345
|
-
SAME_ORIGIN: exports.ReferrerPolicy.SAME_ORIGIN,
|
|
1346
|
-
NO_REFERRER: exports.ReferrerPolicy.NO_REFERRER,
|
|
1347
|
-
STRICT_ORIGIN: exports.ReferrerPolicy.STRICT_ORIGIN,
|
|
1348
|
-
ORIGIN_WHEN_CROSS_ORIGIN: exports.ReferrerPolicy.ORIGIN_WHEN_CROSS_ORIGIN,
|
|
1349
|
-
NO_REFERRER_WHEN_DOWNGRADE: exports.ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE,
|
|
1350
|
-
STRICT_ORIGIN_WHEN_CROSS_ORIGIN: exports.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
|
|
1351
|
-
});
|
|
1369
|
+
return this._fluent("referrerPolicy", ReferrerPolicy);
|
|
1352
1370
|
}
|
|
1353
1371
|
/**
|
|
1354
1372
|
* Sets how the request handles HTTP redirects (3xx status codes).
|
|
@@ -1377,11 +1395,7 @@ class BaseRequest {
|
|
|
1377
1395
|
* request.withRedirect.ERROR()
|
|
1378
1396
|
*/
|
|
1379
1397
|
get withRedirect() {
|
|
1380
|
-
return this.
|
|
1381
|
-
FOLLOW: exports.RedirectMode.FOLLOW,
|
|
1382
|
-
ERROR: exports.RedirectMode.ERROR,
|
|
1383
|
-
MANUAL: exports.RedirectMode.MANUAL,
|
|
1384
|
-
});
|
|
1398
|
+
return this._fluent("redirect", RedirectMode);
|
|
1385
1399
|
}
|
|
1386
1400
|
/**
|
|
1387
1401
|
* Sets the keepalive flag for the request. When enabled, the request can continue
|
|
@@ -1399,7 +1413,7 @@ class BaseRequest {
|
|
|
1399
1413
|
* request.withKeepAlive(true)
|
|
1400
1414
|
*/
|
|
1401
1415
|
withKeepAlive(keepalive) {
|
|
1402
|
-
this.
|
|
1416
|
+
this._opts.keepalive = keepalive;
|
|
1403
1417
|
return this;
|
|
1404
1418
|
}
|
|
1405
1419
|
/**
|
|
@@ -1430,11 +1444,7 @@ class BaseRequest {
|
|
|
1430
1444
|
* request.withPriority.LOW()
|
|
1431
1445
|
*/
|
|
1432
1446
|
get withPriority() {
|
|
1433
|
-
return this.
|
|
1434
|
-
HIGH: exports.RequestPriority.HIGH,
|
|
1435
|
-
LOW: exports.RequestPriority.LOW,
|
|
1436
|
-
AUTO: exports.RequestPriority.AUTO,
|
|
1437
|
-
});
|
|
1447
|
+
return this._fluent("priority", RequestPriority);
|
|
1438
1448
|
}
|
|
1439
1449
|
/**
|
|
1440
1450
|
* Sets the integrity hash for Subresource Integrity (SRI) verification.
|
|
@@ -1457,7 +1467,7 @@ class BaseRequest {
|
|
|
1457
1467
|
* request.withIntegrity("sha256-... sha384-...")
|
|
1458
1468
|
*/
|
|
1459
1469
|
withIntegrity(integrity) {
|
|
1460
|
-
this.
|
|
1470
|
+
this._opts.integrity = integrity;
|
|
1461
1471
|
return this;
|
|
1462
1472
|
}
|
|
1463
1473
|
/**
|
|
@@ -1495,15 +1505,7 @@ class BaseRequest {
|
|
|
1495
1505
|
* request.withCache.ONLY_IF_CACHED()
|
|
1496
1506
|
*/
|
|
1497
1507
|
get withCache() {
|
|
1498
|
-
|
|
1499
|
-
DEFAULT: "default",
|
|
1500
|
-
NO_STORE: "no-store",
|
|
1501
|
-
RELOAD: "reload",
|
|
1502
|
-
NO_CACHE: "no-cache",
|
|
1503
|
-
FORCE_CACHE: "force-cache",
|
|
1504
|
-
ONLY_IF_CACHED: "only-if-cached",
|
|
1505
|
-
};
|
|
1506
|
-
return this.createFluentSetter("cache", cacheOptions);
|
|
1508
|
+
return this._fluent("cache", CacheMode);
|
|
1507
1509
|
}
|
|
1508
1510
|
/**
|
|
1509
1511
|
* Adds query parameters to the request URL.
|
|
@@ -1542,10 +1544,10 @@ class BaseRequest {
|
|
|
1542
1544
|
}
|
|
1543
1545
|
if (Array.isArray(value)) {
|
|
1544
1546
|
// Handle array values - add multiple entries with the same key
|
|
1545
|
-
value.forEach(v => this.
|
|
1547
|
+
value.forEach(v => this._query.append(key, String(v)));
|
|
1546
1548
|
}
|
|
1547
1549
|
else {
|
|
1548
|
-
this.
|
|
1550
|
+
this._query.append(key, String(value));
|
|
1549
1551
|
}
|
|
1550
1552
|
});
|
|
1551
1553
|
return this;
|
|
@@ -1603,12 +1605,7 @@ class BaseRequest {
|
|
|
1603
1605
|
* request.withMode.SAME_ORIGIN()
|
|
1604
1606
|
*/
|
|
1605
1607
|
get withMode() {
|
|
1606
|
-
return this.
|
|
1607
|
-
CORS: exports.RequestMode.CORS,
|
|
1608
|
-
NO_CORS: exports.RequestMode.NO_CORS,
|
|
1609
|
-
SAME_ORIGIN: exports.RequestMode.SAME_ORIGIN,
|
|
1610
|
-
NAVIGATE: exports.RequestMode.NAVIGATE,
|
|
1611
|
-
});
|
|
1608
|
+
return this._fluent("mode", RequestMode);
|
|
1612
1609
|
}
|
|
1613
1610
|
/**
|
|
1614
1611
|
* Sets the Content-Type header for the request.
|
|
@@ -1666,28 +1663,26 @@ class BaseRequest {
|
|
|
1666
1663
|
* ```
|
|
1667
1664
|
*/
|
|
1668
1665
|
withBasicAuth(username, password) {
|
|
1669
|
-
const credentials = this.
|
|
1666
|
+
const credentials = this._b64(`${username}:${password}`);
|
|
1670
1667
|
return this.withAuthorization(`Basic ${credentials}`);
|
|
1671
1668
|
}
|
|
1672
1669
|
/**
|
|
1673
1670
|
* Cross-environment base64 encoding
|
|
1674
1671
|
* Works in both browser and Node.js environments
|
|
1675
1672
|
*/
|
|
1676
|
-
|
|
1673
|
+
_b64(str) {
|
|
1677
1674
|
// Modern approach using TextEncoder (available in both modern browsers and Node.js)
|
|
1678
|
-
if (typeof
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
}
|
|
1683
|
-
// Browser environment
|
|
1684
|
-
if (typeof btoa === "function")
|
|
1675
|
+
if (typeof btoa === "function") {
|
|
1676
|
+
if (typeof TextEncoder !== "undefined")
|
|
1677
|
+
return btoa(String.fromCharCode(...new TextEncoder().encode(str)));
|
|
1678
|
+
// Browser environment without TextEncoder
|
|
1685
1679
|
return btoa(str);
|
|
1680
|
+
}
|
|
1686
1681
|
// Node.js environment
|
|
1687
1682
|
if (typeof Buffer !== "undefined")
|
|
1688
1683
|
return Buffer.from(str).toString("base64");
|
|
1689
1684
|
// Fallback (should never happen in modern environments)
|
|
1690
|
-
throw new RequestError("No encoder", this.
|
|
1685
|
+
throw new RequestError("No encoder", this._url, this._method);
|
|
1691
1686
|
}
|
|
1692
1687
|
/**
|
|
1693
1688
|
* Sets a Bearer token for authentication.
|
|
@@ -1709,9 +1704,9 @@ class BaseRequest {
|
|
|
1709
1704
|
* Safely get headers as a Record<string, string>
|
|
1710
1705
|
* @returns The headers object
|
|
1711
1706
|
*/
|
|
1712
|
-
|
|
1713
|
-
if (typeof this.
|
|
1714
|
-
return this.
|
|
1707
|
+
_headers() {
|
|
1708
|
+
if (typeof this._opts.headers === "object" && this._opts.headers !== null) {
|
|
1709
|
+
return this._opts.headers;
|
|
1715
1710
|
}
|
|
1716
1711
|
return {};
|
|
1717
1712
|
}
|
|
@@ -1720,8 +1715,8 @@ class BaseRequest {
|
|
|
1720
1715
|
* @param headerName Header name to check
|
|
1721
1716
|
* @returns Boolean indicating if the header exists (case-insensitive)
|
|
1722
1717
|
*/
|
|
1723
|
-
|
|
1724
|
-
const headers = this.
|
|
1718
|
+
_hasHeader(headerName) {
|
|
1719
|
+
const headers = this._headers();
|
|
1725
1720
|
return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
|
|
1726
1721
|
}
|
|
1727
1722
|
/**
|
|
@@ -1750,37 +1745,26 @@ class BaseRequest {
|
|
|
1750
1745
|
* ```
|
|
1751
1746
|
*/
|
|
1752
1747
|
withCookies(cookies) {
|
|
1753
|
-
|
|
1754
|
-
if (cookieEntries.length === 0) {
|
|
1748
|
+
if (Object.keys(cookies || {}).length === 0)
|
|
1755
1749
|
return this;
|
|
1756
|
-
|
|
1757
|
-
const
|
|
1758
|
-
// Get all existing cookie headers with different cases
|
|
1750
|
+
// Collect existing cookie header values (any casing), preserving the first header's case
|
|
1751
|
+
const newHeaders = { ...this._headers() };
|
|
1759
1752
|
const cookieValues = [];
|
|
1760
|
-
|
|
1761
|
-
Object.keys(
|
|
1753
|
+
let headerName = "";
|
|
1754
|
+
for (const key of Object.keys(newHeaders)) {
|
|
1762
1755
|
if (key.toLowerCase() === "cookie") {
|
|
1763
1756
|
// Don't add empty cookie values
|
|
1764
|
-
if (
|
|
1765
|
-
cookieValues.push(
|
|
1766
|
-
|
|
1757
|
+
if (newHeaders[key])
|
|
1758
|
+
cookieValues.push(newHeaders[key]);
|
|
1759
|
+
headerName ||= key;
|
|
1760
|
+
delete newHeaders[key];
|
|
1767
1761
|
}
|
|
1768
|
-
}
|
|
1769
|
-
//
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
const headerName = cookieHeaderKeys.length > 0 ? cookieHeaderKeys[0] : "Cookie";
|
|
1773
|
-
// Combine all existing cookie values with the new ones
|
|
1774
|
-
const combinedCookieValue = [...cookieValues, cookieString].filter(Boolean).join("; ");
|
|
1775
|
-
// Create a new headers object without any cookie headers
|
|
1776
|
-
const newHeaders = { ...currentHeaders };
|
|
1777
|
-
cookieHeaderKeys.forEach(key => {
|
|
1778
|
-
delete newHeaders[key];
|
|
1779
|
-
});
|
|
1780
|
-
// Set the new combined cookie header
|
|
1781
|
-
this.requestOptions.headers = {
|
|
1762
|
+
}
|
|
1763
|
+
// Combine all existing cookie values with the newly formatted ones
|
|
1764
|
+
cookieValues.push(CookieUtils.formatRequestCookies(cookies));
|
|
1765
|
+
this._opts.headers = {
|
|
1782
1766
|
...newHeaders,
|
|
1783
|
-
[headerName]:
|
|
1767
|
+
[headerName || "Cookie"]: cookieValues.filter(Boolean).join("; "),
|
|
1784
1768
|
};
|
|
1785
1769
|
return this;
|
|
1786
1770
|
}
|
|
@@ -1834,7 +1818,7 @@ class BaseRequest {
|
|
|
1834
1818
|
* @returns The instance for chaining
|
|
1835
1819
|
*/
|
|
1836
1820
|
withoutCsrfProtection() {
|
|
1837
|
-
this.
|
|
1821
|
+
this._autoCsrf = false;
|
|
1838
1822
|
return this;
|
|
1839
1823
|
}
|
|
1840
1824
|
/**
|
|
@@ -1858,7 +1842,7 @@ class BaseRequest {
|
|
|
1858
1842
|
* });
|
|
1859
1843
|
*/
|
|
1860
1844
|
withRequestInterceptor(interceptor) {
|
|
1861
|
-
this.
|
|
1845
|
+
this._reqI.push(interceptor);
|
|
1862
1846
|
return this;
|
|
1863
1847
|
}
|
|
1864
1848
|
/**
|
|
@@ -1875,7 +1859,7 @@ class BaseRequest {
|
|
|
1875
1859
|
* });
|
|
1876
1860
|
*/
|
|
1877
1861
|
withResponseInterceptor(interceptor) {
|
|
1878
|
-
this.
|
|
1862
|
+
this._resI.push(interceptor);
|
|
1879
1863
|
return this;
|
|
1880
1864
|
}
|
|
1881
1865
|
/**
|
|
@@ -1892,7 +1876,7 @@ class BaseRequest {
|
|
|
1892
1876
|
* });
|
|
1893
1877
|
*/
|
|
1894
1878
|
withErrorInterceptor(interceptor) {
|
|
1895
|
-
this.
|
|
1879
|
+
this._errI.push(interceptor);
|
|
1896
1880
|
return this;
|
|
1897
1881
|
}
|
|
1898
1882
|
/**
|
|
@@ -1906,13 +1890,13 @@ class BaseRequest {
|
|
|
1906
1890
|
* console.log(response.status);
|
|
1907
1891
|
*/
|
|
1908
1892
|
async getResponse() {
|
|
1909
|
-
const url = this.
|
|
1910
|
-
this.
|
|
1893
|
+
const url = this._fullUrl(this._url);
|
|
1894
|
+
this._applyCsrf();
|
|
1911
1895
|
const fetchOptions = {
|
|
1912
|
-
...this.
|
|
1913
|
-
method: this.
|
|
1896
|
+
...this._opts,
|
|
1897
|
+
method: this._method,
|
|
1914
1898
|
};
|
|
1915
|
-
return !this.
|
|
1899
|
+
return !this._opts.retries ? this._run(url, fetchOptions) : this._retry(url, fetchOptions);
|
|
1916
1900
|
}
|
|
1917
1901
|
/**
|
|
1918
1902
|
* Execute the request and parse the response as JSON
|
|
@@ -2053,8 +2037,8 @@ class BaseRequest {
|
|
|
2053
2037
|
/**
|
|
2054
2038
|
* Apply CSRF protection headers based on configuration
|
|
2055
2039
|
*/
|
|
2056
|
-
|
|
2057
|
-
if (!this.
|
|
2040
|
+
_applyCsrf() {
|
|
2041
|
+
if (!this._autoCsrf)
|
|
2058
2042
|
return;
|
|
2059
2043
|
const config = Config.getInstance();
|
|
2060
2044
|
// Apply anti-CSRF headers if enabled
|
|
@@ -2065,7 +2049,7 @@ class BaseRequest {
|
|
|
2065
2049
|
const globalToken = config.getCsrfToken();
|
|
2066
2050
|
if (globalToken) {
|
|
2067
2051
|
const csrfHeaderName = config.getCsrfHeaderName();
|
|
2068
|
-
const hasLocalToken = this.
|
|
2052
|
+
const hasLocalToken = this._hasHeader("X-CSRF-Token") || this._hasHeader(csrfHeaderName);
|
|
2069
2053
|
if (!hasLocalToken) {
|
|
2070
2054
|
this.withHeader(csrfHeaderName, globalToken);
|
|
2071
2055
|
}
|
|
@@ -2075,7 +2059,7 @@ class BaseRequest {
|
|
|
2075
2059
|
const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
|
|
2076
2060
|
if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
|
|
2077
2061
|
const xsrfHeaderName = config.getXsrfHeaderName();
|
|
2078
|
-
const hasLocalToken = this.
|
|
2062
|
+
const hasLocalToken = this._hasHeader("X-XSRF-TOKEN") || this._hasHeader(xsrfHeaderName);
|
|
2079
2063
|
if (!hasLocalToken) {
|
|
2080
2064
|
this.withHeader(xsrfHeaderName, xsrfToken);
|
|
2081
2065
|
}
|
|
@@ -2087,8 +2071,8 @@ class BaseRequest {
|
|
|
2087
2071
|
* @param url The base URL
|
|
2088
2072
|
* @returns The URL with query parameters appended
|
|
2089
2073
|
*/
|
|
2090
|
-
|
|
2091
|
-
const queryString = this.
|
|
2074
|
+
_fullUrl(url) {
|
|
2075
|
+
const queryString = this._query.toString();
|
|
2092
2076
|
if (!queryString) {
|
|
2093
2077
|
return url;
|
|
2094
2078
|
}
|
|
@@ -2096,7 +2080,7 @@ class BaseRequest {
|
|
|
2096
2080
|
// Try to use the URL constructor (works for absolute URLs)
|
|
2097
2081
|
const urlObj = new URL(url);
|
|
2098
2082
|
// Merge our query params with any that might be in the URL already
|
|
2099
|
-
this.
|
|
2083
|
+
this._query.forEach((value, key) => {
|
|
2100
2084
|
urlObj.searchParams.append(key, value);
|
|
2101
2085
|
});
|
|
2102
2086
|
return urlObj.toString();
|
|
@@ -2115,21 +2099,21 @@ class BaseRequest {
|
|
|
2115
2099
|
* @returns A wrapped response object
|
|
2116
2100
|
* @throws RequestError if the request fails after all retries
|
|
2117
2101
|
*/
|
|
2118
|
-
async
|
|
2119
|
-
const retriesConfig = this.
|
|
2102
|
+
async _retry(url, fetchOptions) {
|
|
2103
|
+
const retriesConfig = this._opts.retries;
|
|
2120
2104
|
const maxRetries = typeof retriesConfig === "number" ? retriesConfig : retriesConfig?.attempts || 0;
|
|
2121
2105
|
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
2122
2106
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2123
2107
|
try {
|
|
2124
|
-
return await this.
|
|
2108
|
+
return await this._run(url, fetchOptions);
|
|
2125
2109
|
}
|
|
2126
2110
|
catch (error) {
|
|
2127
|
-
const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method,
|
|
2111
|
+
const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, toError(error));
|
|
2128
2112
|
if (attempt >= maxRetries)
|
|
2129
2113
|
throw requestError;
|
|
2130
2114
|
// Call onRetry callback if provided
|
|
2131
|
-
if (this.
|
|
2132
|
-
await this.
|
|
2115
|
+
if (this._opts.onRetry) {
|
|
2116
|
+
await this._opts.onRetry({ attempt: attempt + 1, error: requestError });
|
|
2133
2117
|
}
|
|
2134
2118
|
// Apply delay if configured
|
|
2135
2119
|
if (typeof retriesConfig === "object" && retriesConfig.delay !== undefined) {
|
|
@@ -2153,9 +2137,9 @@ class BaseRequest {
|
|
|
2153
2137
|
* @param configParam - The request configuration
|
|
2154
2138
|
* @returns Modified config or a Response to short-circuit
|
|
2155
2139
|
*/
|
|
2156
|
-
async
|
|
2140
|
+
async _runReqI(configParam) {
|
|
2157
2141
|
const globalConfig = Config.getInstance();
|
|
2158
|
-
const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this.
|
|
2142
|
+
const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this._reqI];
|
|
2159
2143
|
let currentConfig = configParam;
|
|
2160
2144
|
for (let i = 0; i < allInterceptors.length; i++) {
|
|
2161
2145
|
try {
|
|
@@ -2167,7 +2151,7 @@ class BaseRequest {
|
|
|
2167
2151
|
currentConfig = result;
|
|
2168
2152
|
}
|
|
2169
2153
|
catch (error) {
|
|
2170
|
-
throw new RequestError(`ReqI: ${
|
|
2154
|
+
throw new RequestError(`ReqI: ${errorMessage(error)}`, currentConfig.url, currentConfig.method);
|
|
2171
2155
|
}
|
|
2172
2156
|
}
|
|
2173
2157
|
return currentConfig;
|
|
@@ -2177,18 +2161,18 @@ class BaseRequest {
|
|
|
2177
2161
|
* @param response - The response wrapper
|
|
2178
2162
|
* @returns Modified response wrapper
|
|
2179
2163
|
*/
|
|
2180
|
-
async
|
|
2164
|
+
async _runResI(response) {
|
|
2181
2165
|
const globalConfig = Config.getInstance();
|
|
2182
2166
|
const globalInterceptors = globalConfig.getResponseInterceptors();
|
|
2183
2167
|
// Per-request in order, then global in reverse
|
|
2184
|
-
const allInterceptors = [...this.
|
|
2168
|
+
const allInterceptors = [...this._resI, ...[...globalInterceptors].reverse()];
|
|
2185
2169
|
let currentResponse = response;
|
|
2186
2170
|
for (let i = 0; i < allInterceptors.length; i++) {
|
|
2187
2171
|
try {
|
|
2188
2172
|
currentResponse = await allInterceptors[i](currentResponse);
|
|
2189
2173
|
}
|
|
2190
2174
|
catch (error) {
|
|
2191
|
-
throw new RequestError(`ResI: ${
|
|
2175
|
+
throw new RequestError(`ResI: ${errorMessage(error)}`, currentResponse.url || "", currentResponse.method || "");
|
|
2192
2176
|
}
|
|
2193
2177
|
}
|
|
2194
2178
|
return currentResponse;
|
|
@@ -2198,11 +2182,11 @@ class BaseRequest {
|
|
|
2198
2182
|
* @param error - The error that occurred
|
|
2199
2183
|
* @returns Modified error or a ResponseWrapper to recover
|
|
2200
2184
|
*/
|
|
2201
|
-
async
|
|
2185
|
+
async _runErrI(error) {
|
|
2202
2186
|
const globalConfig = Config.getInstance();
|
|
2203
2187
|
const globalInterceptors = globalConfig.getErrorInterceptors();
|
|
2204
2188
|
// Per-request in order, then global in reverse
|
|
2205
|
-
const allInterceptors = [...this.
|
|
2189
|
+
const allInterceptors = [...this._errI, ...[...globalInterceptors].reverse()];
|
|
2206
2190
|
let currentError = error;
|
|
2207
2191
|
for (let i = 0; i < allInterceptors.length; i++) {
|
|
2208
2192
|
try {
|
|
@@ -2218,23 +2202,20 @@ class BaseRequest {
|
|
|
2218
2202
|
if (interceptorError instanceof RequestError) {
|
|
2219
2203
|
currentError = interceptorError;
|
|
2220
2204
|
}
|
|
2221
|
-
else {
|
|
2222
|
-
const em = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
|
|
2205
|
+
else if (currentError instanceof RequestError) {
|
|
2223
2206
|
// Always wrap in RequestError when we have context
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
/* c8 ignore end */
|
|
2237
|
-
}
|
|
2207
|
+
currentError = new RequestError(`ErrI${i + 1}: ${errorMessage(interceptorError)}`, currentError.url, currentError.method, {
|
|
2208
|
+
status: currentError.status,
|
|
2209
|
+
response: currentError.response,
|
|
2210
|
+
body: currentError.body,
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
else {
|
|
2214
|
+
/* c8 ignore start */
|
|
2215
|
+
// Last resort: if we have no context at all, use the original error's context
|
|
2216
|
+
// This shouldn't happen in practice, but handle it gracefully
|
|
2217
|
+
currentError = RequestError.networkError(error.url, error.method, toError(interceptorError));
|
|
2218
|
+
/* c8 ignore end */
|
|
2238
2219
|
}
|
|
2239
2220
|
}
|
|
2240
2221
|
}
|
|
@@ -2244,7 +2225,7 @@ class BaseRequest {
|
|
|
2244
2225
|
* Helper to create an abort signal with timeout support
|
|
2245
2226
|
* Handles various AbortSignal API levels gracefully
|
|
2246
2227
|
*/
|
|
2247
|
-
|
|
2228
|
+
_signal(timeoutMs, externalController) {
|
|
2248
2229
|
let timeoutId;
|
|
2249
2230
|
let timeoutController;
|
|
2250
2231
|
let isTimeout = false;
|
|
@@ -2280,7 +2261,7 @@ class BaseRequest {
|
|
|
2280
2261
|
const finalSignal = externalController && hasAbortSignalAny
|
|
2281
2262
|
? AbortSignal.any([externalController.signal, timeoutSignal])
|
|
2282
2263
|
: externalController
|
|
2283
|
-
? this.
|
|
2264
|
+
? this._combine(externalController.signal, timeoutSignal)
|
|
2284
2265
|
: timeoutSignal;
|
|
2285
2266
|
return {
|
|
2286
2267
|
signal: finalSignal,
|
|
@@ -2295,7 +2276,7 @@ class BaseRequest {
|
|
|
2295
2276
|
* Manually combine two abort signals for older environments
|
|
2296
2277
|
* Returns the first signal and listens to the second
|
|
2297
2278
|
*/
|
|
2298
|
-
|
|
2279
|
+
_combine(signal1, signal2) {
|
|
2299
2280
|
// If either is already aborted, use that one
|
|
2300
2281
|
if (signal1.aborted)
|
|
2301
2282
|
return signal1;
|
|
@@ -2311,9 +2292,9 @@ class BaseRequest {
|
|
|
2311
2292
|
/**
|
|
2312
2293
|
* Convert fetchOptions to RequestConfig with proper typing
|
|
2313
2294
|
*/
|
|
2314
|
-
|
|
2295
|
+
_config(url, fetchOptions) {
|
|
2315
2296
|
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
2316
|
-
const headers = this.
|
|
2297
|
+
const headers = this._headers();
|
|
2317
2298
|
const extendedOptions = fetchOptions;
|
|
2318
2299
|
return {
|
|
2319
2300
|
url,
|
|
@@ -2335,7 +2316,7 @@ class BaseRequest {
|
|
|
2335
2316
|
/**
|
|
2336
2317
|
* Apply interceptor results back to fetchOptions
|
|
2337
2318
|
*/
|
|
2338
|
-
|
|
2319
|
+
_applyConfig(config, fetchOptions) {
|
|
2339
2320
|
fetchOptions.headers = config.headers;
|
|
2340
2321
|
if (config.body !== undefined) {
|
|
2341
2322
|
fetchOptions.body = config.body;
|
|
@@ -2347,53 +2328,54 @@ class BaseRequest {
|
|
|
2347
2328
|
fetchOptions.cache = config.cache;
|
|
2348
2329
|
}
|
|
2349
2330
|
}
|
|
2350
|
-
async
|
|
2331
|
+
async _run(url, fetchOptions) {
|
|
2351
2332
|
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
2352
2333
|
// Setup abort signal with timeout
|
|
2353
|
-
const abortSignal = this.
|
|
2334
|
+
const abortSignal = this._signal(this._opts.timeout, this._ctrl);
|
|
2354
2335
|
try {
|
|
2355
2336
|
// Run request interceptors before making the request
|
|
2356
|
-
const requestConfig = this.
|
|
2357
|
-
const interceptorResult = await this.
|
|
2337
|
+
const requestConfig = this._config(url, fetchOptions);
|
|
2338
|
+
const interceptorResult = await this._runReqI(requestConfig);
|
|
2358
2339
|
// If interceptor returned a Response, short-circuit and wrap it
|
|
2359
2340
|
if (interceptorResult instanceof Response) {
|
|
2360
|
-
const graphQLOptions = this.
|
|
2361
|
-
const wrappedResponse = new ResponseWrapper(interceptorResult, this.
|
|
2362
|
-
return await this.
|
|
2341
|
+
const graphQLOptions = this._gql();
|
|
2342
|
+
const wrappedResponse = new ResponseWrapper(interceptorResult, this._url, this._method, graphQLOptions);
|
|
2343
|
+
return await this._runResI(wrappedResponse);
|
|
2363
2344
|
}
|
|
2364
2345
|
// Update fetchOptions with interceptor modifications
|
|
2365
2346
|
url = interceptorResult.url;
|
|
2366
|
-
this.
|
|
2367
|
-
this.
|
|
2347
|
+
this._validateUrl(url);
|
|
2348
|
+
this._applyConfig(interceptorResult, fetchOptions);
|
|
2368
2349
|
// Set the combined abort signal
|
|
2369
2350
|
if (abortSignal.signal) {
|
|
2370
2351
|
fetchOptions.signal = abortSignal.signal;
|
|
2371
2352
|
}
|
|
2372
|
-
// Execute fetch
|
|
2353
|
+
// Execute fetch (custom implementation if provided, global fetch otherwise)
|
|
2354
|
+
const fetchFn = this._fetch ?? globalThis.fetch;
|
|
2373
2355
|
let response;
|
|
2374
2356
|
try {
|
|
2375
|
-
response = await
|
|
2357
|
+
response = await fetchFn(url, fetchOptions);
|
|
2376
2358
|
}
|
|
2377
2359
|
catch (error) {
|
|
2378
|
-
const errorObj =
|
|
2360
|
+
const errorObj = toError(error);
|
|
2379
2361
|
const errorName = errorObj.name;
|
|
2380
|
-
const
|
|
2362
|
+
const lowerMessage = errorObj.message.toLowerCase();
|
|
2381
2363
|
// Check if this is a timeout error from our internal timeout
|
|
2382
2364
|
const isOurTimeout = abortSignal.wasTimeout();
|
|
2383
2365
|
// Check if it's an abort error (DOMException in browsers, or AbortSignal abort)
|
|
2384
2366
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
2385
2367
|
// If it was our timeout that caused the abort, throw timeout error
|
|
2386
|
-
if (isOurTimeout && this.
|
|
2387
|
-
throw RequestError.timeout(url, method, this.
|
|
2368
|
+
if (isOurTimeout && this._opts.timeout) {
|
|
2369
|
+
throw RequestError.timeout(url, method, this._opts.timeout);
|
|
2388
2370
|
}
|
|
2389
2371
|
// Otherwise it's a manual abort
|
|
2390
2372
|
throw RequestError.abortError(url, method);
|
|
2391
2373
|
}
|
|
2392
2374
|
// Check for Node.js/undici TimeoutError or other timeout indicators
|
|
2393
2375
|
// This catches timeout errors that aren't thrown as AbortError
|
|
2394
|
-
const isTimeoutError = isOurTimeout || errorName === "TimeoutError" ||
|
|
2395
|
-
if (isTimeoutError && this.
|
|
2396
|
-
throw RequestError.timeout(url, method, this.
|
|
2376
|
+
const isTimeoutError = isOurTimeout || errorName === "TimeoutError" || lowerMessage.includes("timeout");
|
|
2377
|
+
if (isTimeoutError && this._opts.timeout) {
|
|
2378
|
+
throw RequestError.timeout(url, method, this._opts.timeout);
|
|
2397
2379
|
}
|
|
2398
2380
|
// For other network errors, let RequestError.networkError handle them
|
|
2399
2381
|
// It will check for timeout patterns as a safety net (useful for external AbortControllers)
|
|
@@ -2405,24 +2387,19 @@ class BaseRequest {
|
|
|
2405
2387
|
throw RequestError.networkError(url, method, new Error("Failed with status 0 (network error or CORS blocked)"));
|
|
2406
2388
|
}
|
|
2407
2389
|
if (!response.ok) {
|
|
2408
|
-
|
|
2390
|
+
// Capture the response body so it's available on the error object
|
|
2391
|
+
// (reads from a clone, so error.response remains readable)
|
|
2392
|
+
throw RequestError.fromResponse(response, url, method, await RequestError.captureBody(response));
|
|
2409
2393
|
}
|
|
2410
|
-
const graphQLOptions = this.
|
|
2394
|
+
const graphQLOptions = this._gql();
|
|
2411
2395
|
const wrappedResponse = new ResponseWrapper(response, url, method, graphQLOptions);
|
|
2412
|
-
return await this.
|
|
2396
|
+
return await this._runResI(wrappedResponse);
|
|
2413
2397
|
}
|
|
2414
2398
|
catch (error) {
|
|
2415
2399
|
// Convert to RequestError if needed
|
|
2416
|
-
|
|
2417
|
-
if (error instanceof RequestError) {
|
|
2418
|
-
requestError = error;
|
|
2419
|
-
}
|
|
2420
|
-
else {
|
|
2421
|
-
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
2422
|
-
requestError = RequestError.networkError(url, method, errorObj);
|
|
2423
|
-
}
|
|
2400
|
+
const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, toError(error));
|
|
2424
2401
|
// Run error interceptors
|
|
2425
|
-
const interceptorResult = await this.
|
|
2402
|
+
const interceptorResult = await this._runErrI(requestError);
|
|
2426
2403
|
// If error interceptor returned a ResponseWrapper, recover from error
|
|
2427
2404
|
if (interceptorResult instanceof ResponseWrapper) {
|
|
2428
2405
|
return interceptorResult;
|
|
@@ -2439,12 +2416,9 @@ class BaseRequest {
|
|
|
2439
2416
|
* Base class for requests that can have a body (POST, PUT, PATCH)
|
|
2440
2417
|
*/
|
|
2441
2418
|
class BodyRequest extends BaseRequest {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
constructor(url) {
|
|
2446
|
-
super(url);
|
|
2447
|
-
}
|
|
2419
|
+
_body;
|
|
2420
|
+
_bodyType;
|
|
2421
|
+
_gqlOpts = undefined;
|
|
2448
2422
|
/**
|
|
2449
2423
|
* Sets the request body. Automatically detects the body type and sets appropriate Content-Type header.
|
|
2450
2424
|
* Supports JSON objects/arrays, strings, FormData, Blob, ArrayBuffer, URLSearchParams, and ReadableStream.
|
|
@@ -2481,11 +2455,11 @@ class BodyRequest extends BaseRequest {
|
|
|
2481
2455
|
* ```
|
|
2482
2456
|
*/
|
|
2483
2457
|
withBody(body) {
|
|
2484
|
-
this.
|
|
2458
|
+
this._body = body;
|
|
2485
2459
|
// Set body type and validate
|
|
2486
2460
|
if (typeof body === "string") {
|
|
2487
|
-
this.
|
|
2488
|
-
this.
|
|
2461
|
+
this._bodyType = BodyType.STRING;
|
|
2462
|
+
this._setCT("text/plain");
|
|
2489
2463
|
}
|
|
2490
2464
|
else if (body !== null &&
|
|
2491
2465
|
typeof body === "object" &&
|
|
@@ -2496,18 +2470,18 @@ class BodyRequest extends BaseRequest {
|
|
|
2496
2470
|
ArrayBuffer.isView(body) || // Handles TypedArray and DataView
|
|
2497
2471
|
body instanceof URLSearchParams ||
|
|
2498
2472
|
body instanceof ReadableStream)) {
|
|
2499
|
-
this.
|
|
2500
|
-
this.
|
|
2473
|
+
this._bodyType = BodyType.JSON;
|
|
2474
|
+
this._setCT("application/json");
|
|
2501
2475
|
// Validate JSON is stringifiable early
|
|
2502
2476
|
try {
|
|
2503
2477
|
JSON.stringify(body);
|
|
2504
2478
|
}
|
|
2505
2479
|
catch (error) {
|
|
2506
|
-
throw new RequestError(`Bad JSON: ${
|
|
2480
|
+
throw new RequestError(`Bad JSON: ${errorMessage(error)}`, this._url, this._method);
|
|
2507
2481
|
}
|
|
2508
2482
|
}
|
|
2509
2483
|
else {
|
|
2510
|
-
this.
|
|
2484
|
+
this._bodyType = BodyType.BINARY;
|
|
2511
2485
|
}
|
|
2512
2486
|
return this;
|
|
2513
2487
|
}
|
|
@@ -2550,25 +2524,25 @@ class BodyRequest extends BaseRequest {
|
|
|
2550
2524
|
*/
|
|
2551
2525
|
withGraphQL(query, variables, options) {
|
|
2552
2526
|
if (typeof query !== "string" || query.length === 0) {
|
|
2553
|
-
throw new RequestError("Bad query", this.
|
|
2527
|
+
throw new RequestError("Bad query", this._url, this._method);
|
|
2554
2528
|
}
|
|
2555
2529
|
const graphQLBody = {
|
|
2556
2530
|
query: query,
|
|
2557
2531
|
};
|
|
2558
2532
|
if (variables !== undefined) {
|
|
2559
2533
|
if (typeof variables !== "object" || variables === null || Array.isArray(variables)) {
|
|
2560
|
-
throw new RequestError("Bad vars", this.
|
|
2534
|
+
throw new RequestError("Bad vars", this._url, this._method);
|
|
2561
2535
|
}
|
|
2562
2536
|
graphQLBody.variables = variables;
|
|
2563
2537
|
}
|
|
2564
2538
|
// Store GraphQL options if provided
|
|
2565
2539
|
if (options !== undefined) {
|
|
2566
2540
|
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
2567
|
-
throw new RequestError("Bad opts", this.
|
|
2541
|
+
throw new RequestError("Bad opts", this._url, this._method);
|
|
2568
2542
|
}
|
|
2569
2543
|
// Store only the known GraphQL options properties
|
|
2570
2544
|
const opts = options;
|
|
2571
|
-
this.
|
|
2545
|
+
this._gqlOpts = {
|
|
2572
2546
|
throwOnError: typeof opts.throwOnError === "boolean" ? opts.throwOnError : undefined,
|
|
2573
2547
|
};
|
|
2574
2548
|
}
|
|
@@ -2577,26 +2551,26 @@ class BodyRequest extends BaseRequest {
|
|
|
2577
2551
|
JSON.stringify(graphQLBody);
|
|
2578
2552
|
}
|
|
2579
2553
|
catch (error) {
|
|
2580
|
-
throw new RequestError(`Bad JSON: ${
|
|
2554
|
+
throw new RequestError(`Bad JSON: ${errorMessage(error)}`, this._url, this._method);
|
|
2581
2555
|
}
|
|
2582
|
-
this.
|
|
2583
|
-
this.
|
|
2584
|
-
this.
|
|
2556
|
+
this._body = graphQLBody;
|
|
2557
|
+
this._bodyType = BodyType.JSON;
|
|
2558
|
+
this._setCT("application/json");
|
|
2585
2559
|
return this;
|
|
2586
2560
|
}
|
|
2587
2561
|
/**
|
|
2588
2562
|
* Check if Content-Type header is already set (case-insensitive)
|
|
2589
2563
|
*/
|
|
2590
|
-
|
|
2591
|
-
const headers = this.
|
|
2564
|
+
_hasCT() {
|
|
2565
|
+
const headers = this._opts.headers;
|
|
2592
2566
|
if (typeof headers === "object" && headers !== null) {
|
|
2593
2567
|
const headersObj = headers;
|
|
2594
2568
|
return Object.keys(headersObj).some(header => header.toLowerCase() === "content-type");
|
|
2595
2569
|
}
|
|
2596
2570
|
return false;
|
|
2597
2571
|
}
|
|
2598
|
-
|
|
2599
|
-
if (!this.
|
|
2572
|
+
_setCT(contentType) {
|
|
2573
|
+
if (!this._hasCT()) {
|
|
2600
2574
|
this.withContentType(contentType);
|
|
2601
2575
|
}
|
|
2602
2576
|
}
|
|
@@ -2604,24 +2578,24 @@ class BodyRequest extends BaseRequest {
|
|
|
2604
2578
|
* Get the GraphQL options if set
|
|
2605
2579
|
* @returns The GraphQL options or undefined
|
|
2606
2580
|
*/
|
|
2607
|
-
|
|
2608
|
-
return this.
|
|
2581
|
+
_gql() {
|
|
2582
|
+
return this._gqlOpts;
|
|
2609
2583
|
}
|
|
2610
2584
|
/**
|
|
2611
2585
|
* Execute the request and return the ResponseWrapper
|
|
2612
2586
|
* Overrides the base implementation to add body handling
|
|
2613
2587
|
*/
|
|
2614
2588
|
async getResponse() {
|
|
2615
|
-
if (this.
|
|
2589
|
+
if (this._body !== undefined) {
|
|
2616
2590
|
// Remove previous body if it exists
|
|
2617
|
-
if (this.
|
|
2618
|
-
delete this.
|
|
2591
|
+
if (this._opts.body)
|
|
2592
|
+
delete this._opts.body;
|
|
2619
2593
|
// Process the body based on its type
|
|
2620
|
-
if (this.
|
|
2621
|
-
this.
|
|
2594
|
+
if (this._bodyType === BodyType.JSON) {
|
|
2595
|
+
this._opts.body = JSON.stringify(this._body);
|
|
2622
2596
|
}
|
|
2623
2597
|
else {
|
|
2624
|
-
this.
|
|
2598
|
+
this._opts.body = this._body;
|
|
2625
2599
|
}
|
|
2626
2600
|
}
|
|
2627
2601
|
return super.getResponse();
|
|
@@ -2638,10 +2612,7 @@ class BodyRequest extends BaseRequest {
|
|
|
2638
2612
|
* const data = await request.getData();
|
|
2639
2613
|
*/
|
|
2640
2614
|
class GetRequest extends BaseRequest {
|
|
2641
|
-
|
|
2642
|
-
constructor(url) {
|
|
2643
|
-
super(url);
|
|
2644
|
-
}
|
|
2615
|
+
_method = HttpMethod.GET;
|
|
2645
2616
|
}
|
|
2646
2617
|
/**
|
|
2647
2618
|
* HTTP HEAD request implementation
|
|
@@ -2652,10 +2623,7 @@ class GetRequest extends BaseRequest {
|
|
|
2652
2623
|
* const response = await request.getResponse();
|
|
2653
2624
|
*/
|
|
2654
2625
|
class HeadRequest extends BaseRequest {
|
|
2655
|
-
|
|
2656
|
-
constructor(url) {
|
|
2657
|
-
super(url);
|
|
2658
|
-
}
|
|
2626
|
+
_method = HttpMethod.HEAD;
|
|
2659
2627
|
}
|
|
2660
2628
|
/**
|
|
2661
2629
|
* HTTP OPTIONS request implementation
|
|
@@ -2666,10 +2634,7 @@ class HeadRequest extends BaseRequest {
|
|
|
2666
2634
|
* const response = await request.getResponse();
|
|
2667
2635
|
*/
|
|
2668
2636
|
class OptionsRequest extends BaseRequest {
|
|
2669
|
-
|
|
2670
|
-
constructor(url) {
|
|
2671
|
-
super(url);
|
|
2672
|
-
}
|
|
2637
|
+
_method = HttpMethod.OPTIONS;
|
|
2673
2638
|
}
|
|
2674
2639
|
/**
|
|
2675
2640
|
* HTTP DELETE request implementation
|
|
@@ -2680,10 +2645,7 @@ class OptionsRequest extends BaseRequest {
|
|
|
2680
2645
|
* await request.getData();
|
|
2681
2646
|
*/
|
|
2682
2647
|
class DeleteRequest extends BaseRequest {
|
|
2683
|
-
|
|
2684
|
-
constructor(url) {
|
|
2685
|
-
super(url);
|
|
2686
|
-
}
|
|
2648
|
+
_method = HttpMethod.DELETE;
|
|
2687
2649
|
}
|
|
2688
2650
|
/**
|
|
2689
2651
|
* HTTP POST request implementation
|
|
@@ -2695,10 +2657,7 @@ class DeleteRequest extends BaseRequest {
|
|
|
2695
2657
|
* const data = await request.getData();
|
|
2696
2658
|
*/
|
|
2697
2659
|
class PostRequest extends BodyRequest {
|
|
2698
|
-
|
|
2699
|
-
constructor(url) {
|
|
2700
|
-
super(url);
|
|
2701
|
-
}
|
|
2660
|
+
_method = HttpMethod.POST;
|
|
2702
2661
|
}
|
|
2703
2662
|
/**
|
|
2704
2663
|
* HTTP PUT request implementation
|
|
@@ -2710,10 +2669,7 @@ class PostRequest extends BodyRequest {
|
|
|
2710
2669
|
* const data = await request.getData();
|
|
2711
2670
|
*/
|
|
2712
2671
|
class PutRequest extends BodyRequest {
|
|
2713
|
-
|
|
2714
|
-
constructor(url) {
|
|
2715
|
-
super(url);
|
|
2716
|
-
}
|
|
2672
|
+
_method = HttpMethod.PUT;
|
|
2717
2673
|
}
|
|
2718
2674
|
/**
|
|
2719
2675
|
* HTTP PATCH request implementation
|
|
@@ -2725,10 +2681,7 @@ class PutRequest extends BodyRequest {
|
|
|
2725
2681
|
* const data = await request.getData();
|
|
2726
2682
|
*/
|
|
2727
2683
|
class PatchRequest extends BodyRequest {
|
|
2728
|
-
|
|
2729
|
-
constructor(url) {
|
|
2730
|
-
super(url);
|
|
2731
|
-
}
|
|
2684
|
+
_method = HttpMethod.PATCH;
|
|
2732
2685
|
}
|
|
2733
2686
|
|
|
2734
2687
|
/**
|
|
@@ -2840,73 +2793,60 @@ function options(url) {
|
|
|
2840
2793
|
* Internal API builder implementation.
|
|
2841
2794
|
*/
|
|
2842
2795
|
class ApiBuilderImpl {
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2796
|
+
_baseURL;
|
|
2797
|
+
_mods = [];
|
|
2798
|
+
_proxy;
|
|
2846
2799
|
withBaseURL(baseURL) {
|
|
2847
|
-
this.
|
|
2848
|
-
return this.
|
|
2800
|
+
this._baseURL = baseURL;
|
|
2801
|
+
return this._getProxy();
|
|
2849
2802
|
}
|
|
2850
|
-
|
|
2803
|
+
_resolve(url) {
|
|
2851
2804
|
if (!url)
|
|
2852
|
-
return this.
|
|
2805
|
+
return this._baseURL || "";
|
|
2853
2806
|
if (/^https?:\/\//.test(url))
|
|
2854
2807
|
return url;
|
|
2855
|
-
if (!this.
|
|
2808
|
+
if (!this._baseURL)
|
|
2856
2809
|
return url;
|
|
2857
|
-
return this.
|
|
2810
|
+
return this._baseURL.replace(/\/$/, "") + (url[0] === "/" ? url : "/" + url);
|
|
2858
2811
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2812
|
+
_new(Ctor, url) {
|
|
2813
|
+
const request = new Ctor(this._resolve(url));
|
|
2814
|
+
for (const modifier of this._mods)
|
|
2815
|
+
modifier(request);
|
|
2816
|
+
return request;
|
|
2863
2817
|
}
|
|
2864
2818
|
get(url) {
|
|
2865
|
-
|
|
2866
|
-
this.applyModifiers(request);
|
|
2867
|
-
return request;
|
|
2819
|
+
return this._new(GetRequest, url);
|
|
2868
2820
|
}
|
|
2869
2821
|
post(url) {
|
|
2870
|
-
|
|
2871
|
-
this.applyModifiers(request);
|
|
2872
|
-
return request;
|
|
2822
|
+
return this._new(PostRequest, url);
|
|
2873
2823
|
}
|
|
2874
2824
|
put(url) {
|
|
2875
|
-
|
|
2876
|
-
this.applyModifiers(request);
|
|
2877
|
-
return request;
|
|
2825
|
+
return this._new(PutRequest, url);
|
|
2878
2826
|
}
|
|
2879
2827
|
del(url) {
|
|
2880
|
-
|
|
2881
|
-
this.applyModifiers(request);
|
|
2882
|
-
return request;
|
|
2828
|
+
return this._new(DeleteRequest, url);
|
|
2883
2829
|
}
|
|
2884
2830
|
patch(url) {
|
|
2885
|
-
|
|
2886
|
-
this.applyModifiers(request);
|
|
2887
|
-
return request;
|
|
2831
|
+
return this._new(PatchRequest, url);
|
|
2888
2832
|
}
|
|
2889
2833
|
head(url) {
|
|
2890
|
-
|
|
2891
|
-
this.applyModifiers(request);
|
|
2892
|
-
return request;
|
|
2834
|
+
return this._new(HeadRequest, url);
|
|
2893
2835
|
}
|
|
2894
2836
|
options(url) {
|
|
2895
|
-
|
|
2896
|
-
this.applyModifiers(request);
|
|
2897
|
-
return request;
|
|
2837
|
+
return this._new(OptionsRequest, url);
|
|
2898
2838
|
}
|
|
2899
|
-
|
|
2900
|
-
this.
|
|
2901
|
-
return this.
|
|
2839
|
+
_add(modifier) {
|
|
2840
|
+
this._mods.push(modifier);
|
|
2841
|
+
return this._getProxy();
|
|
2902
2842
|
}
|
|
2903
|
-
|
|
2904
|
-
if (!this.
|
|
2905
|
-
this.
|
|
2843
|
+
_getProxy() {
|
|
2844
|
+
if (!this._proxy) {
|
|
2845
|
+
this._proxy = this._mkProxy();
|
|
2906
2846
|
}
|
|
2907
|
-
return this.
|
|
2847
|
+
return this._proxy;
|
|
2908
2848
|
}
|
|
2909
|
-
|
|
2849
|
+
_mkProxy() {
|
|
2910
2850
|
const disallowedMethods = new Set(["withBody", "withGraphQL", "withAbortController"]);
|
|
2911
2851
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
2912
2852
|
return new Proxy(this, {
|
|
@@ -2928,7 +2868,7 @@ class ApiBuilderImpl {
|
|
|
2928
2868
|
// If it's a 'with...' method or other chainable method, create a modifier for it
|
|
2929
2869
|
if (typeof prop === "string" && (prop.startsWith("with") || prop === "onRetry")) {
|
|
2930
2870
|
return (...args) => {
|
|
2931
|
-
return implTarget.
|
|
2871
|
+
return implTarget._add((request) => {
|
|
2932
2872
|
const method = request[prop];
|
|
2933
2873
|
if (typeof method === "function") {
|
|
2934
2874
|
method.apply(request, args);
|
|
@@ -2943,7 +2883,7 @@ class ApiBuilderImpl {
|
|
|
2943
2883
|
});
|
|
2944
2884
|
}
|
|
2945
2885
|
static create() {
|
|
2946
|
-
return new ApiBuilderImpl().
|
|
2886
|
+
return new ApiBuilderImpl()._getProxy();
|
|
2947
2887
|
}
|
|
2948
2888
|
}
|
|
2949
2889
|
/**
|
|
@@ -3024,16 +2964,24 @@ const create = {
|
|
|
3024
2964
|
config: Config.getInstance(),
|
|
3025
2965
|
};
|
|
3026
2966
|
|
|
2967
|
+
exports.CacheMode = CacheMode;
|
|
3027
2968
|
exports.CookieUtils = CookieUtils;
|
|
2969
|
+
exports.CredentialsPolicy = CredentialsPolicy;
|
|
3028
2970
|
exports.DeleteRequest = DeleteRequest;
|
|
3029
2971
|
exports.GetRequest = GetRequest;
|
|
3030
2972
|
exports.HeadRequest = HeadRequest;
|
|
2973
|
+
exports.HttpMethod = HttpMethod;
|
|
3031
2974
|
exports.OptionsRequest = OptionsRequest;
|
|
3032
2975
|
exports.PatchRequest = PatchRequest;
|
|
3033
2976
|
exports.PostRequest = PostRequest;
|
|
3034
2977
|
exports.PutRequest = PutRequest;
|
|
2978
|
+
exports.RedirectMode = RedirectMode;
|
|
2979
|
+
exports.ReferrerPolicy = ReferrerPolicy;
|
|
3035
2980
|
exports.RequestError = RequestError;
|
|
2981
|
+
exports.RequestMode = RequestMode;
|
|
2982
|
+
exports.RequestPriority = RequestPriority;
|
|
3036
2983
|
exports.ResponseWrapper = ResponseWrapper;
|
|
2984
|
+
exports.SameSitePolicy = SameSitePolicy;
|
|
3037
2985
|
exports.createApi = api;
|
|
3038
2986
|
exports.createDelete = del;
|
|
3039
2987
|
exports.createGet = get;
|