create-request 1.6.1 → 2.0.0-next.0

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