create-request 1.6.1 → 2.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/MIGRATION.md +158 -0
- package/README.md +422 -1292
- package/dist/index.cjs +730 -0
- package/dist/index.d.cts +851 -0
- package/dist/index.d.ts +851 -0
- package/dist/index.js +717 -0
- package/package.json +63 -67
- package/dist/library/BaseRequest.d.ts +0 -870
- package/dist/library/BodyRequest.d.ts +0 -100
- package/dist/library/RequestError.d.ts +0 -181
- package/dist/library/ResponseWrapper.d.ts +0 -193
- package/dist/library/apiBuilder.d.ts +0 -560
- package/dist/library/enums.d.ts +0 -94
- package/dist/library/index.cjs +0 -2994
- package/dist/library/index.cjs.map +0 -1
- package/dist/library/index.d.ts +0 -47
- package/dist/library/index.esm.js +0 -2966
- package/dist/library/index.esm.js.map +0 -1
- package/dist/library/index.esm.min.js +0 -1
- package/dist/library/index.esm.min.js.map +0 -1
- package/dist/library/index.min.cjs +0 -1
- package/dist/library/index.min.cjs.map +0 -1
- package/dist/library/requestFactories.d.ts +0 -91
- package/dist/library/requestMethods.d.ts +0 -84
- package/dist/library/types.d.ts +0 -329
- package/dist/library/utils/Config.d.ts +0 -221
- package/dist/library/utils/CookieUtils.d.ts +0 -9
- package/dist/library/utils/CsrfUtils.d.ts +0 -24
package/dist/library/index.cjs
DELETED
|
@@ -1,2994 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Enum for HTTP methods
|
|
5
|
-
*/
|
|
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
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Enum for request priorities
|
|
17
|
-
*/
|
|
18
|
-
const RequestPriority = {
|
|
19
|
-
LOW: "low",
|
|
20
|
-
HIGH: "high",
|
|
21
|
-
AUTO: "auto",
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Enum for credentials policies
|
|
25
|
-
*/
|
|
26
|
-
const CredentialsPolicy = {
|
|
27
|
-
OMIT: "omit",
|
|
28
|
-
INCLUDE: "include",
|
|
29
|
-
SAME_ORIGIN: "same-origin",
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Enum for request modes
|
|
33
|
-
*/
|
|
34
|
-
const RequestMode = {
|
|
35
|
-
CORS: "cors",
|
|
36
|
-
NO_CORS: "no-cors",
|
|
37
|
-
SAME_ORIGIN: "same-origin",
|
|
38
|
-
NAVIGATE: "navigate",
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Enum for redirect modes
|
|
42
|
-
*/
|
|
43
|
-
const RedirectMode = {
|
|
44
|
-
ERROR: "error",
|
|
45
|
-
FOLLOW: "follow",
|
|
46
|
-
MANUAL: "manual",
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* Enum for cookie SameSite policies
|
|
50
|
-
*/
|
|
51
|
-
const SameSitePolicy = {
|
|
52
|
-
LAX: "Lax",
|
|
53
|
-
NONE: "None",
|
|
54
|
-
STRICT: "Strict",
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* Enum for body types
|
|
58
|
-
*/
|
|
59
|
-
const BodyType = {
|
|
60
|
-
JSON: "json",
|
|
61
|
-
STRING: "string",
|
|
62
|
-
BINARY: "binary",
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Referrer policies for fetch requests
|
|
66
|
-
*/
|
|
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
|
-
};
|
|
77
|
-
/**
|
|
78
|
-
* Cache modes for fetch requests
|
|
79
|
-
*/
|
|
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
|
-
};
|
|
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)));
|
|
99
|
-
/**
|
|
100
|
-
* Error class for HTTP request failures.
|
|
101
|
-
* Extends the standard Error class with additional context about the failed request.
|
|
102
|
-
*
|
|
103
|
-
* @example
|
|
104
|
-
* ```typescript
|
|
105
|
-
* try {
|
|
106
|
-
* await create.get('/api/users').getJson();
|
|
107
|
-
* } catch (error) {
|
|
108
|
-
* console.log(`Request failed: ${error.message}`);
|
|
109
|
-
* console.log(`URL: ${error.url}`);
|
|
110
|
-
* console.log(`Method: ${error.method}`);
|
|
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)
|
|
114
|
-
* console.log(`Is timeout: ${error.isTimeout}`);
|
|
115
|
-
* console.log(`Is aborted: ${error.isAborted}`);
|
|
116
|
-
* }
|
|
117
|
-
* ```
|
|
118
|
-
*/
|
|
119
|
-
class RequestError extends Error {
|
|
120
|
-
/** HTTP status code if the request received a response (e.g., 404, 500) */
|
|
121
|
-
status;
|
|
122
|
-
/** The Response object if the request received a response before failing */
|
|
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;
|
|
130
|
-
/** The URL that was requested */
|
|
131
|
-
url;
|
|
132
|
-
/** The HTTP method that was used (e.g., 'GET', 'POST') */
|
|
133
|
-
method;
|
|
134
|
-
/** Whether the request failed due to a timeout */
|
|
135
|
-
isTimeout;
|
|
136
|
-
/** Whether the request was aborted (cancelled) */
|
|
137
|
-
isAborted;
|
|
138
|
-
/** Cached result of parsing `body` as JSON (lazily populated by getJson) */
|
|
139
|
-
_parsed;
|
|
140
|
-
/**
|
|
141
|
-
* Creates a new RequestError instance.
|
|
142
|
-
*
|
|
143
|
-
* @param message - Error message describing what went wrong
|
|
144
|
-
* @param url - The URL that was requested
|
|
145
|
-
* @param method - The HTTP method that was used
|
|
146
|
-
* @param options - Additional error context
|
|
147
|
-
* @param options.status - HTTP status code if available
|
|
148
|
-
* @param options.response - The Response object if available
|
|
149
|
-
* @param options.body - The raw response body as text, if available
|
|
150
|
-
* @param options.isTimeout - Whether this was a timeout error
|
|
151
|
-
* @param options.isAborted - Whether the request was aborted
|
|
152
|
-
* @param options.cause - The underlying error that caused this error
|
|
153
|
-
*/
|
|
154
|
-
constructor(message, url, method, options = {}) {
|
|
155
|
-
super(message, { cause: options.cause });
|
|
156
|
-
this.name = "RequestError";
|
|
157
|
-
this.url = url;
|
|
158
|
-
this.method = method;
|
|
159
|
-
this.status = options.status;
|
|
160
|
-
this.response = options.response;
|
|
161
|
-
this.body = options.body;
|
|
162
|
-
this.isTimeout = !!options.isTimeout;
|
|
163
|
-
this.isAborted = !!options.isAborted;
|
|
164
|
-
// For better stack traces in modern environments
|
|
165
|
-
if (Error.captureStackTrace) {
|
|
166
|
-
Error.captureStackTrace(this, RequestError);
|
|
167
|
-
}
|
|
168
|
-
// Maintains proper prototype chain for instanceof checks
|
|
169
|
-
Object.setPrototypeOf(this, RequestError.prototype);
|
|
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
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Creates a RequestError for a timeout failure.
|
|
229
|
-
*
|
|
230
|
-
* @param url - The URL that timed out
|
|
231
|
-
* @param method - The HTTP method that was used
|
|
232
|
-
* @param timeoutMs - The timeout duration in milliseconds
|
|
233
|
-
* @returns A RequestError with `isTimeout` set to `true`
|
|
234
|
-
*
|
|
235
|
-
* @example
|
|
236
|
-
* ```typescript
|
|
237
|
-
* throw RequestError.timeout('/api/data', 'GET', 5000);
|
|
238
|
-
* ```
|
|
239
|
-
*/
|
|
240
|
-
static timeout(url, method, timeoutMs) {
|
|
241
|
-
return new RequestError(`Timeout:${timeoutMs}`, url, method, {
|
|
242
|
-
isTimeout: true,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Creates a RequestError from an HTTP error response.
|
|
247
|
-
* Used when the server returns a non-2xx status code.
|
|
248
|
-
*
|
|
249
|
-
* @param response - The Response object from the failed request
|
|
250
|
-
* @param url - The URL that was requested
|
|
251
|
-
* @param method - The HTTP method that was used
|
|
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)
|
|
254
|
-
*
|
|
255
|
-
* @example
|
|
256
|
-
* ```typescript
|
|
257
|
-
* const response = await fetch('/api/users');
|
|
258
|
-
* if (!response.ok) {
|
|
259
|
-
* const body = await RequestError.captureBody(response);
|
|
260
|
-
* throw RequestError.fromResponse(response, '/api/users', 'GET', body);
|
|
261
|
-
* }
|
|
262
|
-
* ```
|
|
263
|
-
*/
|
|
264
|
-
static fromResponse(response, url, method, body) {
|
|
265
|
-
return new RequestError(`HTTP ${response.status}`, url, method, {
|
|
266
|
-
status: response.status,
|
|
267
|
-
response,
|
|
268
|
-
body,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Creates a RequestError from a network-level error.
|
|
273
|
-
* Automatically detects and categorizes common network errors (timeouts, DNS errors, connection errors).
|
|
274
|
-
*
|
|
275
|
-
* @param url - The URL that failed
|
|
276
|
-
* @param method - The HTTP method that was used
|
|
277
|
-
* @param originalError - The original error that occurred (e.g., from fetch)
|
|
278
|
-
* @returns A RequestError with enhanced error message and context
|
|
279
|
-
*
|
|
280
|
-
* @example
|
|
281
|
-
* ```typescript
|
|
282
|
-
* try {
|
|
283
|
-
* await fetch('/api/data');
|
|
284
|
-
* } catch (error) {
|
|
285
|
-
* if (error instanceof Error) {
|
|
286
|
-
* throw RequestError.networkError('/api/data', 'GET', error);
|
|
287
|
-
* }
|
|
288
|
-
* }
|
|
289
|
-
* ```
|
|
290
|
-
*/
|
|
291
|
-
static networkError(url, method, originalError) {
|
|
292
|
-
// Provide more descriptive error messages for common network errors
|
|
293
|
-
let message = originalError.message;
|
|
294
|
-
// Check for Node.js error codes (e.g., from undici/dns errors)
|
|
295
|
-
const errorCode = originalError.code;
|
|
296
|
-
const stack = originalError.stack || "";
|
|
297
|
-
// Check for timeout errors (Node.js/undici TimeoutError)
|
|
298
|
-
// Note: While explicit timeouts set via withTimeout() are handled in BaseRequest,
|
|
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") ||
|
|
303
|
-
errorCode === "ETIMEDOUT" ||
|
|
304
|
-
stack.includes("TimeoutError") ||
|
|
305
|
-
stack.includes("timeout");
|
|
306
|
-
// If the error message is generic "fetch failed", provide more context
|
|
307
|
-
if (message === "fetch failed" || message === "Failed to fetch") {
|
|
308
|
-
// Check for DNS resolution errors
|
|
309
|
-
const isDnsError = errorCode === "ENOTFOUND" || errorCode === "EAI_AGAIN" || errorCode === "EAI_NODATA" || /getaddrinfo|ENOTFOUND|EAI_AGAIN/.test(stack);
|
|
310
|
-
// Check for connection errors (but not timeout errors)
|
|
311
|
-
const isConnectionError = !isTimeoutError && (errorCode === "ECONNREFUSED" || errorCode === "ECONNRESET" || stack.includes("ECONNREFUSED") || stack.includes("connect"));
|
|
312
|
-
message = (isTimeoutError ? "Timeout:" : isDnsError ? "DNS:" : isConnectionError ? "Conn:" : "Net:") + url;
|
|
313
|
-
}
|
|
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: ..."
|
|
317
|
-
if (originalError.stack) {
|
|
318
|
-
error.stack = `${error.stack || ""}\n\nCaused by: ${originalError.stack}`;
|
|
319
|
-
}
|
|
320
|
-
return error;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Creates a RequestError for an aborted (cancelled) request.
|
|
324
|
-
*
|
|
325
|
-
* @param url - The URL that was aborted
|
|
326
|
-
* @param method - The HTTP method that was used
|
|
327
|
-
* @returns A RequestError with `isAborted` set to `true`
|
|
328
|
-
*
|
|
329
|
-
* @example
|
|
330
|
-
* ```typescript
|
|
331
|
-
* const controller = new AbortController();
|
|
332
|
-
* controller.abort();
|
|
333
|
-
* throw RequestError.abortError('/api/data', 'GET');
|
|
334
|
-
* ```
|
|
335
|
-
*/
|
|
336
|
-
static abortError(url, method) {
|
|
337
|
-
return new RequestError("Aborted", url, method, {
|
|
338
|
-
isAborted: true,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Wrapper for HTTP responses with methods to transform the response data.
|
|
345
|
-
* Provides convenient methods to parse the response body in different formats.
|
|
346
|
-
* Response bodies are cached after the first read, so you can call multiple methods
|
|
347
|
-
* (e.g., `getJson()` and `getText()`) on the same response.
|
|
348
|
-
*
|
|
349
|
-
* @example
|
|
350
|
-
* ```typescript
|
|
351
|
-
* const response = await create.get('/api/users').getResponse();
|
|
352
|
-
* console.log(response.status); // 200
|
|
353
|
-
* console.log(response.ok); // true
|
|
354
|
-
* const data = await response.getJson();
|
|
355
|
-
* ```
|
|
356
|
-
*/
|
|
357
|
-
class ResponseWrapper {
|
|
358
|
-
/** The URL that was requested (if available) */
|
|
359
|
-
url;
|
|
360
|
-
/** The HTTP method that was used (if available) */
|
|
361
|
-
method;
|
|
362
|
-
_res;
|
|
363
|
-
_gqlOpts;
|
|
364
|
-
// Cache the body as the last used method
|
|
365
|
-
_blob;
|
|
366
|
-
_text;
|
|
367
|
-
_json;
|
|
368
|
-
_buf;
|
|
369
|
-
constructor(response, url, method, graphQLOptions) {
|
|
370
|
-
this._res = response;
|
|
371
|
-
this.url = url;
|
|
372
|
-
this.method = method;
|
|
373
|
-
if (graphQLOptions) {
|
|
374
|
-
this._gqlOpts = {
|
|
375
|
-
throwOnError: graphQLOptions.throwOnError,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* HTTP status code (e.g., 200, 404, 500)
|
|
381
|
-
*/
|
|
382
|
-
get status() {
|
|
383
|
-
return this._res.status;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* HTTP status text (e.g., "OK", "Not Found", "Internal Server Error")
|
|
387
|
-
*/
|
|
388
|
-
get statusText() {
|
|
389
|
-
return this._res.statusText;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Response headers as a Headers object
|
|
393
|
-
*/
|
|
394
|
-
get headers() {
|
|
395
|
-
return this._res.headers;
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Whether the response status is in the 200-299 range (successful)
|
|
399
|
-
*/
|
|
400
|
-
get ok() {
|
|
401
|
-
return this._res.ok;
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* The raw Response object from the fetch API.
|
|
405
|
-
* Use this if you need direct access to the underlying Response.
|
|
406
|
-
*/
|
|
407
|
-
get raw() {
|
|
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
|
-
}
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Check if the response body has already been consumed and throw an error if so
|
|
437
|
-
* @throws RequestError if the body has already been consumed
|
|
438
|
-
*/
|
|
439
|
-
_checkUsed() {
|
|
440
|
-
if (this._res.bodyUsed) {
|
|
441
|
-
throw this._err("Body used");
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Check for GraphQL errors and throw if throwOnError is enabled
|
|
446
|
-
* @param data - The parsed JSON data
|
|
447
|
-
* @throws RequestError if GraphQL response contains errors and throwOnError is enabled
|
|
448
|
-
*/
|
|
449
|
-
_checkGql(data) {
|
|
450
|
-
if (!this._gqlOpts?.throwOnError || typeof data !== "object" || data === null)
|
|
451
|
-
return;
|
|
452
|
-
const responseData = data;
|
|
453
|
-
if (!Array.isArray(responseData.errors) || responseData.errors.length === 0)
|
|
454
|
-
return;
|
|
455
|
-
const errors = responseData.errors;
|
|
456
|
-
const errorMessages = errors.map(x => {
|
|
457
|
-
if (typeof x === "string")
|
|
458
|
-
return x;
|
|
459
|
-
if (x && typeof x === "object" && "message" in x) {
|
|
460
|
-
const message = x.message;
|
|
461
|
-
if (message == null)
|
|
462
|
-
return "Unknown error";
|
|
463
|
-
if (typeof message === "string")
|
|
464
|
-
return message;
|
|
465
|
-
if (typeof message === "object") {
|
|
466
|
-
try {
|
|
467
|
-
return JSON.stringify(message);
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
return "Unknown error";
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
// For primitives (number, boolean, etc.), safe to convert
|
|
474
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
475
|
-
return String(message);
|
|
476
|
-
}
|
|
477
|
-
return String(x);
|
|
478
|
-
});
|
|
479
|
-
throw this._err(`GQL: ${errorMessages.join(", ")}`, true);
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Parse the response body as JSON
|
|
483
|
-
* If GraphQL options are set with throwOnError=true, will check for GraphQL errors and throw.
|
|
484
|
-
*
|
|
485
|
-
* Returns `null` for empty responses (204 No Content, content-length: 0, or empty body).
|
|
486
|
-
* This handles common API patterns where PUT/DELETE operations return no content on success.
|
|
487
|
-
*
|
|
488
|
-
* @returns The parsed JSON data, or `null` for empty responses
|
|
489
|
-
* @throws {RequestError} When the request fails, JSON parsing fails, or GraphQL errors occur (if throwOnError enabled).
|
|
490
|
-
*
|
|
491
|
-
* @example
|
|
492
|
-
* const data = await response.getJson();
|
|
493
|
-
* if (data !== null) {
|
|
494
|
-
* console.log(data.items);
|
|
495
|
-
* }
|
|
496
|
-
*
|
|
497
|
-
* @example
|
|
498
|
-
* // Error handling - errors are always RequestError
|
|
499
|
-
* try {
|
|
500
|
-
* const data = await response.getJson();
|
|
501
|
-
* } catch (error) {
|
|
502
|
-
* if (error instanceof RequestError) {
|
|
503
|
-
* console.log(error.status, error.url, error.method);
|
|
504
|
-
* }
|
|
505
|
-
* }
|
|
506
|
-
*/
|
|
507
|
-
async getJson() {
|
|
508
|
-
if (this._json !== undefined)
|
|
509
|
-
return this._json;
|
|
510
|
-
// Handle empty responses: 204 No Content or content-length: 0
|
|
511
|
-
const contentLength = this._res.headers.get("content-length");
|
|
512
|
-
if (this._res.status === 204 || contentLength === "0") {
|
|
513
|
-
this._json = null;
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
this._checkUsed();
|
|
517
|
-
try {
|
|
518
|
-
// Read as text first to handle empty bodies and cache for getText()
|
|
519
|
-
const text = await this._res.text();
|
|
520
|
-
this._text = text;
|
|
521
|
-
// Handle empty or whitespace-only responses
|
|
522
|
-
if (!text || text.trim() === "") {
|
|
523
|
-
this._json = null;
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
// Parse the text as JSON
|
|
527
|
-
const parsed = JSON.parse(text);
|
|
528
|
-
this._json = parsed;
|
|
529
|
-
this._checkGql(parsed);
|
|
530
|
-
return parsed;
|
|
531
|
-
}
|
|
532
|
-
catch (error) {
|
|
533
|
-
if (error instanceof RequestError) {
|
|
534
|
-
throw error;
|
|
535
|
-
}
|
|
536
|
-
throw this._err(`Bad JSON: ${errorMessage(error)}`, true);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Get the response body as text.
|
|
541
|
-
* The result is cached, so subsequent calls return the same value without re-reading the body.
|
|
542
|
-
*
|
|
543
|
-
* @returns A promise that resolves to the response body as a string
|
|
544
|
-
* @throws {RequestError} When the body has already been consumed or reading fails
|
|
545
|
-
*
|
|
546
|
-
* @example
|
|
547
|
-
* ```typescript
|
|
548
|
-
* const text = await response.getText();
|
|
549
|
-
* console.log(text); // "Hello, world!"
|
|
550
|
-
* ```
|
|
551
|
-
*/
|
|
552
|
-
async getText() {
|
|
553
|
-
if (this._text !== undefined)
|
|
554
|
-
return this._text;
|
|
555
|
-
return (this._text = await this._read(() => this._res.text()));
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Get the response body as a Blob.
|
|
559
|
-
* Useful for downloading files or handling binary data.
|
|
560
|
-
* The result is cached, so subsequent calls return the same value without re-reading the body.
|
|
561
|
-
*
|
|
562
|
-
* @returns A promise that resolves to the response body as a Blob
|
|
563
|
-
* @throws {RequestError} When the body has already been consumed or reading fails
|
|
564
|
-
*
|
|
565
|
-
* @example
|
|
566
|
-
* ```typescript
|
|
567
|
-
* const blob = await response.getBlob();
|
|
568
|
-
* const url = URL.createObjectURL(blob);
|
|
569
|
-
* // Use the blob URL for downloading or displaying
|
|
570
|
-
* ```
|
|
571
|
-
*/
|
|
572
|
-
async getBlob() {
|
|
573
|
-
if (this._blob !== undefined)
|
|
574
|
-
return this._blob;
|
|
575
|
-
return (this._blob = await this._read(() => this._res.blob()));
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Get the response body as an ArrayBuffer.
|
|
579
|
-
* Useful for processing binary data at a low level.
|
|
580
|
-
* The result is cached, so subsequent calls return the same value without re-reading the body.
|
|
581
|
-
*
|
|
582
|
-
* @returns A promise that resolves to the response body as an ArrayBuffer
|
|
583
|
-
* @throws {RequestError} When the body has already been consumed or reading fails
|
|
584
|
-
*
|
|
585
|
-
* @example
|
|
586
|
-
* ```typescript
|
|
587
|
-
* const buffer = await response.getArrayBuffer();
|
|
588
|
-
* const uint8Array = new Uint8Array(buffer);
|
|
589
|
-
* // Process the binary data
|
|
590
|
-
* ```
|
|
591
|
-
*/
|
|
592
|
-
async getArrayBuffer() {
|
|
593
|
-
if (this._buf !== undefined) {
|
|
594
|
-
return this._buf;
|
|
595
|
-
}
|
|
596
|
-
return (this._buf = await this._read(() => this._res.arrayBuffer()));
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Get the raw response body as a ReadableStream
|
|
600
|
-
* Note: This consumes the response body and should only be called once.
|
|
601
|
-
* Unlike other methods, streams cannot be cached, so this will throw if the body is already consumed.
|
|
602
|
-
*
|
|
603
|
-
* @returns The response body as a ReadableStream or null
|
|
604
|
-
* @throws {RequestError} When the response body has already been consumed
|
|
605
|
-
*
|
|
606
|
-
* @example
|
|
607
|
-
* const stream = response.getBody();
|
|
608
|
-
* if (stream) {
|
|
609
|
-
* const reader = stream.getReader();
|
|
610
|
-
* // Process the stream
|
|
611
|
-
* }
|
|
612
|
-
*/
|
|
613
|
-
getBody() {
|
|
614
|
-
this._checkUsed();
|
|
615
|
-
return this._res.body;
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* Extract specific data using a selector function
|
|
619
|
-
* If no selector is provided, returns the full JSON response.
|
|
620
|
-
*
|
|
621
|
-
* Returns `null` for empty responses (204 No Content, content-length: 0, or empty body).
|
|
622
|
-
* If a selector is provided and data is `null`, the selector will receive `null`.
|
|
623
|
-
*
|
|
624
|
-
* @param selector - Optional function to extract and transform data
|
|
625
|
-
* @returns A promise that resolves to the selected data, or `null` for empty responses
|
|
626
|
-
* @throws {RequestError} When the request fails, JSON parsing fails, or the selector throws an error
|
|
627
|
-
*
|
|
628
|
-
* @example
|
|
629
|
-
* // Get full response
|
|
630
|
-
* const data = await response.getData();
|
|
631
|
-
* if (data !== null) {
|
|
632
|
-
* console.log(data.items);
|
|
633
|
-
* }
|
|
634
|
-
*
|
|
635
|
-
* @example
|
|
636
|
-
* // Extract specific data (use null-safe selector for empty responses)
|
|
637
|
-
* const users = await response.getData(data => data?.results?.users);
|
|
638
|
-
*
|
|
639
|
-
* @example
|
|
640
|
-
* // Error handling - errors are always RequestError
|
|
641
|
-
* try {
|
|
642
|
-
* const data = await response.getData();
|
|
643
|
-
* } catch (error) {
|
|
644
|
-
* if (error instanceof RequestError) {
|
|
645
|
-
* console.log(error.status, error.url, error.method);
|
|
646
|
-
* }
|
|
647
|
-
* }
|
|
648
|
-
*/
|
|
649
|
-
async getData(selector) {
|
|
650
|
-
try {
|
|
651
|
-
const data = await this.getJson();
|
|
652
|
-
// If no selector is provided, return the raw JSON data (may be null)
|
|
653
|
-
if (!selector)
|
|
654
|
-
return data;
|
|
655
|
-
// Apply the selector if provided (selector receives null for empty responses)
|
|
656
|
-
return selector(data);
|
|
657
|
-
}
|
|
658
|
-
catch (error) {
|
|
659
|
-
// If it's already a RequestError, re-throw it
|
|
660
|
-
if (error instanceof RequestError) {
|
|
661
|
-
throw error;
|
|
662
|
-
}
|
|
663
|
-
// Enhance selector errors with context
|
|
664
|
-
if (selector) {
|
|
665
|
-
throw this._err(`Selector: ${errorMessage(error)}`, true);
|
|
666
|
-
}
|
|
667
|
-
// If we get here and it's not a RequestError, wrap it
|
|
668
|
-
// This should rarely happen as getJson() should throw RequestError
|
|
669
|
-
throw RequestError.networkError(this.url || "", this.method || "", toError(error));
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
class CookieUtils {
|
|
675
|
-
/**
|
|
676
|
-
* Formats cookies for a request
|
|
677
|
-
* @param cookies Object containing cookie name-value pairs or cookie options
|
|
678
|
-
* @returns Formatted cookie string for the Cookie header
|
|
679
|
-
*/
|
|
680
|
-
static formatRequestCookies(cookies) {
|
|
681
|
-
return Object.entries(cookies)
|
|
682
|
-
.map(([name, valueOrOptions]) => `${encodeURIComponent(name)}=${encodeURIComponent(typeof valueOrOptions === "string" ? valueOrOptions : valueOrOptions.value)}`)
|
|
683
|
-
.join("; ");
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Utility class for CSRF token management
|
|
689
|
-
*/
|
|
690
|
-
class CsrfUtils {
|
|
691
|
-
/**
|
|
692
|
-
* Extracts CSRF token from a meta tag in the document head
|
|
693
|
-
* @param metaName The name attribute of the meta tag (default: "csrf-token")
|
|
694
|
-
* @returns The CSRF token or null if not found
|
|
695
|
-
*/
|
|
696
|
-
static getTokenFromMeta(metaName = "csrf-token") {
|
|
697
|
-
if (typeof document === "undefined") {
|
|
698
|
-
return null;
|
|
699
|
-
}
|
|
700
|
-
const meta = document.querySelector(`meta[name="${metaName}"]`);
|
|
701
|
-
return meta?.getAttribute("content") || null;
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Extracts CSRF token from a cookie
|
|
705
|
-
* @param cookieName The name of the cookie containing the CSRF token
|
|
706
|
-
* @returns The CSRF token or null if not found
|
|
707
|
-
*/
|
|
708
|
-
static getTokenFromCookie(cookieName = "csrf-token") {
|
|
709
|
-
if (typeof document === "undefined") {
|
|
710
|
-
return null;
|
|
711
|
-
}
|
|
712
|
-
const cookies = document.cookie.split(";");
|
|
713
|
-
for (const cookie of cookies) {
|
|
714
|
-
const [name, value] = cookie.trim().split("=");
|
|
715
|
-
if (name === cookieName) {
|
|
716
|
-
return decodeURIComponent(value);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Validates if the provided string is a potential CSRF token
|
|
723
|
-
* Checks if the token meets security requirements
|
|
724
|
-
* @param token The token to validate
|
|
725
|
-
* @returns Whether the token is valid
|
|
726
|
-
*/
|
|
727
|
-
static isValidToken(token) {
|
|
728
|
-
// Basic checks for null/undefined and type
|
|
729
|
-
if (typeof token !== "string") {
|
|
730
|
-
return false;
|
|
731
|
-
}
|
|
732
|
-
if (token.length < 8)
|
|
733
|
-
return false;
|
|
734
|
-
// If token is longer than 10 chars, perform additional security checks
|
|
735
|
-
if (token.length > 10) {
|
|
736
|
-
// Check for valid character set (alphanumeric & common token symbols)
|
|
737
|
-
if (!/^[A-Za-z0-9\-_=+/.]+$/.test(token)) {
|
|
738
|
-
return false;
|
|
739
|
-
}
|
|
740
|
-
// For longer tokens, check for sufficient entropy (at least 2 character types)
|
|
741
|
-
return [/[A-Z]/, /[a-z]/, /[0-9]/, /[-_=+/.]/].filter(re => re.test(token)).length >= 2;
|
|
742
|
-
}
|
|
743
|
-
// For shorter tokens (8-10 chars), just return true if we reached here
|
|
744
|
-
return true;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* Global configuration for create-request
|
|
750
|
-
*/
|
|
751
|
-
class Config {
|
|
752
|
-
static _instance;
|
|
753
|
-
// CSRF configuration
|
|
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
|
|
760
|
-
// Interceptor configuration
|
|
761
|
-
_reqI = [];
|
|
762
|
-
_resI = [];
|
|
763
|
-
_errI = [];
|
|
764
|
-
_nextId = 1;
|
|
765
|
-
constructor() { }
|
|
766
|
-
/**
|
|
767
|
-
* Get the singleton instance of the Config class
|
|
768
|
-
*
|
|
769
|
-
* @returns The global configuration instance
|
|
770
|
-
*
|
|
771
|
-
* @example
|
|
772
|
-
* const config = Config.getInstance();
|
|
773
|
-
* config.setCsrfToken('token123');
|
|
774
|
-
*/
|
|
775
|
-
static getInstance() {
|
|
776
|
-
if (!Config._instance) {
|
|
777
|
-
Config._instance = new Config();
|
|
778
|
-
}
|
|
779
|
-
return Config._instance;
|
|
780
|
-
}
|
|
781
|
-
/**
|
|
782
|
-
* Set a global CSRF token to be used for all requests
|
|
783
|
-
*
|
|
784
|
-
* @param token - The CSRF token value
|
|
785
|
-
* @returns The config instance for chaining
|
|
786
|
-
*
|
|
787
|
-
* @example
|
|
788
|
-
* Config.getInstance().setCsrfToken('myToken123');
|
|
789
|
-
*/
|
|
790
|
-
setCsrfToken(token) {
|
|
791
|
-
this._csrfToken = token;
|
|
792
|
-
return this;
|
|
793
|
-
}
|
|
794
|
-
/**
|
|
795
|
-
* Get the global CSRF token that will be automatically applied to requests
|
|
796
|
-
*
|
|
797
|
-
* @returns The current CSRF token or null if not set
|
|
798
|
-
*/
|
|
799
|
-
getCsrfToken() {
|
|
800
|
-
return this._csrfToken;
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Set the CSRF header name used when sending the token
|
|
804
|
-
*
|
|
805
|
-
* @param name - The header name to use
|
|
806
|
-
* @returns The config instance for chaining
|
|
807
|
-
*
|
|
808
|
-
* @example
|
|
809
|
-
* Config.getInstance().setCsrfHeaderName('X-My-CSRF-Token');
|
|
810
|
-
*/
|
|
811
|
-
setCsrfHeaderName(name) {
|
|
812
|
-
this._csrfHeader = name;
|
|
813
|
-
return this;
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Get the configured CSRF header name
|
|
817
|
-
*
|
|
818
|
-
* @returns The current CSRF header name
|
|
819
|
-
*/
|
|
820
|
-
getCsrfHeaderName() {
|
|
821
|
-
return this._csrfHeader;
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Set the XSRF cookie name to look for when extracting tokens from cookies
|
|
825
|
-
*
|
|
826
|
-
* @param name - The cookie name to look for
|
|
827
|
-
* @returns The config instance for chaining
|
|
828
|
-
*
|
|
829
|
-
* @example
|
|
830
|
-
* Config.getInstance().setXsrfCookieName('MY-XSRF-COOKIE');
|
|
831
|
-
*/
|
|
832
|
-
setXsrfCookieName(name) {
|
|
833
|
-
this._xsrfCookie = name;
|
|
834
|
-
return this;
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* Get the configured XSRF cookie name
|
|
838
|
-
*
|
|
839
|
-
* @returns The current XSRF cookie name
|
|
840
|
-
*/
|
|
841
|
-
getXsrfCookieName() {
|
|
842
|
-
return this._xsrfCookie;
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Set the XSRF header name for sending tokens extracted from cookies
|
|
846
|
-
*
|
|
847
|
-
* @param name - The header name to use
|
|
848
|
-
* @returns The config instance for chaining
|
|
849
|
-
*/
|
|
850
|
-
setXsrfHeaderName(name) {
|
|
851
|
-
this._xsrfHeader = name;
|
|
852
|
-
return this;
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Get the configured XSRF header name
|
|
856
|
-
*
|
|
857
|
-
* @returns The current XSRF header name
|
|
858
|
-
*/
|
|
859
|
-
getXsrfHeaderName() {
|
|
860
|
-
return this._xsrfHeader;
|
|
861
|
-
}
|
|
862
|
-
/**
|
|
863
|
-
* Enable or disable automatic extraction of XSRF tokens from cookies
|
|
864
|
-
* When enabled, the library will look for XSRF tokens in cookies and
|
|
865
|
-
* automatically add them to request headers.
|
|
866
|
-
*
|
|
867
|
-
* @param enable - Whether to enable this feature
|
|
868
|
-
* @returns The config instance for chaining
|
|
869
|
-
*
|
|
870
|
-
* @example
|
|
871
|
-
* Config.getInstance().setEnableAutoXsrf(false); // Disable XSRF extraction
|
|
872
|
-
*/
|
|
873
|
-
setEnableAutoXsrf(enable) {
|
|
874
|
-
this._autoXsrf = enable;
|
|
875
|
-
return this;
|
|
876
|
-
}
|
|
877
|
-
/**
|
|
878
|
-
* Check if automatic XSRF token extraction is enabled
|
|
879
|
-
*
|
|
880
|
-
* @returns True if automatic XSRF is enabled
|
|
881
|
-
*/
|
|
882
|
-
isAutoXsrfEnabled() {
|
|
883
|
-
return this._autoXsrf;
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Enable or disable automatic addition of anti-CSRF headers
|
|
887
|
-
* When enabled, X-Requested-With: XMLHttpRequest will be added to all requests.
|
|
888
|
-
*
|
|
889
|
-
* @param enable - Whether to enable this feature
|
|
890
|
-
* @returns The config instance for chaining
|
|
891
|
-
*/
|
|
892
|
-
setEnableAntiCsrf(enable) {
|
|
893
|
-
this._antiCsrf = enable;
|
|
894
|
-
return this;
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Check if anti-CSRF protection is enabled
|
|
898
|
-
*
|
|
899
|
-
* @returns True if anti-CSRF protection is enabled
|
|
900
|
-
*/
|
|
901
|
-
isAntiCsrfEnabled() {
|
|
902
|
-
return this._antiCsrf;
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Add a global request interceptor
|
|
906
|
-
* Request interceptors can modify the request configuration or return an early response
|
|
907
|
-
*
|
|
908
|
-
* @param interceptor - The request interceptor function
|
|
909
|
-
* @returns The interceptor ID for later removal
|
|
910
|
-
*
|
|
911
|
-
* @example
|
|
912
|
-
* const id = Config.getInstance().addRequestInterceptor((config) => {
|
|
913
|
-
* config.headers['X-Custom'] = 'value';
|
|
914
|
-
* return config;
|
|
915
|
-
* });
|
|
916
|
-
*/
|
|
917
|
-
addRequestInterceptor(interceptor) {
|
|
918
|
-
const id = this._nextId++;
|
|
919
|
-
this._reqI.push([id, interceptor]);
|
|
920
|
-
return id;
|
|
921
|
-
}
|
|
922
|
-
/**
|
|
923
|
-
* Add a global response interceptor
|
|
924
|
-
* Response interceptors can transform the response
|
|
925
|
-
*
|
|
926
|
-
* @param interceptor - The response interceptor function
|
|
927
|
-
* @returns The interceptor ID for later removal
|
|
928
|
-
*
|
|
929
|
-
* @example
|
|
930
|
-
* const id = Config.getInstance().addResponseInterceptor((response) => {
|
|
931
|
-
* console.log('Response received:', response.status);
|
|
932
|
-
* return response;
|
|
933
|
-
* });
|
|
934
|
-
*/
|
|
935
|
-
addResponseInterceptor(interceptor) {
|
|
936
|
-
const id = this._nextId++;
|
|
937
|
-
this._resI.push([id, interceptor]);
|
|
938
|
-
return id;
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* Add a global error interceptor
|
|
942
|
-
* Error interceptors can handle or transform errors
|
|
943
|
-
*
|
|
944
|
-
* @param interceptor - The error interceptor function
|
|
945
|
-
* @returns The interceptor ID for later removal
|
|
946
|
-
*
|
|
947
|
-
* @example
|
|
948
|
-
* const id = Config.getInstance().addErrorInterceptor((error) => {
|
|
949
|
-
* console.error('Request failed:', error);
|
|
950
|
-
* throw error;
|
|
951
|
-
* });
|
|
952
|
-
*/
|
|
953
|
-
addErrorInterceptor(interceptor) {
|
|
954
|
-
const id = this._nextId++;
|
|
955
|
-
this._errI.push([id, interceptor]);
|
|
956
|
-
return id;
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Remove a request interceptor by its ID
|
|
960
|
-
*
|
|
961
|
-
* @param id - The interceptor ID returned from addRequestInterceptor
|
|
962
|
-
*
|
|
963
|
-
* @example
|
|
964
|
-
* Config.getInstance().removeRequestInterceptor(id);
|
|
965
|
-
*/
|
|
966
|
-
removeRequestInterceptor(id) {
|
|
967
|
-
this._reqI = this._reqI.filter(item => item[0] !== id);
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* Remove a response interceptor by its ID
|
|
971
|
-
*
|
|
972
|
-
* @param id - The interceptor ID returned from addResponseInterceptor
|
|
973
|
-
*
|
|
974
|
-
* @example
|
|
975
|
-
* Config.getInstance().removeResponseInterceptor(id);
|
|
976
|
-
*/
|
|
977
|
-
removeResponseInterceptor(id) {
|
|
978
|
-
this._resI = this._resI.filter(item => item[0] !== id);
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* Remove an error interceptor by its ID
|
|
982
|
-
*
|
|
983
|
-
* @param id - The interceptor ID returned from addErrorInterceptor
|
|
984
|
-
*
|
|
985
|
-
* @example
|
|
986
|
-
* Config.getInstance().removeErrorInterceptor(id);
|
|
987
|
-
*/
|
|
988
|
-
removeErrorInterceptor(id) {
|
|
989
|
-
this._errI = this._errI.filter(item => item[0] !== id);
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Clear all interceptors (request, response, and error)
|
|
993
|
-
*
|
|
994
|
-
* @example
|
|
995
|
-
* Config.getInstance().clearInterceptors();
|
|
996
|
-
*/
|
|
997
|
-
clearInterceptors() {
|
|
998
|
-
this._reqI = [];
|
|
999
|
-
this._resI = [];
|
|
1000
|
-
this._errI = [];
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Get all global request interceptors (in registration order)
|
|
1004
|
-
* @internal
|
|
1005
|
-
*/
|
|
1006
|
-
getRequestInterceptors() {
|
|
1007
|
-
return this._reqI.map(item => item[1]);
|
|
1008
|
-
}
|
|
1009
|
-
/**
|
|
1010
|
-
* Get all global response interceptors (in registration order)
|
|
1011
|
-
* @internal
|
|
1012
|
-
*/
|
|
1013
|
-
getResponseInterceptors() {
|
|
1014
|
-
return this._resI.map(item => item[1]);
|
|
1015
|
-
}
|
|
1016
|
-
/**
|
|
1017
|
-
* Get all global error interceptors (in registration order)
|
|
1018
|
-
* @internal
|
|
1019
|
-
*/
|
|
1020
|
-
getErrorInterceptors() {
|
|
1021
|
-
return this._errI.map(item => item[1]);
|
|
1022
|
-
}
|
|
1023
|
-
/**
|
|
1024
|
-
* Reset all configuration options to their default values
|
|
1025
|
-
*
|
|
1026
|
-
* @returns The config instance for chaining
|
|
1027
|
-
*
|
|
1028
|
-
* @example
|
|
1029
|
-
* Config.getInstance().reset();
|
|
1030
|
-
*/
|
|
1031
|
-
reset() {
|
|
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;
|
|
1038
|
-
this.clearInterceptors();
|
|
1039
|
-
return this;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
* Base class with common functionality for all request types
|
|
1045
|
-
* Provides the core request building and execution capabilities.
|
|
1046
|
-
*/
|
|
1047
|
-
class BaseRequest {
|
|
1048
|
-
_url;
|
|
1049
|
-
_opts = {
|
|
1050
|
-
headers: {},
|
|
1051
|
-
};
|
|
1052
|
-
_ctrl;
|
|
1053
|
-
_fetch;
|
|
1054
|
-
_query = new URLSearchParams();
|
|
1055
|
-
_autoCsrf = true;
|
|
1056
|
-
// Per-request interceptors
|
|
1057
|
-
_reqI = [];
|
|
1058
|
-
_resI = [];
|
|
1059
|
-
_errI = [];
|
|
1060
|
-
constructor(url) {
|
|
1061
|
-
this._url = url;
|
|
1062
|
-
}
|
|
1063
|
-
/**
|
|
1064
|
-
* Get GraphQL options if set (only for BodyRequest subclasses)
|
|
1065
|
-
* @returns GraphQL options or undefined
|
|
1066
|
-
*/
|
|
1067
|
-
_gql() {
|
|
1068
|
-
return undefined;
|
|
1069
|
-
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Creates a fluent API for setting enum-based options
|
|
1072
|
-
* Combines direct setter with convenience methods
|
|
1073
|
-
*/
|
|
1074
|
-
_fluent(optionName, options) {
|
|
1075
|
-
const fluent = {};
|
|
1076
|
-
// Create convenience methods for each enum value
|
|
1077
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
1078
|
-
fluent[key] = () => {
|
|
1079
|
-
this._opts[optionName] = value;
|
|
1080
|
-
return this;
|
|
1081
|
-
};
|
|
1082
|
-
});
|
|
1083
|
-
// Create the callable setter
|
|
1084
|
-
const callable = (value) => {
|
|
1085
|
-
this._opts[optionName] = value;
|
|
1086
|
-
return this;
|
|
1087
|
-
};
|
|
1088
|
-
return Object.assign(callable, fluent);
|
|
1089
|
-
}
|
|
1090
|
-
_validateUrl(url) {
|
|
1091
|
-
const errorMessage = "Bad URL";
|
|
1092
|
-
if (!url?.trim())
|
|
1093
|
-
throw new RequestError(errorMessage, url, this._method);
|
|
1094
|
-
if (url.includes("\0") || url.includes("\r") || url.includes("\n")) {
|
|
1095
|
-
throw new RequestError(errorMessage, url, this._method);
|
|
1096
|
-
}
|
|
1097
|
-
const trimmed = url.trim();
|
|
1098
|
-
if (/^https?:\/\//.test(trimmed)) {
|
|
1099
|
-
try {
|
|
1100
|
-
new URL(trimmed);
|
|
1101
|
-
}
|
|
1102
|
-
catch {
|
|
1103
|
-
throw new RequestError(errorMessage, trimmed, this._method);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
/**
|
|
1108
|
-
* Add multiple HTTP headers to the request
|
|
1109
|
-
*
|
|
1110
|
-
* @param headers - Key-value pairs of header names and values
|
|
1111
|
-
* @returns The request instance for chaining
|
|
1112
|
-
*
|
|
1113
|
-
* @example
|
|
1114
|
-
* request.withHeaders({
|
|
1115
|
-
* 'Accept': 'application/json',
|
|
1116
|
-
* 'X-Custom-Header': 'value'
|
|
1117
|
-
* });
|
|
1118
|
-
*/
|
|
1119
|
-
withHeaders(headers) {
|
|
1120
|
-
// Filter out null and undefined values
|
|
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;
|
|
1127
|
-
return this;
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* Add a single HTTP header to the request
|
|
1131
|
-
*
|
|
1132
|
-
* @param key - The header name
|
|
1133
|
-
* @param value - The header value
|
|
1134
|
-
* @returns The request instance for chaining
|
|
1135
|
-
*
|
|
1136
|
-
* @example
|
|
1137
|
-
* request.withHeader('Accept', 'application/json');
|
|
1138
|
-
*/
|
|
1139
|
-
withHeader(key, value) {
|
|
1140
|
-
return this.withHeaders({ [key]: value });
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Set a timeout for the request
|
|
1144
|
-
* If the request takes longer than the specified timeout, it will be aborted.
|
|
1145
|
-
*
|
|
1146
|
-
* @param timeout - The timeout in milliseconds
|
|
1147
|
-
* @returns The request instance for chaining
|
|
1148
|
-
* @throws RequestError if timeout is not a positive number
|
|
1149
|
-
*
|
|
1150
|
-
* @example
|
|
1151
|
-
* request.withTimeout(5000); // 5 seconds timeout
|
|
1152
|
-
*/
|
|
1153
|
-
withTimeout(timeout) {
|
|
1154
|
-
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
1155
|
-
throw new RequestError("Bad timeout", this._url, this._method);
|
|
1156
|
-
this._opts.timeout = timeout;
|
|
1157
|
-
return this;
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Configure automatic retry behavior for failed requests
|
|
1161
|
-
*
|
|
1162
|
-
* @param retries - Number of retry attempts before failing, or a configuration object
|
|
1163
|
-
* @returns The request instance for chaining
|
|
1164
|
-
* @throws RequestError if retries is not a non-negative integer or invalid config
|
|
1165
|
-
*
|
|
1166
|
-
* @example
|
|
1167
|
-
* // Simple number (backward compatible)
|
|
1168
|
-
* request.withRetries(3); // Retry up to 3 times
|
|
1169
|
-
*
|
|
1170
|
-
* @example
|
|
1171
|
-
* // With fixed delay
|
|
1172
|
-
* request.withRetries({ attempts: 3, delay: 1000 }); // Retry 3 times with 1 second delay
|
|
1173
|
-
*
|
|
1174
|
-
* @example
|
|
1175
|
-
* // With exponential backoff function
|
|
1176
|
-
* request.withRetries({
|
|
1177
|
-
* attempts: 3,
|
|
1178
|
-
* delay: ({ attempt }) => Math.min(1000 * Math.pow(2, attempt - 1), 10000)
|
|
1179
|
-
* });
|
|
1180
|
-
*
|
|
1181
|
-
* @example
|
|
1182
|
-
* // With delay function based on error
|
|
1183
|
-
* request.withRetries({
|
|
1184
|
-
* attempts: 3,
|
|
1185
|
-
* delay: ({ attempt, error }) => {
|
|
1186
|
-
* if (error.status === 429) return 5000; // Rate limited, wait longer
|
|
1187
|
-
* return attempt * 1000; // Exponential backoff
|
|
1188
|
-
* }
|
|
1189
|
-
* });
|
|
1190
|
-
*/
|
|
1191
|
-
withRetries(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);
|
|
1196
|
-
}
|
|
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);
|
|
1202
|
-
}
|
|
1203
|
-
else if (typeof delay !== "function") {
|
|
1204
|
-
throw new RequestError(`Bad delay: ${typeof delay}`, this._url, this._method);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
this._opts.retries = retries;
|
|
1208
|
-
return this;
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Set a callback to be invoked before each retry attempt
|
|
1212
|
-
* Useful for implementing backoff strategies or logging retry attempts.
|
|
1213
|
-
*
|
|
1214
|
-
* @param callback - Function to call before retrying
|
|
1215
|
-
* @returns The request instance for chaining
|
|
1216
|
-
*
|
|
1217
|
-
* @example
|
|
1218
|
-
* request.onRetry(({ attempt, error }) => {
|
|
1219
|
-
* console.log(`Retry attempt ${attempt} after error: ${error.message}`);
|
|
1220
|
-
* return new Promise(resolve => setTimeout(resolve, attempt * 1000));
|
|
1221
|
-
* });
|
|
1222
|
-
*/
|
|
1223
|
-
onRetry(callback) {
|
|
1224
|
-
this._opts.onRetry = callback;
|
|
1225
|
-
return this;
|
|
1226
|
-
}
|
|
1227
|
-
/**
|
|
1228
|
-
* Sets the credentials policy for the request, controlling whether cookies and authentication
|
|
1229
|
-
* headers are sent with cross-origin requests.
|
|
1230
|
-
*
|
|
1231
|
-
* @param credentialsPolicy - The credentials policy to use:
|
|
1232
|
-
* - `"include"` or `CredentialsPolicy.INCLUDE`: Always send credentials (cookies, authorization headers) with the request, even for cross-origin requests.
|
|
1233
|
-
* - `"omit"` or `CredentialsPolicy.OMIT`: Never send credentials, even for same-origin requests.
|
|
1234
|
-
* - `"same-origin"` or `CredentialsPolicy.SAME_ORIGIN`: Only send credentials for same-origin requests (default behavior in most browsers).
|
|
1235
|
-
*
|
|
1236
|
-
* @returns The request instance for chaining
|
|
1237
|
-
*
|
|
1238
|
-
* @example
|
|
1239
|
-
* // Using string values
|
|
1240
|
-
* request.withCredentials("include")
|
|
1241
|
-
*
|
|
1242
|
-
* @example
|
|
1243
|
-
* // Using enum values
|
|
1244
|
-
* request.withCredentials(CredentialsPolicy.INCLUDE)
|
|
1245
|
-
*
|
|
1246
|
-
* @example
|
|
1247
|
-
* // Using fluent API
|
|
1248
|
-
* request.withCredentials.INCLUDE()
|
|
1249
|
-
*/
|
|
1250
|
-
get withCredentials() {
|
|
1251
|
-
return this._fluent("credentials", CredentialsPolicy);
|
|
1252
|
-
}
|
|
1253
|
-
/**
|
|
1254
|
-
* Allows providing an external AbortController to cancel the request.
|
|
1255
|
-
* This is useful when you need to cancel a request from outside the request chain,
|
|
1256
|
-
*
|
|
1257
|
-
* @param controller - The AbortController to use for this request. When `controller.abort()` is called,
|
|
1258
|
-
* the request will be cancelled and throw an abort error.
|
|
1259
|
-
*
|
|
1260
|
-
* @returns The request instance for chaining
|
|
1261
|
-
*
|
|
1262
|
-
* @example
|
|
1263
|
-
* const controller = new AbortController();
|
|
1264
|
-
* const request = createRequest('/api/data')
|
|
1265
|
-
* .withAbortController(controller)
|
|
1266
|
-
* .getJson();
|
|
1267
|
-
*
|
|
1268
|
-
* // Later, cancel the request
|
|
1269
|
-
* controller.abort();
|
|
1270
|
-
*
|
|
1271
|
-
* @example
|
|
1272
|
-
* // Share abort controller across multiple requests
|
|
1273
|
-
* const controller = new AbortController();
|
|
1274
|
-
* request1.withAbortController(controller).getJson();
|
|
1275
|
-
* request2.withAbortController(controller).getJson();
|
|
1276
|
-
* // Aborting will cancel both requests
|
|
1277
|
-
* controller.abort();
|
|
1278
|
-
*/
|
|
1279
|
-
withAbortController(controller) {
|
|
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;
|
|
1316
|
-
return this;
|
|
1317
|
-
}
|
|
1318
|
-
/**
|
|
1319
|
-
* Sets the referrer URL for the request. The referrer is the URL of the page that initiated the request.
|
|
1320
|
-
* This can be used to override the default referrer that the browser would normally send.
|
|
1321
|
-
*
|
|
1322
|
-
* @param referrer - The referrer URL to send with the request. Can be:
|
|
1323
|
-
* - A full URL (e.g., "https://example.com/page")
|
|
1324
|
-
* - An empty string to omit the referrer
|
|
1325
|
-
* - A relative URL (will be resolved relative to the current page)
|
|
1326
|
-
*
|
|
1327
|
-
* @returns The request instance for chaining
|
|
1328
|
-
*
|
|
1329
|
-
* @example
|
|
1330
|
-
* request.withReferrer("https://example.com/previous-page")
|
|
1331
|
-
*
|
|
1332
|
-
* @example
|
|
1333
|
-
* // Omit referrer
|
|
1334
|
-
* request.withReferrer("")
|
|
1335
|
-
*/
|
|
1336
|
-
withReferrer(referrer) {
|
|
1337
|
-
this._opts.referrer = referrer;
|
|
1338
|
-
return this;
|
|
1339
|
-
}
|
|
1340
|
-
/**
|
|
1341
|
-
* Sets the referrer policy for the request, controlling how much referrer information
|
|
1342
|
-
* is sent with the request. This helps balance privacy and functionality.
|
|
1343
|
-
*
|
|
1344
|
-
* @param policy - The referrer policy to use:
|
|
1345
|
-
* - `"no-referrer"` or `ReferrerPolicy.NO_REFERRER`: Never send the referrer header.
|
|
1346
|
-
* - `"no-referrer-when-downgrade"` or `ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE`: Send full referrer for same-origin or HTTPS→HTTPS, omit for HTTPS→HTTP (default in most browsers).
|
|
1347
|
-
* - `"origin"` or `ReferrerPolicy.ORIGIN`: Only send the origin (scheme, host, port), not the full URL.
|
|
1348
|
-
* - `"origin-when-cross-origin"` or `ReferrerPolicy.ORIGIN_WHEN_CROSS_ORIGIN`: Send full referrer for same-origin, only origin for cross-origin.
|
|
1349
|
-
* - `"same-origin"` or `ReferrerPolicy.SAME_ORIGIN`: Send full referrer for same-origin requests only, omit for cross-origin.
|
|
1350
|
-
* - `"strict-origin"` or `ReferrerPolicy.STRICT_ORIGIN`: Send origin for HTTPS→HTTPS or HTTP→HTTP, omit for HTTPS→HTTP.
|
|
1351
|
-
* - `"strict-origin-when-cross-origin"` or `ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN`: Send full referrer for same-origin, origin for cross-origin HTTPS→HTTPS, omit for HTTPS→HTTP.
|
|
1352
|
-
* - `"unsafe-url"` or `ReferrerPolicy.UNSAFE_URL`: Always send the full referrer URL (may leak sensitive information).
|
|
1353
|
-
*
|
|
1354
|
-
* @returns The request instance for chaining
|
|
1355
|
-
*
|
|
1356
|
-
* @example
|
|
1357
|
-
* // Using string values
|
|
1358
|
-
* request.withReferrerPolicy("no-referrer")
|
|
1359
|
-
*
|
|
1360
|
-
* @example
|
|
1361
|
-
* // Using enum values
|
|
1362
|
-
* request.withReferrerPolicy(ReferrerPolicy.NO_REFERRER)
|
|
1363
|
-
*
|
|
1364
|
-
* @example
|
|
1365
|
-
* // Using fluent API
|
|
1366
|
-
* request.withReferrerPolicy.NO_REFERRER()
|
|
1367
|
-
*/
|
|
1368
|
-
get withReferrerPolicy() {
|
|
1369
|
-
return this._fluent("referrerPolicy", ReferrerPolicy);
|
|
1370
|
-
}
|
|
1371
|
-
/**
|
|
1372
|
-
* Sets how the request handles HTTP redirects (3xx status codes).
|
|
1373
|
-
*
|
|
1374
|
-
* @param redirect - The redirect handling mode:
|
|
1375
|
-
* - `"follow"` or `RedirectMode.FOLLOW`: Automatically follow redirects. The fetch will transparently follow redirects and return the final response (default behavior).
|
|
1376
|
-
* - `"error"` or `RedirectMode.ERROR`: Treat redirects as errors. If a redirect occurs, the request will fail with an error.
|
|
1377
|
-
* - `"manual"` or `RedirectMode.MANUAL`: Return the redirect response without following it. The response will have a `type` of "opaqueredirect" and you can manually handle the redirect.
|
|
1378
|
-
*
|
|
1379
|
-
* @returns The request instance for chaining
|
|
1380
|
-
*
|
|
1381
|
-
* @example
|
|
1382
|
-
* // Using string values
|
|
1383
|
-
* request.withRedirect("follow")
|
|
1384
|
-
*
|
|
1385
|
-
* @example
|
|
1386
|
-
* // Using enum values
|
|
1387
|
-
* request.withRedirect(RedirectMode.FOLLOW)
|
|
1388
|
-
*
|
|
1389
|
-
* @example
|
|
1390
|
-
* // Using fluent API
|
|
1391
|
-
* request.withRedirect.FOLLOW()
|
|
1392
|
-
*
|
|
1393
|
-
* @example
|
|
1394
|
-
* // Fail on redirects
|
|
1395
|
-
* request.withRedirect.ERROR()
|
|
1396
|
-
*/
|
|
1397
|
-
get withRedirect() {
|
|
1398
|
-
return this._fluent("redirect", RedirectMode);
|
|
1399
|
-
}
|
|
1400
|
-
/**
|
|
1401
|
-
* Sets the keepalive flag for the request. When enabled, the request can continue
|
|
1402
|
-
* even after the page that initiated it is closed. This is useful for analytics,
|
|
1403
|
-
* logging, or other background requests that should complete even if the user navigates away.
|
|
1404
|
-
*
|
|
1405
|
-
* @param keepalive - Whether to allow the request to outlive the page:
|
|
1406
|
-
* - `true`: The request will continue even if the page is closed or navigated away.
|
|
1407
|
-
* - `false`: The request will be cancelled if the page is closed (default).
|
|
1408
|
-
*
|
|
1409
|
-
* @returns The request instance for chaining
|
|
1410
|
-
*
|
|
1411
|
-
* @example
|
|
1412
|
-
* // Send analytics event that should complete even if user navigates away
|
|
1413
|
-
* request.withKeepAlive(true)
|
|
1414
|
-
*/
|
|
1415
|
-
withKeepAlive(keepalive) {
|
|
1416
|
-
this._opts.keepalive = keepalive;
|
|
1417
|
-
return this;
|
|
1418
|
-
}
|
|
1419
|
-
/**
|
|
1420
|
-
* Sets the priority hint for the request, indicating to the browser how important
|
|
1421
|
-
* this request is relative to other requests. This helps the browser optimize resource loading.
|
|
1422
|
-
*
|
|
1423
|
-
* @param priority - The request priority:
|
|
1424
|
-
* - `"high"` or `RequestPriority.HIGH`: High priority - the browser should prioritize this request.
|
|
1425
|
-
* - `"low"` or `RequestPriority.LOW`: Low priority - the browser can defer this request if needed.
|
|
1426
|
-
* - `"auto"` or `RequestPriority.AUTO`: Automatic priority based on the request type (default).
|
|
1427
|
-
*
|
|
1428
|
-
* @returns The request instance for chaining
|
|
1429
|
-
*
|
|
1430
|
-
* @example
|
|
1431
|
-
* // Using string values
|
|
1432
|
-
* request.withPriority("high")
|
|
1433
|
-
*
|
|
1434
|
-
* @example
|
|
1435
|
-
* // Using enum values
|
|
1436
|
-
* request.withPriority(RequestPriority.HIGH)
|
|
1437
|
-
*
|
|
1438
|
-
* @example
|
|
1439
|
-
* // Using fluent API
|
|
1440
|
-
* request.withPriority.HIGH()
|
|
1441
|
-
*
|
|
1442
|
-
* @example
|
|
1443
|
-
* // Low priority for non-critical requests
|
|
1444
|
-
* request.withPriority.LOW()
|
|
1445
|
-
*/
|
|
1446
|
-
get withPriority() {
|
|
1447
|
-
return this._fluent("priority", RequestPriority);
|
|
1448
|
-
}
|
|
1449
|
-
/**
|
|
1450
|
-
* Sets the integrity hash for Subresource Integrity (SRI) verification.
|
|
1451
|
-
* This allows the browser to verify that the fetched resource hasn't been tampered with
|
|
1452
|
-
* by comparing its hash against the provided value. If the hashes don't match, the request fails.
|
|
1453
|
-
*
|
|
1454
|
-
* @param integrity - The integrity hash string in the format `"algorithm-hash"`:
|
|
1455
|
-
* - Example: `"sha256-abcdef1234567890..."` (SHA-256 hash)
|
|
1456
|
-
* - Example: `"sha384-abcdef1234567890..."` (SHA-384 hash)
|
|
1457
|
-
* - Example: `"sha512-abcdef1234567890..."` (SHA-512 hash)
|
|
1458
|
-
* - Multiple hashes can be separated by spaces: `"sha256-... sha384-..."`
|
|
1459
|
-
*
|
|
1460
|
-
* @returns The request instance for chaining
|
|
1461
|
-
*
|
|
1462
|
-
* @example
|
|
1463
|
-
* request.withIntegrity("sha256-abcdef1234567890...")
|
|
1464
|
-
*
|
|
1465
|
-
* @example
|
|
1466
|
-
* // Multiple algorithms for better compatibility
|
|
1467
|
-
* request.withIntegrity("sha256-... sha384-...")
|
|
1468
|
-
*/
|
|
1469
|
-
withIntegrity(integrity) {
|
|
1470
|
-
this._opts.integrity = integrity;
|
|
1471
|
-
return this;
|
|
1472
|
-
}
|
|
1473
|
-
/**
|
|
1474
|
-
* Sets the cache mode for the request, controlling how the browser's HTTP cache
|
|
1475
|
-
* is used for this request.
|
|
1476
|
-
*
|
|
1477
|
-
* @param cache - The cache mode:
|
|
1478
|
-
* - `"default"` or `CacheMode.DEFAULT`: Use the browser's default cache behavior. The browser will check the cache and use it if valid, otherwise fetch from network.
|
|
1479
|
-
* - `"no-store"` or `CacheMode.NO_STORE`: Never use the cache and don't store the response in cache. Always fetch from network.
|
|
1480
|
-
* - `"reload"` or `CacheMode.RELOAD`: Bypass the cache but store the response. Always fetch from network, ignoring cached responses.
|
|
1481
|
-
* - `"no-cache"` or `CacheMode.NO_CACHE`: Check the cache but revalidate with the server. Use cached response only if server confirms it's still valid.
|
|
1482
|
-
* - `"force-cache"` or `CacheMode.FORCE_CACHE`: Use the cache if available, even if stale. Only fetch from network if not in cache.
|
|
1483
|
-
* - `"only-if-cached"` or `CacheMode.ONLY_IF_CACHED`: Only use the cache. If not in cache, return an error. Never fetch from network.
|
|
1484
|
-
*
|
|
1485
|
-
* @returns The request instance for chaining
|
|
1486
|
-
*
|
|
1487
|
-
* @example
|
|
1488
|
-
* // Using string values
|
|
1489
|
-
* request.withCache("no-cache")
|
|
1490
|
-
*
|
|
1491
|
-
* @example
|
|
1492
|
-
* // Using enum values
|
|
1493
|
-
* request.withCache(CacheMode.NO_CACHE)
|
|
1494
|
-
*
|
|
1495
|
-
* @example
|
|
1496
|
-
* // Using fluent API
|
|
1497
|
-
* request.withCache.NO_CACHE()
|
|
1498
|
-
*
|
|
1499
|
-
* @example
|
|
1500
|
-
* // Always fetch fresh data
|
|
1501
|
-
* request.withCache.RELOAD()
|
|
1502
|
-
*
|
|
1503
|
-
* @example
|
|
1504
|
-
* // Use cache only, fail if not cached
|
|
1505
|
-
* request.withCache.ONLY_IF_CACHED()
|
|
1506
|
-
*/
|
|
1507
|
-
get withCache() {
|
|
1508
|
-
return this._fluent("cache", CacheMode);
|
|
1509
|
-
}
|
|
1510
|
-
/**
|
|
1511
|
-
* Adds query parameters to the request URL.
|
|
1512
|
-
* Multiple calls will append parameters. Array values will create multiple query parameters with the same key.
|
|
1513
|
-
* Null and undefined values are ignored.
|
|
1514
|
-
*
|
|
1515
|
-
* @param params - An object containing query parameter key-value pairs.
|
|
1516
|
-
* Values can be strings, numbers, booleans, arrays (for multiple values), or null/undefined (ignored).
|
|
1517
|
-
* @returns The request instance for chaining
|
|
1518
|
-
*
|
|
1519
|
-
* @example
|
|
1520
|
-
* ```typescript
|
|
1521
|
-
* // Simple parameters
|
|
1522
|
-
* request.withQueryParams({ page: 1, limit: 10, active: true });
|
|
1523
|
-
* // Results in: ?page=1&limit=10&active=true
|
|
1524
|
-
* ```
|
|
1525
|
-
*
|
|
1526
|
-
* @example
|
|
1527
|
-
* ```typescript
|
|
1528
|
-
* // Array values create multiple parameters
|
|
1529
|
-
* request.withQueryParams({ tags: ['js', 'ts', 'node'] });
|
|
1530
|
-
* // Results in: ?tags=js&tags=ts&tags=node
|
|
1531
|
-
* ```
|
|
1532
|
-
*
|
|
1533
|
-
* @example
|
|
1534
|
-
* ```typescript
|
|
1535
|
-
* // Null/undefined values are ignored
|
|
1536
|
-
* request.withQueryParams({ page: 1, filter: null, sort: undefined });
|
|
1537
|
-
* // Results in: ?page=1
|
|
1538
|
-
* ```
|
|
1539
|
-
*/
|
|
1540
|
-
withQueryParams(params) {
|
|
1541
|
-
Object.entries(params).forEach(([key, value]) => {
|
|
1542
|
-
if (value === null || value === undefined) {
|
|
1543
|
-
return;
|
|
1544
|
-
}
|
|
1545
|
-
if (Array.isArray(value)) {
|
|
1546
|
-
// Handle array values - add multiple entries with the same key
|
|
1547
|
-
value.forEach(v => this._query.append(key, String(v)));
|
|
1548
|
-
}
|
|
1549
|
-
else {
|
|
1550
|
-
this._query.append(key, String(value));
|
|
1551
|
-
}
|
|
1552
|
-
});
|
|
1553
|
-
return this;
|
|
1554
|
-
}
|
|
1555
|
-
/**
|
|
1556
|
-
* Adds a single query parameter to the request URL.
|
|
1557
|
-
* Convenience method for adding one parameter at a time.
|
|
1558
|
-
*
|
|
1559
|
-
* @param key - The query parameter name
|
|
1560
|
-
* @param value - The query parameter value. Can be a string, number, boolean, array (for multiple values), or null/undefined (ignored).
|
|
1561
|
-
* @returns The request instance for chaining
|
|
1562
|
-
*
|
|
1563
|
-
* @example
|
|
1564
|
-
* ```typescript
|
|
1565
|
-
* request.withQueryParam('page', 1).withQueryParam('limit', 10);
|
|
1566
|
-
* // Results in: ?page=1&limit=10
|
|
1567
|
-
* ```
|
|
1568
|
-
*
|
|
1569
|
-
* @example
|
|
1570
|
-
* ```typescript
|
|
1571
|
-
* // Array values create multiple parameters
|
|
1572
|
-
* request.withQueryParam('tags', ['js', 'ts']);
|
|
1573
|
-
* // Results in: ?tags=js&tags=ts
|
|
1574
|
-
* ```
|
|
1575
|
-
*/
|
|
1576
|
-
withQueryParam(key, value) {
|
|
1577
|
-
return this.withQueryParams({ [key]: value });
|
|
1578
|
-
}
|
|
1579
|
-
/**
|
|
1580
|
-
* Sets the request mode, which determines the CORS (Cross-Origin Resource Sharing) behavior
|
|
1581
|
-
* for the request. This controls how the browser handles cross-origin requests.
|
|
1582
|
-
*
|
|
1583
|
-
* @param mode - The request mode:
|
|
1584
|
-
* - `"cors"` or `RequestMode.CORS`: Enable CORS. The browser will send CORS headers and enforce CORS rules. This is the default for most cross-origin requests.
|
|
1585
|
-
* - `"no-cors"` or `RequestMode.NO_CORS`: Disable CORS. The request is sent as a "simple" request without CORS headers. The response will be opaque (you can't read it).
|
|
1586
|
-
* - `"same-origin"` or `RequestMode.SAME_ORIGIN`: Only allow same-origin requests. Cross-origin requests will fail.
|
|
1587
|
-
* - `"navigate"` or `RequestMode.NAVIGATE`: Used for navigation requests (typically only used by the browser itself).
|
|
1588
|
-
*
|
|
1589
|
-
* @returns The request instance for chaining
|
|
1590
|
-
*
|
|
1591
|
-
* @example
|
|
1592
|
-
* // Using string values
|
|
1593
|
-
* request.withMode("cors")
|
|
1594
|
-
*
|
|
1595
|
-
* @example
|
|
1596
|
-
* // Using enum values
|
|
1597
|
-
* request.withMode(RequestMode.CORS)
|
|
1598
|
-
*
|
|
1599
|
-
* @example
|
|
1600
|
-
* // Using fluent API
|
|
1601
|
-
* request.withMode.CORS()
|
|
1602
|
-
*
|
|
1603
|
-
* @example
|
|
1604
|
-
* // Restrict to same-origin only
|
|
1605
|
-
* request.withMode.SAME_ORIGIN()
|
|
1606
|
-
*/
|
|
1607
|
-
get withMode() {
|
|
1608
|
-
return this._fluent("mode", RequestMode);
|
|
1609
|
-
}
|
|
1610
|
-
/**
|
|
1611
|
-
* Sets the Content-Type header for the request.
|
|
1612
|
-
* Shorthand for `withHeader('Content-Type', contentType)`.
|
|
1613
|
-
*
|
|
1614
|
-
* @param contentType - The MIME type (e.g., `'application/json'`, `'text/plain'`, `'multipart/form-data'`)
|
|
1615
|
-
* @returns The request instance for chaining
|
|
1616
|
-
*
|
|
1617
|
-
* @example
|
|
1618
|
-
* ```typescript
|
|
1619
|
-
* request.withContentType('application/json');
|
|
1620
|
-
* ```
|
|
1621
|
-
*
|
|
1622
|
-
* @example
|
|
1623
|
-
* ```typescript
|
|
1624
|
-
* request.withContentType('application/xml');
|
|
1625
|
-
* ```
|
|
1626
|
-
*/
|
|
1627
|
-
withContentType(contentType) {
|
|
1628
|
-
return this.withHeader("Content-Type", contentType);
|
|
1629
|
-
}
|
|
1630
|
-
/**
|
|
1631
|
-
* Sets the Authorization header for the request.
|
|
1632
|
-
* Shorthand for `withHeader('Authorization', authValue)`.
|
|
1633
|
-
* For Bearer tokens, use `withBearerToken()` instead. For Basic auth, use `withBasicAuth()`.
|
|
1634
|
-
*
|
|
1635
|
-
* @param authValue - The full authorization header value (e.g., `'Bearer token123'`, `'Basic base64string'`)
|
|
1636
|
-
* @returns The request instance for chaining
|
|
1637
|
-
*
|
|
1638
|
-
* @example
|
|
1639
|
-
* ```typescript
|
|
1640
|
-
* request.withAuthorization('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
|
|
1641
|
-
* ```
|
|
1642
|
-
*
|
|
1643
|
-
* @example
|
|
1644
|
-
* ```typescript
|
|
1645
|
-
* request.withAuthorization('CustomScheme customToken');
|
|
1646
|
-
* ```
|
|
1647
|
-
*/
|
|
1648
|
-
withAuthorization(authValue) {
|
|
1649
|
-
return this.withHeader("Authorization", authValue);
|
|
1650
|
-
}
|
|
1651
|
-
/**
|
|
1652
|
-
* Sets up HTTP Basic Authentication.
|
|
1653
|
-
* Encodes the username and password in base64 and sets the Authorization header.
|
|
1654
|
-
*
|
|
1655
|
-
* @param username - The username for Basic authentication
|
|
1656
|
-
* @param password - The password for Basic authentication
|
|
1657
|
-
* @returns The request instance for chaining
|
|
1658
|
-
*
|
|
1659
|
-
* @example
|
|
1660
|
-
* ```typescript
|
|
1661
|
-
* request.withBasicAuth('myuser', 'mypassword');
|
|
1662
|
-
* // Sets: Authorization: Basic bXl1c2VyOm15cGFzc3dvcmQ=
|
|
1663
|
-
* ```
|
|
1664
|
-
*/
|
|
1665
|
-
withBasicAuth(username, password) {
|
|
1666
|
-
const credentials = this._b64(`${username}:${password}`);
|
|
1667
|
-
return this.withAuthorization(`Basic ${credentials}`);
|
|
1668
|
-
}
|
|
1669
|
-
/**
|
|
1670
|
-
* Cross-environment base64 encoding
|
|
1671
|
-
* Works in both browser and Node.js environments
|
|
1672
|
-
*/
|
|
1673
|
-
_b64(str) {
|
|
1674
|
-
// Modern approach using TextEncoder (available in both modern browsers and Node.js)
|
|
1675
|
-
if (typeof btoa === "function") {
|
|
1676
|
-
if (typeof TextEncoder !== "undefined")
|
|
1677
|
-
return btoa(String.fromCharCode(...new TextEncoder().encode(str)));
|
|
1678
|
-
// Browser environment without TextEncoder
|
|
1679
|
-
return btoa(str);
|
|
1680
|
-
}
|
|
1681
|
-
// Node.js environment
|
|
1682
|
-
if (typeof Buffer !== "undefined")
|
|
1683
|
-
return Buffer.from(str).toString("base64");
|
|
1684
|
-
// Fallback (should never happen in modern environments)
|
|
1685
|
-
throw new RequestError("No encoder", this._url, this._method);
|
|
1686
|
-
}
|
|
1687
|
-
/**
|
|
1688
|
-
* Sets a Bearer token for authentication.
|
|
1689
|
-
* Shorthand for `withAuthorization('Bearer ' + token)`.
|
|
1690
|
-
*
|
|
1691
|
-
* @param token - The Bearer token (JWT, OAuth token, etc.)
|
|
1692
|
-
* @returns The request instance for chaining
|
|
1693
|
-
*
|
|
1694
|
-
* @example
|
|
1695
|
-
* ```typescript
|
|
1696
|
-
* request.withBearerToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
|
|
1697
|
-
* // Sets: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
1698
|
-
* ```
|
|
1699
|
-
*/
|
|
1700
|
-
withBearerToken(token) {
|
|
1701
|
-
return this.withAuthorization(`Bearer ${token}`);
|
|
1702
|
-
}
|
|
1703
|
-
/**
|
|
1704
|
-
* Safely get headers as a Record<string, string>
|
|
1705
|
-
* @returns The headers object
|
|
1706
|
-
*/
|
|
1707
|
-
_headers() {
|
|
1708
|
-
if (typeof this._opts.headers === "object" && this._opts.headers !== null) {
|
|
1709
|
-
return this._opts.headers;
|
|
1710
|
-
}
|
|
1711
|
-
return {};
|
|
1712
|
-
}
|
|
1713
|
-
/**
|
|
1714
|
-
* Helper function to check for header presence in a case-insensitive way
|
|
1715
|
-
* @param headerName Header name to check
|
|
1716
|
-
* @returns Boolean indicating if the header exists (case-insensitive)
|
|
1717
|
-
*/
|
|
1718
|
-
_hasHeader(headerName) {
|
|
1719
|
-
const headers = this._headers();
|
|
1720
|
-
return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
|
|
1721
|
-
}
|
|
1722
|
-
/**
|
|
1723
|
-
* Sets cookies for the request.
|
|
1724
|
-
* Cookies are sent in the Cookie header. Multiple calls will merge cookies.
|
|
1725
|
-
* Cookie values can be simple strings or objects with additional cookie options.
|
|
1726
|
-
*
|
|
1727
|
-
* @param cookies - An object where keys are cookie names and values are either:
|
|
1728
|
-
* - A string (the cookie value)
|
|
1729
|
-
* - A CookieOptions object with `value` and optional properties (secure, httpOnly, sameSite, expires, path, domain, maxAge)
|
|
1730
|
-
* @returns The request instance for chaining
|
|
1731
|
-
*
|
|
1732
|
-
* @example
|
|
1733
|
-
* ```typescript
|
|
1734
|
-
* // Simple string cookies
|
|
1735
|
-
* request.withCookies({ sessionId: 'abc123', userId: '456' });
|
|
1736
|
-
* ```
|
|
1737
|
-
*
|
|
1738
|
-
* @example
|
|
1739
|
-
* ```typescript
|
|
1740
|
-
* // Cookies with options (note: options are for documentation only in request cookies)
|
|
1741
|
-
* request.withCookies({
|
|
1742
|
-
* sessionId: 'abc123',
|
|
1743
|
-
* token: { value: 'xyz789', secure: true }
|
|
1744
|
-
* });
|
|
1745
|
-
* ```
|
|
1746
|
-
*/
|
|
1747
|
-
withCookies(cookies) {
|
|
1748
|
-
if (Object.keys(cookies || {}).length === 0)
|
|
1749
|
-
return this;
|
|
1750
|
-
// Collect existing cookie header values (any casing), preserving the first header's case
|
|
1751
|
-
const newHeaders = { ...this._headers() };
|
|
1752
|
-
const cookieValues = [];
|
|
1753
|
-
let headerName = "";
|
|
1754
|
-
for (const key of Object.keys(newHeaders)) {
|
|
1755
|
-
if (key.toLowerCase() === "cookie") {
|
|
1756
|
-
// Don't add empty cookie values
|
|
1757
|
-
if (newHeaders[key])
|
|
1758
|
-
cookieValues.push(newHeaders[key]);
|
|
1759
|
-
headerName ||= key;
|
|
1760
|
-
delete newHeaders[key];
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
// Combine all existing cookie values with the newly formatted ones
|
|
1764
|
-
cookieValues.push(CookieUtils.formatRequestCookies(cookies));
|
|
1765
|
-
this._opts.headers = {
|
|
1766
|
-
...newHeaders,
|
|
1767
|
-
[headerName || "Cookie"]: cookieValues.filter(Boolean).join("; "),
|
|
1768
|
-
};
|
|
1769
|
-
return this;
|
|
1770
|
-
}
|
|
1771
|
-
/**
|
|
1772
|
-
* Sets a single cookie for the request.
|
|
1773
|
-
* Convenience method for adding one cookie at a time.
|
|
1774
|
-
*
|
|
1775
|
-
* @param name - The cookie name
|
|
1776
|
-
* @param value - The cookie value as a string, or a CookieOptions object with `value` and optional properties
|
|
1777
|
-
* @returns The request instance for chaining
|
|
1778
|
-
*
|
|
1779
|
-
* @example
|
|
1780
|
-
* ```typescript
|
|
1781
|
-
* request.withCookie('sessionId', 'abc123');
|
|
1782
|
-
* ```
|
|
1783
|
-
*
|
|
1784
|
-
* @example
|
|
1785
|
-
* ```typescript
|
|
1786
|
-
* request.withCookie('token', { value: 'xyz789', secure: true });
|
|
1787
|
-
* ```
|
|
1788
|
-
*/
|
|
1789
|
-
withCookie(name, value) {
|
|
1790
|
-
return this.withCookies({ [name]: value });
|
|
1791
|
-
}
|
|
1792
|
-
/**
|
|
1793
|
-
* Sets a CSRF (Cross-Site Request Forgery) token in the request headers.
|
|
1794
|
-
* This is commonly used to protect against CSRF attacks in web applications.
|
|
1795
|
-
*
|
|
1796
|
-
* @param token - The CSRF token value
|
|
1797
|
-
* @param headerName - The name of the header to use. Defaults to `'X-CSRF-Token'`.
|
|
1798
|
-
* @returns The request instance for chaining
|
|
1799
|
-
*
|
|
1800
|
-
* @example
|
|
1801
|
-
* ```typescript
|
|
1802
|
-
* request.withCsrfToken('csrf-token-123');
|
|
1803
|
-
* // Sets: X-CSRF-Token: csrf-token-123
|
|
1804
|
-
* ```
|
|
1805
|
-
*
|
|
1806
|
-
* @example
|
|
1807
|
-
* ```typescript
|
|
1808
|
-
* request.withCsrfToken('token', 'X-Custom-CSRF-Header');
|
|
1809
|
-
* // Sets: X-Custom-CSRF-Header: token
|
|
1810
|
-
* ```
|
|
1811
|
-
*/
|
|
1812
|
-
withCsrfToken(token, headerName = "X-CSRF-Token") {
|
|
1813
|
-
return this.withHeader(headerName, token);
|
|
1814
|
-
}
|
|
1815
|
-
/**
|
|
1816
|
-
* Disables automatic anti-CSRF protection.
|
|
1817
|
-
* By default, X-Requested-With: XMLHttpRequest header is sent with all requests.
|
|
1818
|
-
* @returns The instance for chaining
|
|
1819
|
-
*/
|
|
1820
|
-
withoutCsrfProtection() {
|
|
1821
|
-
this._autoCsrf = false;
|
|
1822
|
-
return this;
|
|
1823
|
-
}
|
|
1824
|
-
/**
|
|
1825
|
-
* Sets common security headers to help prevent CSRF attacks
|
|
1826
|
-
* @returns The instance for chaining
|
|
1827
|
-
*/
|
|
1828
|
-
withAntiCsrfHeaders() {
|
|
1829
|
-
return this.withHeader("X-Requested-With", "XMLHttpRequest");
|
|
1830
|
-
}
|
|
1831
|
-
/**
|
|
1832
|
-
* Add a request interceptor for this specific request
|
|
1833
|
-
* Request interceptors can modify the request configuration or return an early response
|
|
1834
|
-
*
|
|
1835
|
-
* @param interceptor - The request interceptor function
|
|
1836
|
-
* @returns The instance for chaining
|
|
1837
|
-
*
|
|
1838
|
-
* @example
|
|
1839
|
-
* request.withRequestInterceptor((config) => {
|
|
1840
|
-
* config.headers['X-Custom'] = 'value';
|
|
1841
|
-
* return config;
|
|
1842
|
-
* });
|
|
1843
|
-
*/
|
|
1844
|
-
withRequestInterceptor(interceptor) {
|
|
1845
|
-
this._reqI.push(interceptor);
|
|
1846
|
-
return this;
|
|
1847
|
-
}
|
|
1848
|
-
/**
|
|
1849
|
-
* Add a response interceptor for this specific request
|
|
1850
|
-
* Response interceptors can transform the response
|
|
1851
|
-
*
|
|
1852
|
-
* @param interceptor - The response interceptor function
|
|
1853
|
-
* @returns The instance for chaining
|
|
1854
|
-
*
|
|
1855
|
-
* @example
|
|
1856
|
-
* request.withResponseInterceptor((response) => {
|
|
1857
|
-
* console.log('Status:', response.status);
|
|
1858
|
-
* return response;
|
|
1859
|
-
* });
|
|
1860
|
-
*/
|
|
1861
|
-
withResponseInterceptor(interceptor) {
|
|
1862
|
-
this._resI.push(interceptor);
|
|
1863
|
-
return this;
|
|
1864
|
-
}
|
|
1865
|
-
/**
|
|
1866
|
-
* Add an error interceptor for this specific request
|
|
1867
|
-
* Error interceptors can handle or transform errors
|
|
1868
|
-
*
|
|
1869
|
-
* @param interceptor - The error interceptor function
|
|
1870
|
-
* @returns The instance for chaining
|
|
1871
|
-
*
|
|
1872
|
-
* @example
|
|
1873
|
-
* request.withErrorInterceptor((error) => {
|
|
1874
|
-
* console.error('Request failed:', error);
|
|
1875
|
-
* throw error;
|
|
1876
|
-
* });
|
|
1877
|
-
*/
|
|
1878
|
-
withErrorInterceptor(interceptor) {
|
|
1879
|
-
this._errI.push(interceptor);
|
|
1880
|
-
return this;
|
|
1881
|
-
}
|
|
1882
|
-
/**
|
|
1883
|
-
* Execute the request and return the ResponseWrapper
|
|
1884
|
-
* This is the base method for getting the full response.
|
|
1885
|
-
*
|
|
1886
|
-
* @returns A promise that resolves to the ResponseWrapper
|
|
1887
|
-
*
|
|
1888
|
-
* @example
|
|
1889
|
-
* const response = await request.getResponse();
|
|
1890
|
-
* console.log(response.status);
|
|
1891
|
-
*/
|
|
1892
|
-
async getResponse() {
|
|
1893
|
-
const url = this._fullUrl(this._url);
|
|
1894
|
-
this._applyCsrf();
|
|
1895
|
-
const fetchOptions = {
|
|
1896
|
-
...this._opts,
|
|
1897
|
-
method: this._method,
|
|
1898
|
-
};
|
|
1899
|
-
return !this._opts.retries ? this._run(url, fetchOptions) : this._retry(url, fetchOptions);
|
|
1900
|
-
}
|
|
1901
|
-
/**
|
|
1902
|
-
* Execute the request and parse the response as JSON
|
|
1903
|
-
*
|
|
1904
|
-
* Returns `null` for empty responses (204 No Content, content-length: 0, or empty body).
|
|
1905
|
-
*
|
|
1906
|
-
* @returns A promise that resolves to the parsed JSON data, or `null` for empty responses
|
|
1907
|
-
* @throws {RequestError} When the request fails, JSON parsing fails, GraphQL errors occur (if throwOnError enabled), or body is already consumed
|
|
1908
|
-
*
|
|
1909
|
-
* @example
|
|
1910
|
-
* const users = await request.getJson<User[]>();
|
|
1911
|
-
* if (users !== null) {
|
|
1912
|
-
* users.forEach(u => console.log(u.name));
|
|
1913
|
-
* }
|
|
1914
|
-
*
|
|
1915
|
-
* @example
|
|
1916
|
-
* // Error handling - errors are always RequestError
|
|
1917
|
-
* try {
|
|
1918
|
-
* const data = await request.getJson();
|
|
1919
|
-
* } catch (error) {
|
|
1920
|
-
* if (error instanceof RequestError) {
|
|
1921
|
-
* console.log(error.status, error.url, error.method);
|
|
1922
|
-
* }
|
|
1923
|
-
* }
|
|
1924
|
-
*/
|
|
1925
|
-
async getJson() {
|
|
1926
|
-
const response = await this.getResponse();
|
|
1927
|
-
return response.getJson();
|
|
1928
|
-
}
|
|
1929
|
-
/**
|
|
1930
|
-
* Execute the request and get the response body as text.
|
|
1931
|
-
*
|
|
1932
|
-
* @returns A promise that resolves to the response body as a string
|
|
1933
|
-
* @throws {RequestError} When the request fails or reading the response fails
|
|
1934
|
-
*
|
|
1935
|
-
* @example
|
|
1936
|
-
* ```typescript
|
|
1937
|
-
* const text = await request.getText();
|
|
1938
|
-
* console.log(text); // "Hello, world!"
|
|
1939
|
-
* ```
|
|
1940
|
-
*/
|
|
1941
|
-
async getText() {
|
|
1942
|
-
const response = await this.getResponse();
|
|
1943
|
-
return response.getText();
|
|
1944
|
-
}
|
|
1945
|
-
/**
|
|
1946
|
-
* Execute the request and get the response body as a Blob.
|
|
1947
|
-
* Useful for downloading files or handling binary data.
|
|
1948
|
-
*
|
|
1949
|
-
* @returns A promise that resolves to the response body as a Blob
|
|
1950
|
-
* @throws {RequestError} When the request fails or reading the response fails
|
|
1951
|
-
*
|
|
1952
|
-
* @example
|
|
1953
|
-
* ```typescript
|
|
1954
|
-
* const blob = await request.getBlob();
|
|
1955
|
-
* const url = URL.createObjectURL(blob);
|
|
1956
|
-
* // Use the blob URL (e.g., for downloading or displaying)
|
|
1957
|
-
* ```
|
|
1958
|
-
*/
|
|
1959
|
-
async getBlob() {
|
|
1960
|
-
const response = await this.getResponse();
|
|
1961
|
-
return response.getBlob();
|
|
1962
|
-
}
|
|
1963
|
-
/**
|
|
1964
|
-
* Execute the request and get the response body as an ArrayBuffer.
|
|
1965
|
-
* Useful for processing binary data at a low level.
|
|
1966
|
-
*
|
|
1967
|
-
* @returns A promise that resolves to the response body as an ArrayBuffer
|
|
1968
|
-
* @throws {RequestError} When the request fails or reading the response fails
|
|
1969
|
-
*
|
|
1970
|
-
* @example
|
|
1971
|
-
* ```typescript
|
|
1972
|
-
* const buffer = await request.getArrayBuffer();
|
|
1973
|
-
* const uint8Array = new Uint8Array(buffer);
|
|
1974
|
-
* // Process the binary data
|
|
1975
|
-
* ```
|
|
1976
|
-
*/
|
|
1977
|
-
async getArrayBuffer() {
|
|
1978
|
-
const response = await this.getResponse();
|
|
1979
|
-
return response.getArrayBuffer();
|
|
1980
|
-
}
|
|
1981
|
-
/**
|
|
1982
|
-
* Execute the request and get the response body as a ReadableStream.
|
|
1983
|
-
* Note: Unlike other methods, streams cannot be cached. The body can only be consumed once.
|
|
1984
|
-
*
|
|
1985
|
-
* @returns A promise that resolves to the response body as a ReadableStream, or `null` if the body is not available
|
|
1986
|
-
* @throws {RequestError} When the request fails or the body has already been consumed
|
|
1987
|
-
*
|
|
1988
|
-
* @example
|
|
1989
|
-
* ```typescript
|
|
1990
|
-
* const stream = await request.getBody();
|
|
1991
|
-
* if (stream) {
|
|
1992
|
-
* const reader = stream.getReader();
|
|
1993
|
-
* // Process the stream chunk by chunk
|
|
1994
|
-
* }
|
|
1995
|
-
* ```
|
|
1996
|
-
*/
|
|
1997
|
-
async getBody() {
|
|
1998
|
-
const response = await this.getResponse();
|
|
1999
|
-
return response.getBody();
|
|
2000
|
-
}
|
|
2001
|
-
/**
|
|
2002
|
-
* Execute the request and extract specific data using a selector function
|
|
2003
|
-
* If no selector is provided, returns the full JSON response.
|
|
2004
|
-
*
|
|
2005
|
-
* Returns `null` for empty responses (204 No Content, content-length: 0, or empty body).
|
|
2006
|
-
* If a selector is provided and data is `null`, the selector will receive `null`.
|
|
2007
|
-
*
|
|
2008
|
-
* @param selector - Optional function to extract and transform data (receives `null` for empty responses)
|
|
2009
|
-
* @returns A promise that resolves to the selected data, or `null` for empty responses
|
|
2010
|
-
* @throws {RequestError} When the request fails, JSON parsing fails, or the selector throws an error
|
|
2011
|
-
*
|
|
2012
|
-
* @example
|
|
2013
|
-
* // Get full response
|
|
2014
|
-
* const data = await request.getData();
|
|
2015
|
-
* if (data !== null) {
|
|
2016
|
-
* console.log(data.items);
|
|
2017
|
-
* }
|
|
2018
|
-
*
|
|
2019
|
-
* @example
|
|
2020
|
-
* // Extract specific data (use null-safe selector for empty responses)
|
|
2021
|
-
* const users = await request.getData(data => data?.results?.users);
|
|
2022
|
-
*
|
|
2023
|
-
* @example
|
|
2024
|
-
* // Error handling - errors are always RequestError
|
|
2025
|
-
* try {
|
|
2026
|
-
* const data = await request.getData();
|
|
2027
|
-
* } catch (error) {
|
|
2028
|
-
* if (error instanceof RequestError) {
|
|
2029
|
-
* console.log(error.status, error.url, error.method);
|
|
2030
|
-
* }
|
|
2031
|
-
* }
|
|
2032
|
-
*/
|
|
2033
|
-
async getData(selector) {
|
|
2034
|
-
const response = await this.getResponse();
|
|
2035
|
-
return response.getData(selector);
|
|
2036
|
-
}
|
|
2037
|
-
/**
|
|
2038
|
-
* Apply CSRF protection headers based on configuration
|
|
2039
|
-
*/
|
|
2040
|
-
_applyCsrf() {
|
|
2041
|
-
if (!this._autoCsrf)
|
|
2042
|
-
return;
|
|
2043
|
-
const config = Config.getInstance();
|
|
2044
|
-
// Apply anti-CSRF headers if enabled
|
|
2045
|
-
if (config.isAntiCsrfEnabled()) {
|
|
2046
|
-
this.withAntiCsrfHeaders();
|
|
2047
|
-
}
|
|
2048
|
-
// Apply global CSRF token if set
|
|
2049
|
-
const globalToken = config.getCsrfToken();
|
|
2050
|
-
if (globalToken) {
|
|
2051
|
-
const csrfHeaderName = config.getCsrfHeaderName();
|
|
2052
|
-
const hasLocalToken = this._hasHeader("X-CSRF-Token") || this._hasHeader(csrfHeaderName);
|
|
2053
|
-
if (!hasLocalToken) {
|
|
2054
|
-
this.withHeader(csrfHeaderName, globalToken);
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
// Check for XSRF token in cookies and send it as a header
|
|
2058
|
-
if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
|
|
2059
|
-
const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
|
|
2060
|
-
if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
|
|
2061
|
-
const xsrfHeaderName = config.getXsrfHeaderName();
|
|
2062
|
-
const hasLocalToken = this._hasHeader("X-XSRF-TOKEN") || this._hasHeader(xsrfHeaderName);
|
|
2063
|
-
if (!hasLocalToken) {
|
|
2064
|
-
this.withHeader(xsrfHeaderName, xsrfToken);
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
/**
|
|
2070
|
-
* Formats the URL with any query parameters
|
|
2071
|
-
* @param url The base URL
|
|
2072
|
-
* @returns The URL with query parameters appended
|
|
2073
|
-
*/
|
|
2074
|
-
_fullUrl(url) {
|
|
2075
|
-
const queryString = this._query.toString();
|
|
2076
|
-
if (!queryString) {
|
|
2077
|
-
return url;
|
|
2078
|
-
}
|
|
2079
|
-
try {
|
|
2080
|
-
// Try to use the URL constructor (works for absolute URLs)
|
|
2081
|
-
const urlObj = new URL(url);
|
|
2082
|
-
// Merge our query params with any that might be in the URL already
|
|
2083
|
-
this._query.forEach((value, key) => {
|
|
2084
|
-
urlObj.searchParams.append(key, value);
|
|
2085
|
-
});
|
|
2086
|
-
return urlObj.toString();
|
|
2087
|
-
}
|
|
2088
|
-
catch (_error) {
|
|
2089
|
-
// Handle relative URLs
|
|
2090
|
-
const hasExistingParams = url.includes("?");
|
|
2091
|
-
const separator = hasExistingParams ? "&" : "?";
|
|
2092
|
-
return `${url}${separator}${queryString}`;
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
/**
|
|
2096
|
-
* Executes a request with configured retry logic
|
|
2097
|
-
* @param url The formatted URL to send the request to
|
|
2098
|
-
* @param fetchOptions The fetch options to use
|
|
2099
|
-
* @returns A wrapped response object
|
|
2100
|
-
* @throws RequestError if the request fails after all retries
|
|
2101
|
-
*/
|
|
2102
|
-
async _retry(url, fetchOptions) {
|
|
2103
|
-
const retriesConfig = this._opts.retries;
|
|
2104
|
-
const maxRetries = typeof retriesConfig === "number" ? retriesConfig : retriesConfig?.attempts || 0;
|
|
2105
|
-
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
2106
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2107
|
-
try {
|
|
2108
|
-
return await this._run(url, fetchOptions);
|
|
2109
|
-
}
|
|
2110
|
-
catch (error) {
|
|
2111
|
-
const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, toError(error));
|
|
2112
|
-
if (attempt >= maxRetries)
|
|
2113
|
-
throw requestError;
|
|
2114
|
-
// Call onRetry callback if provided
|
|
2115
|
-
if (this._opts.onRetry) {
|
|
2116
|
-
await this._opts.onRetry({ attempt: attempt + 1, error: requestError });
|
|
2117
|
-
}
|
|
2118
|
-
// Apply delay if configured
|
|
2119
|
-
if (typeof retriesConfig === "object" && retriesConfig.delay !== undefined) {
|
|
2120
|
-
const delay = typeof retriesConfig.delay === "function" ? retriesConfig.delay({ attempt: attempt + 1, error: requestError }) : retriesConfig.delay;
|
|
2121
|
-
// Validate delay result
|
|
2122
|
-
if (typeof delay !== "number" || !Number.isFinite(delay) || delay < 0) {
|
|
2123
|
-
throw new RequestError(`Bad delay: ${delay}`, url, method);
|
|
2124
|
-
}
|
|
2125
|
-
// Wait for the delay
|
|
2126
|
-
if (delay > 0) {
|
|
2127
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
// This should never happen but is needed for type safety
|
|
2133
|
-
throw new RequestError(`EO`, url, method);
|
|
2134
|
-
}
|
|
2135
|
-
/**
|
|
2136
|
-
* Run request interceptors in order: global interceptors first, then per-request
|
|
2137
|
-
* @param configParam - The request configuration
|
|
2138
|
-
* @returns Modified config or a Response to short-circuit
|
|
2139
|
-
*/
|
|
2140
|
-
async _runReqI(configParam) {
|
|
2141
|
-
const globalConfig = Config.getInstance();
|
|
2142
|
-
const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this._reqI];
|
|
2143
|
-
let currentConfig = configParam;
|
|
2144
|
-
for (let i = 0; i < allInterceptors.length; i++) {
|
|
2145
|
-
try {
|
|
2146
|
-
const result = await allInterceptors[i](currentConfig);
|
|
2147
|
-
// If interceptor returns a Response, short-circuit
|
|
2148
|
-
if (result instanceof Response) {
|
|
2149
|
-
return result;
|
|
2150
|
-
}
|
|
2151
|
-
currentConfig = result;
|
|
2152
|
-
}
|
|
2153
|
-
catch (error) {
|
|
2154
|
-
throw new RequestError(`ReqI: ${errorMessage(error)}`, currentConfig.url, currentConfig.method);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
return currentConfig;
|
|
2158
|
-
}
|
|
2159
|
-
/**
|
|
2160
|
-
* Run response interceptors in reverse order: per-request interceptors first, then global in reverse
|
|
2161
|
-
* @param response - The response wrapper
|
|
2162
|
-
* @returns Modified response wrapper
|
|
2163
|
-
*/
|
|
2164
|
-
async _runResI(response) {
|
|
2165
|
-
const globalConfig = Config.getInstance();
|
|
2166
|
-
const globalInterceptors = globalConfig.getResponseInterceptors();
|
|
2167
|
-
// Per-request in order, then global in reverse
|
|
2168
|
-
const allInterceptors = [...this._resI, ...[...globalInterceptors].reverse()];
|
|
2169
|
-
let currentResponse = response;
|
|
2170
|
-
for (let i = 0; i < allInterceptors.length; i++) {
|
|
2171
|
-
try {
|
|
2172
|
-
currentResponse = await allInterceptors[i](currentResponse);
|
|
2173
|
-
}
|
|
2174
|
-
catch (error) {
|
|
2175
|
-
throw new RequestError(`ResI: ${errorMessage(error)}`, currentResponse.url || "", currentResponse.method || "");
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
return currentResponse;
|
|
2179
|
-
}
|
|
2180
|
-
/**
|
|
2181
|
-
* Run error interceptors in reverse order: per-request interceptors first, then global in reverse
|
|
2182
|
-
* @param error - The error that occurred
|
|
2183
|
-
* @returns Modified error or a ResponseWrapper to recover
|
|
2184
|
-
*/
|
|
2185
|
-
async _runErrI(error) {
|
|
2186
|
-
const globalConfig = Config.getInstance();
|
|
2187
|
-
const globalInterceptors = globalConfig.getErrorInterceptors();
|
|
2188
|
-
// Per-request in order, then global in reverse
|
|
2189
|
-
const allInterceptors = [...this._errI, ...[...globalInterceptors].reverse()];
|
|
2190
|
-
let currentError = error;
|
|
2191
|
-
for (let i = 0; i < allInterceptors.length; i++) {
|
|
2192
|
-
try {
|
|
2193
|
-
// If previous interceptor recovered with a response, stop processing
|
|
2194
|
-
if (currentError instanceof ResponseWrapper) {
|
|
2195
|
-
return currentError;
|
|
2196
|
-
}
|
|
2197
|
-
const result = await allInterceptors[i](currentError);
|
|
2198
|
-
currentError = result;
|
|
2199
|
-
}
|
|
2200
|
-
catch (interceptorError) {
|
|
2201
|
-
// If an error interceptor throws, that becomes the new error
|
|
2202
|
-
if (interceptorError instanceof RequestError) {
|
|
2203
|
-
currentError = interceptorError;
|
|
2204
|
-
}
|
|
2205
|
-
else if (currentError instanceof RequestError) {
|
|
2206
|
-
// Always wrap in RequestError when we have context
|
|
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 */
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
return currentError;
|
|
2223
|
-
}
|
|
2224
|
-
/**
|
|
2225
|
-
* Helper to create an abort signal with timeout support
|
|
2226
|
-
* Handles various AbortSignal API levels gracefully
|
|
2227
|
-
*/
|
|
2228
|
-
_signal(timeoutMs, externalController) {
|
|
2229
|
-
let timeoutId;
|
|
2230
|
-
let timeoutController;
|
|
2231
|
-
let isTimeout = false;
|
|
2232
|
-
// No timeout - just use external controller
|
|
2233
|
-
if (!timeoutMs) {
|
|
2234
|
-
return {
|
|
2235
|
-
signal: externalController?.signal,
|
|
2236
|
-
cleanup: () => { },
|
|
2237
|
-
wasTimeout: () => false,
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
// Check if AbortSignal.any() is available (modern browsers)
|
|
2241
|
-
const hasAbortSignalAny = typeof AbortSignal.any === "function";
|
|
2242
|
-
const hasAbortSignalTimeout = typeof AbortSignal.timeout === "function";
|
|
2243
|
-
// Create timeout signal
|
|
2244
|
-
const createTimeoutSignal = () => {
|
|
2245
|
-
if (hasAbortSignalTimeout) {
|
|
2246
|
-
const signal = AbortSignal.timeout(timeoutMs);
|
|
2247
|
-
signal.addEventListener("abort", () => (isTimeout = true), { once: true });
|
|
2248
|
-
return signal;
|
|
2249
|
-
}
|
|
2250
|
-
else {
|
|
2251
|
-
timeoutController = new AbortController();
|
|
2252
|
-
timeoutId = setTimeout(() => {
|
|
2253
|
-
isTimeout = true;
|
|
2254
|
-
timeoutController.abort();
|
|
2255
|
-
}, timeoutMs);
|
|
2256
|
-
return timeoutController.signal;
|
|
2257
|
-
}
|
|
2258
|
-
};
|
|
2259
|
-
const timeoutSignal = createTimeoutSignal();
|
|
2260
|
-
// Combine signals if we have both timeout and external controller
|
|
2261
|
-
const finalSignal = externalController && hasAbortSignalAny
|
|
2262
|
-
? AbortSignal.any([externalController.signal, timeoutSignal])
|
|
2263
|
-
: externalController
|
|
2264
|
-
? this._combine(externalController.signal, timeoutSignal)
|
|
2265
|
-
: timeoutSignal;
|
|
2266
|
-
return {
|
|
2267
|
-
signal: finalSignal,
|
|
2268
|
-
cleanup: () => {
|
|
2269
|
-
if (timeoutId !== undefined)
|
|
2270
|
-
clearTimeout(timeoutId);
|
|
2271
|
-
},
|
|
2272
|
-
wasTimeout: () => isTimeout,
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
/**
|
|
2276
|
-
* Manually combine two abort signals for older environments
|
|
2277
|
-
* Returns the first signal and listens to the second
|
|
2278
|
-
*/
|
|
2279
|
-
_combine(signal1, signal2) {
|
|
2280
|
-
// If either is already aborted, use that one
|
|
2281
|
-
if (signal1.aborted)
|
|
2282
|
-
return signal1;
|
|
2283
|
-
if (signal2.aborted)
|
|
2284
|
-
return signal2;
|
|
2285
|
-
// Use a controller to create a combined signal
|
|
2286
|
-
const controller = new AbortController();
|
|
2287
|
-
const abort = () => controller.abort();
|
|
2288
|
-
signal1.addEventListener("abort", abort, { once: true });
|
|
2289
|
-
signal2.addEventListener("abort", abort, { once: true });
|
|
2290
|
-
return controller.signal;
|
|
2291
|
-
}
|
|
2292
|
-
/**
|
|
2293
|
-
* Convert fetchOptions to RequestConfig with proper typing
|
|
2294
|
-
*/
|
|
2295
|
-
_config(url, fetchOptions) {
|
|
2296
|
-
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
2297
|
-
const headers = this._headers();
|
|
2298
|
-
const extendedOptions = fetchOptions;
|
|
2299
|
-
return {
|
|
2300
|
-
url,
|
|
2301
|
-
method,
|
|
2302
|
-
headers,
|
|
2303
|
-
body: fetchOptions.body,
|
|
2304
|
-
signal: fetchOptions.signal || undefined,
|
|
2305
|
-
credentials: fetchOptions.credentials,
|
|
2306
|
-
mode: fetchOptions.mode,
|
|
2307
|
-
redirect: fetchOptions.redirect,
|
|
2308
|
-
referrer: fetchOptions.referrer,
|
|
2309
|
-
referrerPolicy: fetchOptions.referrerPolicy,
|
|
2310
|
-
keepalive: fetchOptions.keepalive,
|
|
2311
|
-
priority: extendedOptions.priority,
|
|
2312
|
-
integrity: fetchOptions.integrity,
|
|
2313
|
-
cache: fetchOptions.cache,
|
|
2314
|
-
};
|
|
2315
|
-
}
|
|
2316
|
-
/**
|
|
2317
|
-
* Apply interceptor results back to fetchOptions
|
|
2318
|
-
*/
|
|
2319
|
-
_applyConfig(config, fetchOptions) {
|
|
2320
|
-
fetchOptions.headers = config.headers;
|
|
2321
|
-
if (config.body !== undefined) {
|
|
2322
|
-
fetchOptions.body = config.body;
|
|
2323
|
-
}
|
|
2324
|
-
if (config.integrity !== undefined) {
|
|
2325
|
-
fetchOptions.integrity = config.integrity;
|
|
2326
|
-
}
|
|
2327
|
-
if (config.cache !== undefined) {
|
|
2328
|
-
fetchOptions.cache = config.cache;
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
async _run(url, fetchOptions) {
|
|
2332
|
-
const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
|
|
2333
|
-
// Setup abort signal with timeout
|
|
2334
|
-
const abortSignal = this._signal(this._opts.timeout, this._ctrl);
|
|
2335
|
-
try {
|
|
2336
|
-
// Run request interceptors before making the request
|
|
2337
|
-
const requestConfig = this._config(url, fetchOptions);
|
|
2338
|
-
const interceptorResult = await this._runReqI(requestConfig);
|
|
2339
|
-
// If interceptor returned a Response, short-circuit and wrap it
|
|
2340
|
-
if (interceptorResult instanceof Response) {
|
|
2341
|
-
const graphQLOptions = this._gql();
|
|
2342
|
-
const wrappedResponse = new ResponseWrapper(interceptorResult, this._url, this._method, graphQLOptions);
|
|
2343
|
-
return await this._runResI(wrappedResponse);
|
|
2344
|
-
}
|
|
2345
|
-
// Update fetchOptions with interceptor modifications
|
|
2346
|
-
url = interceptorResult.url;
|
|
2347
|
-
this._validateUrl(url);
|
|
2348
|
-
this._applyConfig(interceptorResult, fetchOptions);
|
|
2349
|
-
// Set the combined abort signal
|
|
2350
|
-
if (abortSignal.signal) {
|
|
2351
|
-
fetchOptions.signal = abortSignal.signal;
|
|
2352
|
-
}
|
|
2353
|
-
// Execute fetch (custom implementation if provided, global fetch otherwise)
|
|
2354
|
-
const fetchFn = this._fetch ?? globalThis.fetch;
|
|
2355
|
-
let response;
|
|
2356
|
-
try {
|
|
2357
|
-
response = await fetchFn(url, fetchOptions);
|
|
2358
|
-
}
|
|
2359
|
-
catch (error) {
|
|
2360
|
-
const errorObj = toError(error);
|
|
2361
|
-
const errorName = errorObj.name;
|
|
2362
|
-
const lowerMessage = errorObj.message.toLowerCase();
|
|
2363
|
-
// Check if this is a timeout error from our internal timeout
|
|
2364
|
-
const isOurTimeout = abortSignal.wasTimeout();
|
|
2365
|
-
// Check if it's an abort error (DOMException in browsers, or AbortSignal abort)
|
|
2366
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
2367
|
-
// If it was our timeout that caused the abort, throw timeout error
|
|
2368
|
-
if (isOurTimeout && this._opts.timeout) {
|
|
2369
|
-
throw RequestError.timeout(url, method, this._opts.timeout);
|
|
2370
|
-
}
|
|
2371
|
-
// Otherwise it's a manual abort
|
|
2372
|
-
throw RequestError.abortError(url, method);
|
|
2373
|
-
}
|
|
2374
|
-
// Check for Node.js/undici TimeoutError or other timeout indicators
|
|
2375
|
-
// This catches timeout errors that aren't thrown as AbortError
|
|
2376
|
-
const isTimeoutError = isOurTimeout || errorName === "TimeoutError" || lowerMessage.includes("timeout");
|
|
2377
|
-
if (isTimeoutError && this._opts.timeout) {
|
|
2378
|
-
throw RequestError.timeout(url, method, this._opts.timeout);
|
|
2379
|
-
}
|
|
2380
|
-
// For other network errors, let RequestError.networkError handle them
|
|
2381
|
-
// It will check for timeout patterns as a safety net (useful for external AbortControllers)
|
|
2382
|
-
throw RequestError.networkError(url, method, errorObj);
|
|
2383
|
-
}
|
|
2384
|
-
// Status 0 indicates the request failed before receiving a proper HTTP response
|
|
2385
|
-
// (e.g., CORS errors, network failures that don't throw). Treat as network error.
|
|
2386
|
-
if (response.status === 0) {
|
|
2387
|
-
throw RequestError.networkError(url, method, new Error("Failed with status 0 (network error or CORS blocked)"));
|
|
2388
|
-
}
|
|
2389
|
-
if (!response.ok) {
|
|
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));
|
|
2393
|
-
}
|
|
2394
|
-
const graphQLOptions = this._gql();
|
|
2395
|
-
const wrappedResponse = new ResponseWrapper(response, url, method, graphQLOptions);
|
|
2396
|
-
return await this._runResI(wrappedResponse);
|
|
2397
|
-
}
|
|
2398
|
-
catch (error) {
|
|
2399
|
-
// Convert to RequestError if needed
|
|
2400
|
-
const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, toError(error));
|
|
2401
|
-
// Run error interceptors
|
|
2402
|
-
const interceptorResult = await this._runErrI(requestError);
|
|
2403
|
-
// If error interceptor returned a ResponseWrapper, recover from error
|
|
2404
|
-
if (interceptorResult instanceof ResponseWrapper) {
|
|
2405
|
-
return interceptorResult;
|
|
2406
|
-
}
|
|
2407
|
-
throw interceptorResult;
|
|
2408
|
-
}
|
|
2409
|
-
finally {
|
|
2410
|
-
abortSignal.cleanup();
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
/**
|
|
2416
|
-
* Base class for requests that can have a body (POST, PUT, PATCH)
|
|
2417
|
-
*/
|
|
2418
|
-
class BodyRequest extends BaseRequest {
|
|
2419
|
-
_body;
|
|
2420
|
-
_bodyType;
|
|
2421
|
-
_gqlOpts = undefined;
|
|
2422
|
-
/**
|
|
2423
|
-
* Sets the request body. Automatically detects the body type and sets appropriate Content-Type header.
|
|
2424
|
-
* Supports JSON objects/arrays, strings, FormData, Blob, ArrayBuffer, URLSearchParams, and ReadableStream.
|
|
2425
|
-
*
|
|
2426
|
-
* @param body - The request body. Can be:
|
|
2427
|
-
* - A JSON-serializable object or array (automatically stringified)
|
|
2428
|
-
* - A string (sets Content-Type to `text/plain` if not already set)
|
|
2429
|
-
* - FormData, Blob, File, ArrayBuffer, TypedArray, URLSearchParams, or ReadableStream
|
|
2430
|
-
* @returns The request instance for chaining
|
|
2431
|
-
* @throws {RequestError} If the body is a JSON object that cannot be stringified
|
|
2432
|
-
*
|
|
2433
|
-
* @example
|
|
2434
|
-
* ```typescript
|
|
2435
|
-
* // JSON object (automatically stringified)
|
|
2436
|
-
* request.withBody({ name: 'John', age: 30 });
|
|
2437
|
-
*
|
|
2438
|
-
* @example
|
|
2439
|
-
* // JSON array
|
|
2440
|
-
* request.withBody([1, 2, 3]);
|
|
2441
|
-
*
|
|
2442
|
-
* @example
|
|
2443
|
-
* // String
|
|
2444
|
-
* request.withBody('plain text');
|
|
2445
|
-
*
|
|
2446
|
-
* @example
|
|
2447
|
-
* // FormData
|
|
2448
|
-
* const formData = new FormData();
|
|
2449
|
-
* formData.append('file', fileBlob);
|
|
2450
|
-
* request.withBody(formData);
|
|
2451
|
-
*
|
|
2452
|
-
* @example
|
|
2453
|
-
* // Blob
|
|
2454
|
-
* request.withBody(new Blob(['content'], { type: 'text/plain' }));
|
|
2455
|
-
* ```
|
|
2456
|
-
*/
|
|
2457
|
-
withBody(body) {
|
|
2458
|
-
this._body = body;
|
|
2459
|
-
// Set body type and validate
|
|
2460
|
-
if (typeof body === "string") {
|
|
2461
|
-
this._bodyType = BodyType.STRING;
|
|
2462
|
-
this._setCT("text/plain");
|
|
2463
|
-
}
|
|
2464
|
-
else if (body !== null &&
|
|
2465
|
-
typeof body === "object" &&
|
|
2466
|
-
!(body instanceof FormData ||
|
|
2467
|
-
body instanceof Blob ||
|
|
2468
|
-
body instanceof File ||
|
|
2469
|
-
body instanceof ArrayBuffer ||
|
|
2470
|
-
ArrayBuffer.isView(body) || // Handles TypedArray and DataView
|
|
2471
|
-
body instanceof URLSearchParams ||
|
|
2472
|
-
body instanceof ReadableStream)) {
|
|
2473
|
-
this._bodyType = BodyType.JSON;
|
|
2474
|
-
this._setCT("application/json");
|
|
2475
|
-
// Validate JSON is stringifiable early
|
|
2476
|
-
try {
|
|
2477
|
-
JSON.stringify(body);
|
|
2478
|
-
}
|
|
2479
|
-
catch (error) {
|
|
2480
|
-
throw new RequestError(`Bad JSON: ${errorMessage(error)}`, this._url, this._method);
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
else {
|
|
2484
|
-
this._bodyType = BodyType.BINARY;
|
|
2485
|
-
}
|
|
2486
|
-
return this;
|
|
2487
|
-
}
|
|
2488
|
-
/**
|
|
2489
|
-
* Sets a GraphQL query or mutation as the request body.
|
|
2490
|
-
* Automatically formats the body as JSON and sets Content-Type to `application/json`.
|
|
2491
|
-
* If `throwOnError` is enabled in options, the response will be checked for GraphQL errors
|
|
2492
|
-
* and a RequestError will be thrown if any are found.
|
|
2493
|
-
*
|
|
2494
|
-
* @param query - The GraphQL query or mutation string (e.g., `'query { user { id } }'`)
|
|
2495
|
-
* @param variables - Optional variables object to pass with the query. Must be a plain object.
|
|
2496
|
-
* @param options - Optional GraphQL-specific options
|
|
2497
|
-
* @param options.throwOnError - If `true`, throws a RequestError when the GraphQL response contains errors
|
|
2498
|
-
* @returns The request instance for chaining
|
|
2499
|
-
* @throws {RequestError} If the query is empty, variables is invalid, or JSON stringification fails
|
|
2500
|
-
*
|
|
2501
|
-
* @example
|
|
2502
|
-
* ```typescript
|
|
2503
|
-
* // Simple query with variables
|
|
2504
|
-
* const request = create.post('/graphql')
|
|
2505
|
-
* .withGraphQL('query { user(id: $id) { name email } }', { id: '123' });
|
|
2506
|
-
* const data = await request.getJson();
|
|
2507
|
-
* ```
|
|
2508
|
-
*
|
|
2509
|
-
* @example
|
|
2510
|
-
* ```typescript
|
|
2511
|
-
* // Mutation with variables
|
|
2512
|
-
* const request = create.post('/graphql')
|
|
2513
|
-
* .withGraphQL('mutation { createUser(name: $name) { id } }', { name: 'John' });
|
|
2514
|
-
* ```
|
|
2515
|
-
*
|
|
2516
|
-
* @example
|
|
2517
|
-
* ```typescript
|
|
2518
|
-
* // Throw error if GraphQL response contains errors
|
|
2519
|
-
* const request = create.post('/graphql')
|
|
2520
|
-
* .withGraphQL('query { user { id } }', undefined, { throwOnError: true });
|
|
2521
|
-
* // If the response has errors, this will throw a RequestError
|
|
2522
|
-
* const data = await request.getJson();
|
|
2523
|
-
* ```
|
|
2524
|
-
*/
|
|
2525
|
-
withGraphQL(query, variables, options) {
|
|
2526
|
-
if (typeof query !== "string" || query.length === 0) {
|
|
2527
|
-
throw new RequestError("Bad query", this._url, this._method);
|
|
2528
|
-
}
|
|
2529
|
-
const graphQLBody = {
|
|
2530
|
-
query: query,
|
|
2531
|
-
};
|
|
2532
|
-
if (variables !== undefined) {
|
|
2533
|
-
if (typeof variables !== "object" || variables === null || Array.isArray(variables)) {
|
|
2534
|
-
throw new RequestError("Bad vars", this._url, this._method);
|
|
2535
|
-
}
|
|
2536
|
-
graphQLBody.variables = variables;
|
|
2537
|
-
}
|
|
2538
|
-
// Store GraphQL options if provided
|
|
2539
|
-
if (options !== undefined) {
|
|
2540
|
-
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
2541
|
-
throw new RequestError("Bad opts", this._url, this._method);
|
|
2542
|
-
}
|
|
2543
|
-
// Store only the known GraphQL options properties
|
|
2544
|
-
const opts = options;
|
|
2545
|
-
this._gqlOpts = {
|
|
2546
|
-
throwOnError: typeof opts.throwOnError === "boolean" ? opts.throwOnError : undefined,
|
|
2547
|
-
};
|
|
2548
|
-
}
|
|
2549
|
-
// Validate JSON is stringifiable early
|
|
2550
|
-
try {
|
|
2551
|
-
JSON.stringify(graphQLBody);
|
|
2552
|
-
}
|
|
2553
|
-
catch (error) {
|
|
2554
|
-
throw new RequestError(`Bad JSON: ${errorMessage(error)}`, this._url, this._method);
|
|
2555
|
-
}
|
|
2556
|
-
this._body = graphQLBody;
|
|
2557
|
-
this._bodyType = BodyType.JSON;
|
|
2558
|
-
this._setCT("application/json");
|
|
2559
|
-
return this;
|
|
2560
|
-
}
|
|
2561
|
-
/**
|
|
2562
|
-
* Check if Content-Type header is already set (case-insensitive)
|
|
2563
|
-
*/
|
|
2564
|
-
_hasCT() {
|
|
2565
|
-
const headers = this._opts.headers;
|
|
2566
|
-
if (typeof headers === "object" && headers !== null) {
|
|
2567
|
-
const headersObj = headers;
|
|
2568
|
-
return Object.keys(headersObj).some(header => header.toLowerCase() === "content-type");
|
|
2569
|
-
}
|
|
2570
|
-
return false;
|
|
2571
|
-
}
|
|
2572
|
-
_setCT(contentType) {
|
|
2573
|
-
if (!this._hasCT()) {
|
|
2574
|
-
this.withContentType(contentType);
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
/**
|
|
2578
|
-
* Get the GraphQL options if set
|
|
2579
|
-
* @returns The GraphQL options or undefined
|
|
2580
|
-
*/
|
|
2581
|
-
_gql() {
|
|
2582
|
-
return this._gqlOpts;
|
|
2583
|
-
}
|
|
2584
|
-
/**
|
|
2585
|
-
* Execute the request and return the ResponseWrapper
|
|
2586
|
-
* Overrides the base implementation to add body handling
|
|
2587
|
-
*/
|
|
2588
|
-
async getResponse() {
|
|
2589
|
-
if (this._body !== undefined) {
|
|
2590
|
-
// Remove previous body if it exists
|
|
2591
|
-
if (this._opts.body)
|
|
2592
|
-
delete this._opts.body;
|
|
2593
|
-
// Process the body based on its type
|
|
2594
|
-
if (this._bodyType === BodyType.JSON) {
|
|
2595
|
-
this._opts.body = JSON.stringify(this._body);
|
|
2596
|
-
}
|
|
2597
|
-
else {
|
|
2598
|
-
this._opts.body = this._body;
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
return super.getResponse();
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
/**
|
|
2606
|
-
* HTTP GET request implementation
|
|
2607
|
-
* Used for retrieving data from an API endpoint.
|
|
2608
|
-
*
|
|
2609
|
-
* @example
|
|
2610
|
-
* const request = new GetRequest('/api/users')
|
|
2611
|
-
* .withQueryParams({ id: '123' });
|
|
2612
|
-
* const data = await request.getData();
|
|
2613
|
-
*/
|
|
2614
|
-
class GetRequest extends BaseRequest {
|
|
2615
|
-
_method = HttpMethod.GET;
|
|
2616
|
-
}
|
|
2617
|
-
/**
|
|
2618
|
-
* HTTP HEAD request implementation
|
|
2619
|
-
* Similar to GET but returns only headers without a response body.
|
|
2620
|
-
*
|
|
2621
|
-
* @example
|
|
2622
|
-
* const request = new HeadRequest('/api/users/123');
|
|
2623
|
-
* const response = await request.getResponse();
|
|
2624
|
-
*/
|
|
2625
|
-
class HeadRequest extends BaseRequest {
|
|
2626
|
-
_method = HttpMethod.HEAD;
|
|
2627
|
-
}
|
|
2628
|
-
/**
|
|
2629
|
-
* HTTP OPTIONS request implementation
|
|
2630
|
-
* Used to describe the communication options for the target resource.
|
|
2631
|
-
*
|
|
2632
|
-
* @example
|
|
2633
|
-
* const request = new OptionsRequest('/api/users');
|
|
2634
|
-
* const response = await request.getResponse();
|
|
2635
|
-
*/
|
|
2636
|
-
class OptionsRequest extends BaseRequest {
|
|
2637
|
-
_method = HttpMethod.OPTIONS;
|
|
2638
|
-
}
|
|
2639
|
-
/**
|
|
2640
|
-
* HTTP DELETE request implementation
|
|
2641
|
-
* Used to delete a resource on the server.
|
|
2642
|
-
*
|
|
2643
|
-
* @example
|
|
2644
|
-
* const request = new DeleteRequest('/api/users/123');
|
|
2645
|
-
* await request.getData();
|
|
2646
|
-
*/
|
|
2647
|
-
class DeleteRequest extends BaseRequest {
|
|
2648
|
-
_method = HttpMethod.DELETE;
|
|
2649
|
-
}
|
|
2650
|
-
/**
|
|
2651
|
-
* HTTP POST request implementation
|
|
2652
|
-
* Used to create a new resource or submit data for processing.
|
|
2653
|
-
*
|
|
2654
|
-
* @example
|
|
2655
|
-
* const request = new PostRequest('/api/users')
|
|
2656
|
-
* .withBody({ name: 'John', email: 'john@example.com' });
|
|
2657
|
-
* const data = await request.getData();
|
|
2658
|
-
*/
|
|
2659
|
-
class PostRequest extends BodyRequest {
|
|
2660
|
-
_method = HttpMethod.POST;
|
|
2661
|
-
}
|
|
2662
|
-
/**
|
|
2663
|
-
* HTTP PUT request implementation
|
|
2664
|
-
* Used to replace or update an existing resource.
|
|
2665
|
-
*
|
|
2666
|
-
* @example
|
|
2667
|
-
* const request = new PutRequest('/api/users/123')
|
|
2668
|
-
* .withBody({ id: '123', name: 'John', email: 'john@example.com' });
|
|
2669
|
-
* const data = await request.getData();
|
|
2670
|
-
*/
|
|
2671
|
-
class PutRequest extends BodyRequest {
|
|
2672
|
-
_method = HttpMethod.PUT;
|
|
2673
|
-
}
|
|
2674
|
-
/**
|
|
2675
|
-
* HTTP PATCH request implementation
|
|
2676
|
-
* Used to apply partial modifications to a resource.
|
|
2677
|
-
*
|
|
2678
|
-
* @example
|
|
2679
|
-
* const request = new PatchRequest('/api/users/123')
|
|
2680
|
-
* .withBody({ email: 'new.email@example.com' });
|
|
2681
|
-
* const data = await request.getData();
|
|
2682
|
-
*/
|
|
2683
|
-
class PatchRequest extends BodyRequest {
|
|
2684
|
-
_method = HttpMethod.PATCH;
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
/**
|
|
2688
|
-
* Create a GET request
|
|
2689
|
-
* Used for retrieving resources from the server.
|
|
2690
|
-
*
|
|
2691
|
-
* @param url - The URL to send the request to
|
|
2692
|
-
* @returns A new GET request instance
|
|
2693
|
-
*
|
|
2694
|
-
* @example
|
|
2695
|
-
* const request = get('/api/users')
|
|
2696
|
-
* .withQueryParams({ limit: 10 });
|
|
2697
|
-
* const users = await request.getData();
|
|
2698
|
-
*/
|
|
2699
|
-
function get(url) {
|
|
2700
|
-
return new GetRequest(url);
|
|
2701
|
-
}
|
|
2702
|
-
/**
|
|
2703
|
-
* Create a POST request
|
|
2704
|
-
* Used for creating new resources or submitting data.
|
|
2705
|
-
*
|
|
2706
|
-
* @param url - The URL to send the request to
|
|
2707
|
-
* @returns A new POST request instance
|
|
2708
|
-
*
|
|
2709
|
-
* @example
|
|
2710
|
-
* const request = post('/api/users')
|
|
2711
|
-
* .withBody({ name: 'John', email: 'john@example.com' });
|
|
2712
|
-
* const newUser = await request.getData();
|
|
2713
|
-
*/
|
|
2714
|
-
function post(url) {
|
|
2715
|
-
return new PostRequest(url);
|
|
2716
|
-
}
|
|
2717
|
-
/**
|
|
2718
|
-
* Create a PUT request
|
|
2719
|
-
* Used for replacing or updating an existing resource.
|
|
2720
|
-
*
|
|
2721
|
-
* @param url - The URL to send the request to
|
|
2722
|
-
* @returns A new PUT request instance
|
|
2723
|
-
*
|
|
2724
|
-
* @example
|
|
2725
|
-
* const request = put('/api/users/123')
|
|
2726
|
-
* .withBody({ name: 'John Updated', email: 'john@example.com' });
|
|
2727
|
-
* const updatedUser = await request.getData();
|
|
2728
|
-
*/
|
|
2729
|
-
function put(url) {
|
|
2730
|
-
return new PutRequest(url);
|
|
2731
|
-
}
|
|
2732
|
-
/**
|
|
2733
|
-
* Create a DELETE request
|
|
2734
|
-
* Used for removing resources from the server.
|
|
2735
|
-
*
|
|
2736
|
-
* @param url - The URL to send the request to
|
|
2737
|
-
* @returns A new DELETE request instance
|
|
2738
|
-
*
|
|
2739
|
-
* @example
|
|
2740
|
-
* const request = del('/api/users/123');
|
|
2741
|
-
* await request.getData();
|
|
2742
|
-
*/
|
|
2743
|
-
function del(url) {
|
|
2744
|
-
return new DeleteRequest(url);
|
|
2745
|
-
}
|
|
2746
|
-
/**
|
|
2747
|
-
* Create a PATCH request
|
|
2748
|
-
* Used for applying partial modifications to a resource.
|
|
2749
|
-
*
|
|
2750
|
-
* @param url - The URL to send the request to
|
|
2751
|
-
* @returns A new PATCH request instance
|
|
2752
|
-
*
|
|
2753
|
-
* @example
|
|
2754
|
-
* const request = patch('/api/users/123')
|
|
2755
|
-
* .withBody({ status: 'active' });
|
|
2756
|
-
* const patchedUser = await request.getData();
|
|
2757
|
-
*/
|
|
2758
|
-
function patch(url) {
|
|
2759
|
-
return new PatchRequest(url);
|
|
2760
|
-
}
|
|
2761
|
-
/**
|
|
2762
|
-
* Create a HEAD request
|
|
2763
|
-
* Similar to GET but returns only headers without a body.
|
|
2764
|
-
*
|
|
2765
|
-
* @param url - The URL to send the request to
|
|
2766
|
-
* @returns A new HEAD request instance
|
|
2767
|
-
*
|
|
2768
|
-
* @example
|
|
2769
|
-
* const request = head('/api/users/123');
|
|
2770
|
-
* const response = await request.getResponse();
|
|
2771
|
-
* console.log(response.headers.get('Last-Modified'));
|
|
2772
|
-
*/
|
|
2773
|
-
function head(url) {
|
|
2774
|
-
return new HeadRequest(url);
|
|
2775
|
-
}
|
|
2776
|
-
/**
|
|
2777
|
-
* Create an OPTIONS request
|
|
2778
|
-
* Used to describe the communication options for a resource.
|
|
2779
|
-
*
|
|
2780
|
-
* @param url - The URL to send the request to
|
|
2781
|
-
* @returns A new OPTIONS request instance
|
|
2782
|
-
*
|
|
2783
|
-
* @example
|
|
2784
|
-
* const request = options('/api/users');
|
|
2785
|
-
* const response = await request.getResponse();
|
|
2786
|
-
* console.log(response.headers.get('Allow'));
|
|
2787
|
-
*/
|
|
2788
|
-
function options(url) {
|
|
2789
|
-
return new OptionsRequest(url);
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
/**
|
|
2793
|
-
* Internal API builder implementation.
|
|
2794
|
-
*/
|
|
2795
|
-
class ApiBuilderImpl {
|
|
2796
|
-
_baseURL;
|
|
2797
|
-
_mods = [];
|
|
2798
|
-
_proxy;
|
|
2799
|
-
withBaseURL(baseURL) {
|
|
2800
|
-
this._baseURL = baseURL;
|
|
2801
|
-
return this._getProxy();
|
|
2802
|
-
}
|
|
2803
|
-
_resolve(url) {
|
|
2804
|
-
if (!url)
|
|
2805
|
-
return this._baseURL || "";
|
|
2806
|
-
if (/^https?:\/\//.test(url))
|
|
2807
|
-
return url;
|
|
2808
|
-
if (!this._baseURL)
|
|
2809
|
-
return url;
|
|
2810
|
-
return this._baseURL.replace(/\/$/, "") + (url[0] === "/" ? url : "/" + url);
|
|
2811
|
-
}
|
|
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;
|
|
2817
|
-
}
|
|
2818
|
-
get(url) {
|
|
2819
|
-
return this._new(GetRequest, url);
|
|
2820
|
-
}
|
|
2821
|
-
post(url) {
|
|
2822
|
-
return this._new(PostRequest, url);
|
|
2823
|
-
}
|
|
2824
|
-
put(url) {
|
|
2825
|
-
return this._new(PutRequest, url);
|
|
2826
|
-
}
|
|
2827
|
-
del(url) {
|
|
2828
|
-
return this._new(DeleteRequest, url);
|
|
2829
|
-
}
|
|
2830
|
-
patch(url) {
|
|
2831
|
-
return this._new(PatchRequest, url);
|
|
2832
|
-
}
|
|
2833
|
-
head(url) {
|
|
2834
|
-
return this._new(HeadRequest, url);
|
|
2835
|
-
}
|
|
2836
|
-
options(url) {
|
|
2837
|
-
return this._new(OptionsRequest, url);
|
|
2838
|
-
}
|
|
2839
|
-
_add(modifier) {
|
|
2840
|
-
this._mods.push(modifier);
|
|
2841
|
-
return this._getProxy();
|
|
2842
|
-
}
|
|
2843
|
-
_getProxy() {
|
|
2844
|
-
if (!this._proxy) {
|
|
2845
|
-
this._proxy = this._mkProxy();
|
|
2846
|
-
}
|
|
2847
|
-
return this._proxy;
|
|
2848
|
-
}
|
|
2849
|
-
_mkProxy() {
|
|
2850
|
-
const disallowedMethods = new Set(["withBody", "withGraphQL", "withAbortController"]);
|
|
2851
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
2852
|
-
return new Proxy(this, {
|
|
2853
|
-
get(target, prop) {
|
|
2854
|
-
const implTarget = target;
|
|
2855
|
-
// Return undefined for disallowed methods
|
|
2856
|
-
if (typeof prop === "string" && disallowedMethods.has(prop)) {
|
|
2857
|
-
return undefined;
|
|
2858
|
-
}
|
|
2859
|
-
// Check if it's an HTTP method - these should be called directly
|
|
2860
|
-
if (prop === "get" || prop === "post" || prop === "put" || prop === "del" || prop === "patch" || prop === "head" || prop === "options") {
|
|
2861
|
-
return implTarget[prop].bind(implTarget);
|
|
2862
|
-
}
|
|
2863
|
-
// Check if it's a configuration method that already exists
|
|
2864
|
-
if (prop === "withBaseURL") {
|
|
2865
|
-
return implTarget[prop].bind(implTarget);
|
|
2866
|
-
}
|
|
2867
|
-
// Check if the property exists on BaseRequest prototype
|
|
2868
|
-
// If it's a 'with...' method or other chainable method, create a modifier for it
|
|
2869
|
-
if (typeof prop === "string" && (prop.startsWith("with") || prop === "onRetry")) {
|
|
2870
|
-
return (...args) => {
|
|
2871
|
-
return implTarget._add((request) => {
|
|
2872
|
-
const method = request[prop];
|
|
2873
|
-
if (typeof method === "function") {
|
|
2874
|
-
method.apply(request, args);
|
|
2875
|
-
}
|
|
2876
|
-
});
|
|
2877
|
-
};
|
|
2878
|
-
}
|
|
2879
|
-
// For other properties, return them directly if they exist
|
|
2880
|
-
const targetValue = implTarget[prop];
|
|
2881
|
-
return targetValue;
|
|
2882
|
-
},
|
|
2883
|
-
});
|
|
2884
|
-
}
|
|
2885
|
-
static create() {
|
|
2886
|
-
return new ApiBuilderImpl()._getProxy();
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
2889
|
-
/**
|
|
2890
|
-
* Creates a new API builder for configuring default request settings.
|
|
2891
|
-
* The API builder allows you to set up a base URL, default headers, timeout,
|
|
2892
|
-
* and other configuration options that will be applied to all requests made through it.
|
|
2893
|
-
*
|
|
2894
|
-
* @returns A new API builder instance
|
|
2895
|
-
*
|
|
2896
|
-
* @example
|
|
2897
|
-
* ```typescript
|
|
2898
|
-
* // Create an API instance with defaults
|
|
2899
|
-
* const api = api()
|
|
2900
|
-
* .withBaseURL('https://api.example.com')
|
|
2901
|
-
* .withBearerToken('token123')
|
|
2902
|
-
* .withTimeout(5000);
|
|
2903
|
-
*
|
|
2904
|
-
* // All requests will use these defaults
|
|
2905
|
-
* const users = await api.get('/users').getJson();
|
|
2906
|
-
* const newUser = await api.post('/users').withBody({ name: 'John' }).getJson();
|
|
2907
|
-
* ```
|
|
2908
|
-
*
|
|
2909
|
-
* @example
|
|
2910
|
-
* ```typescript
|
|
2911
|
-
* // Use without URL when baseURL is set
|
|
2912
|
-
* const api = api().withBaseURL('https://api.example.com');
|
|
2913
|
-
* const data = await api.get().getJson(); // Requests to https://api.example.com
|
|
2914
|
-
* ```
|
|
2915
|
-
*
|
|
2916
|
-
* @example
|
|
2917
|
-
* ```typescript
|
|
2918
|
-
* // Override defaults per request
|
|
2919
|
-
* const api = api()
|
|
2920
|
-
* .withBaseURL('https://api.example.com')
|
|
2921
|
-
* .withTimeout(5000);
|
|
2922
|
-
*
|
|
2923
|
-
* // This request uses a longer timeout
|
|
2924
|
-
* await api.get('/slow-endpoint').withTimeout(30000).getJson();
|
|
2925
|
-
* ```
|
|
2926
|
-
*/
|
|
2927
|
-
function api() {
|
|
2928
|
-
return ApiBuilderImpl.create();
|
|
2929
|
-
}
|
|
2930
|
-
|
|
2931
|
-
/**
|
|
2932
|
-
* Main API object for creating HTTP requests.
|
|
2933
|
-
* Provides factory methods for all HTTP methods and access to global configuration.
|
|
2934
|
-
*
|
|
2935
|
-
* @example
|
|
2936
|
-
* ```typescript
|
|
2937
|
-
* import create from 'create-request';
|
|
2938
|
-
*
|
|
2939
|
-
* // Simple GET request
|
|
2940
|
-
* const users = await create.get('/api/users').getJson();
|
|
2941
|
-
*
|
|
2942
|
-
* // POST request with body
|
|
2943
|
-
* const newUser = await create.post('/api/users')
|
|
2944
|
-
* .withBody({ name: 'John', email: 'john@example.com' })
|
|
2945
|
-
* .getJson();
|
|
2946
|
-
*
|
|
2947
|
-
* // Configure API instance with defaults
|
|
2948
|
-
* const api = create.api()
|
|
2949
|
-
* .withBaseURL('https://api.example.com')
|
|
2950
|
-
* .withBearerToken('token123');
|
|
2951
|
-
*
|
|
2952
|
-
* const data = await api.get('/users').getJson();
|
|
2953
|
-
* ```
|
|
2954
|
-
*/
|
|
2955
|
-
const create = {
|
|
2956
|
-
api,
|
|
2957
|
-
get,
|
|
2958
|
-
put,
|
|
2959
|
-
del,
|
|
2960
|
-
post,
|
|
2961
|
-
patch,
|
|
2962
|
-
head,
|
|
2963
|
-
options,
|
|
2964
|
-
config: Config.getInstance(),
|
|
2965
|
-
};
|
|
2966
|
-
|
|
2967
|
-
exports.CacheMode = CacheMode;
|
|
2968
|
-
exports.CookieUtils = CookieUtils;
|
|
2969
|
-
exports.CredentialsPolicy = CredentialsPolicy;
|
|
2970
|
-
exports.DeleteRequest = DeleteRequest;
|
|
2971
|
-
exports.GetRequest = GetRequest;
|
|
2972
|
-
exports.HeadRequest = HeadRequest;
|
|
2973
|
-
exports.HttpMethod = HttpMethod;
|
|
2974
|
-
exports.OptionsRequest = OptionsRequest;
|
|
2975
|
-
exports.PatchRequest = PatchRequest;
|
|
2976
|
-
exports.PostRequest = PostRequest;
|
|
2977
|
-
exports.PutRequest = PutRequest;
|
|
2978
|
-
exports.RedirectMode = RedirectMode;
|
|
2979
|
-
exports.ReferrerPolicy = ReferrerPolicy;
|
|
2980
|
-
exports.RequestError = RequestError;
|
|
2981
|
-
exports.RequestMode = RequestMode;
|
|
2982
|
-
exports.RequestPriority = RequestPriority;
|
|
2983
|
-
exports.ResponseWrapper = ResponseWrapper;
|
|
2984
|
-
exports.SameSitePolicy = SameSitePolicy;
|
|
2985
|
-
exports.createApi = api;
|
|
2986
|
-
exports.createDelete = del;
|
|
2987
|
-
exports.createGet = get;
|
|
2988
|
-
exports.createHead = head;
|
|
2989
|
-
exports.createOptions = options;
|
|
2990
|
-
exports.createPatch = patch;
|
|
2991
|
-
exports.createPost = post;
|
|
2992
|
-
exports.createPut = put;
|
|
2993
|
-
exports.default = create;
|
|
2994
|
-
//# sourceMappingURL=index.cjs.map
|