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