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