create-request 1.6.0 → 2.0.0-next.0

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