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