vigor-fetch 3.1.8 → 4.0.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.
Files changed (6) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +471 -1143
  3. package/dist/index.d.ts +5717 -742
  4. package/dist/index.js +1726 -1530
  5. package/dist/index.mjs +1678 -1530
  6. package/package.json +56 -52
package/README.md CHANGED
@@ -1,1143 +1,471 @@
1
- # vigor-fetch
2
-
3
- ## Vigor is a composable, lightweighted (minified + gzipped ~5.8kb) network workflow toolkit built on top of native Fetch.
4
-
5
- > Vigor provides a fluent, chainable API for building robust network logic with built-in retry, backoff, interceptors, parsing, and concurrency control.
6
-
7
- ---
8
-
9
- ## Features
10
-
11
- - 🧩 **Fluent & Immutable API** — Fully composable, side-effect-free chaining
12
- - 🔁 **Advanced Retry System** — Constant, linear, backoff, custom delay with jitter support.
13
- - 🌐 **Smart Fetch Layer** — Automatic 429 handling & configurable retry rules
14
- - **Parallel Requests** — Concurrency-limited task runner
15
- - 🔌 **Smart Response Parsing** — Auto parsing based on Content-Type, Sniffing
16
- - **Zero Dependencies** — Built on native Fetch + AbortController
17
- - 🪝 **Powerful Interceptors** — Lifecycle hooks for full control flow
18
- - 🧠 **TypeScript First** — Fully typed inference across all modules
19
-
20
- ---
21
-
22
- ## Installation
23
-
24
- ```bash
25
-
26
- npm install vigor-fetch
27
-
28
- ```
29
-
30
- ## Why Vigor?
31
-
32
- | Feature | Vigor | Axios | Ky | Got |
33
- |---|:---:|:---:|:---:|:---:|
34
- | Runtime | Browser + Node | Browser + Node | Browser + Node | Primarily Node.js |
35
- | Built on Fetch | Native Fetch-based | Custom adapter-based | | |
36
- | Immutable Fluent Builder | ✅ | ❌ | ⚠️ Partial | ⚠️ Partial |
37
- | Built-in Retry Engine | ✅ Advanced | ⚠️ Limited | ✅ | ✅ |
38
- | Custom Retry Algorithms | ✅ | ❌ | ⚠️ Limited | ⚠️ Limited |
39
- | Retry Interceptors | ✅ | ❌ | ❌ | ❌ |
40
- | Automatic RateLimit Handling (`429`) | ✅ | ❌ | ⚠️ Partial | ✅ |
41
- | Automatic Content-Type Parsing | ✅ | ⚠️ Mostly JSON | ✅ | ✅ |
42
- | Standalone Parse Engine | ✅ | ❌ | ❌ | ❌ |
43
- | Custom Parsing Strategies | ✅ | ❌ | ❌ | ⚠️ Hook-level |
44
- | Lifecycle Interceptors | ✅ Full lifecycle | ✅ | ⚠️ Hook-based | ✅ Hook-based |
45
- | Concurrency Queue Engine | ✅ | ❌ | ❌ | ❌ |
46
- | Default Fallback Values | ✅ | ❌ | ❌ | ❌ |
47
-
48
- ## Quick Start
49
-
50
- ### Fetch
51
- ```ts
52
- import vigor from "vigor-fetch";
53
-
54
- const data = await vigor
55
- .fetch("https://api.example.com", "api")
56
- .path("v1", "main")
57
- .request();
58
- // -> https://api.example.com/api/v1/main
59
- ```
60
-
61
- #### Advanced
62
- ```ts
63
- const data = await vigor
64
- .fetch("https://api.example.com")
65
- .path("users")
66
- .retryConfig(r => r
67
- .settings(s => s.attempt(5))
68
- )
69
- .parseConfig(p => p
70
- .strategies(s => s.sniff())
71
- )
72
- .interceptors(i => i
73
- .onError((ctx, api) => {
74
- api.restart();
75
- })
76
- )
77
- .request();
78
- ```
79
-
80
- ### Retry
81
- ```ts
82
- import vigor from "vigor-fetch";
83
-
84
- const data = await vigor
85
- .retry(async(ctx, {signal, abort}) => {
86
- return await db.select(~)
87
- })
88
- .request()
89
- ```
90
-
91
- ### Parse
92
- ```ts
93
- import vigor from "vigor-fetch";
94
-
95
- const response = await fetch("https://api.example.com");
96
- const data = await vigor
97
- .parse(response)
98
- .request();
99
- ```
100
-
101
- ### Concurrency
102
- ```ts
103
- import vigor from "vigor-fetch";
104
-
105
- const data = await vigor
106
- .all(
107
- async () => fetch("/api/1"),
108
- async () => fetch("/api/2")
109
- )
110
- .request();
111
- ```
112
-
113
- ## vigor.retry
114
-
115
- ### Methods
116
-
117
- | Method | Description |
118
- |---|---|
119
- | target(fn) | Sets retry target function |
120
- | settings(fn \| config) | Configures retry settings |
121
- | interceptors(fn \| config) | Configures retry interceptors |
122
- | algorithms(fn) | Configures retry delay algorithm |
123
- | abortSignals(...signals) | Attaches external AbortSignals |
124
- | request(config?) | Executes retry pipeline |
125
-
126
- ---
127
-
128
- ### target
129
-
130
- Sets the retry target.
131
-
132
- ```ts
133
- vigor.retry(async () => {
134
- return await fetch("/api");
135
- })
136
- ```
137
-
138
- ---
139
-
140
- ### settings
141
-
142
- Configures retry behavior.
143
-
144
- ```ts
145
- .settings(s => s
146
- .attempt(10)
147
- .timeout(5000)
148
- .jitter(1000)
149
- )
150
- ```
151
-
152
- #### Settings API
153
-
154
- | Method | Description | Default |
155
- |---|---|---|
156
- | attempt(number) | Maximum retry attempts | `5` |
157
- | timeout(ms) | Timeout per attempt | `20000` |
158
- | jitter(ms) | Random retry jitter | `1000` |
159
- | default(value) | Fallback return value | `throws` |
160
-
161
- ---
162
-
163
- ### algorithms
164
-
165
- Configures retry delay algorithm.
166
-
167
- ```ts
168
- .algorithms(a => a
169
- .backoff()
170
- .initial(1000)
171
- .multiplier(2)
172
- )
173
- ```
174
-
175
- ---
176
-
177
- ## Retry Algorithms
178
-
179
- ### constant
180
-
181
- Fixed retry delay.
182
-
183
- ```ts
184
- .algorithms(a => a
185
- .constant()
186
- .interval(2000)
187
- )
188
- ```
189
-
190
- #### API
191
-
192
- | Method | Description |
193
- |---|---|
194
- | interval(ms) | Fixed retry interval |
195
-
196
- ---
197
-
198
- ### linear
199
-
200
- Linearly increasing delay.
201
-
202
- ```ts
203
- .algorithms(a => a
204
- .linear()
205
- .initial(1000)
206
- .increment(1000)
207
- )
208
- ```
209
-
210
- #### API
211
-
212
- | Method | Description |
213
- |---|---|
214
- | initial(ms) | Initial delay |
215
- | increment(ms) | Delay increment |
216
- | minDelay(ms) | Minimum delay |
217
- | maxDelay(ms) | Maximum delay |
218
-
219
- ---
220
-
221
- ### backoff
222
-
223
- Exponential backoff delay.
224
-
225
- ```ts
226
- .algorithms(a => a
227
- .backoff()
228
- .initial(1000)
229
- .multiplier(1.7)
230
- )
231
- ```
232
-
233
- #### API
234
-
235
- | Method | Description |
236
- |---|---|
237
- | initial(ms) | Initial delay |
238
- | multiplier(number) | Exponential multiplier |
239
- | unit(ms) | Delay unit |
240
- | minDelay(ms) | Minimum delay |
241
- | maxDelay(ms) | Maximum delay |
242
-
243
- ---
244
-
245
- ### custom
246
-
247
- Fully custom retry algorithm.
248
-
249
- ```ts
250
- .algorithms(a => a
251
- .custom()
252
- .func(attempt => {
253
- return attempt * 3000;
254
- })
255
- )
256
- ```
257
-
258
- #### API
259
-
260
- | Method | Description |
261
- |---|---|
262
- | func(fn) | Custom delay calculator |
263
-
264
- ---
265
-
266
- ### abortSignals
267
-
268
- Attaches external AbortSignals.
269
-
270
- ```ts
271
- const controller = new AbortController();
272
-
273
- await vigor
274
- .retry(task)
275
- .abortSignals(controller.signal)
276
- .request();
277
- ```
278
-
279
- ---
280
-
281
- ### request
282
-
283
- Executes retry workflow.
284
-
285
- ```ts
286
- const result = await vigor
287
- .retry(task)
288
- .request();
289
- ```
290
-
291
- ---
292
-
293
- ## Interceptors
294
-
295
- | Name | API |
296
- |---|:---:|
297
- | before | throwError / breakRetry / abort |
298
- | after | setResult / throwError / breakRetry |
299
- | result | setResult / throwError |
300
- | retryIf | proceedRetry / cancelRetry |
301
- | onRetry | throwError / setDelay / setAttempt |
302
- | onError | setResult / throwError / restart |
303
-
304
- ---
305
-
306
- ### before
307
-
308
- Runs before each retry attempt.
309
-
310
- ```ts
311
- .before(async (ctx, api) => {
312
- console.log(ctx.attempt);
313
- })
314
- ```
315
-
316
- #### Available APIs
317
-
318
- | API | Description |
319
- |---|---|
320
- | throwError(error) | Immediately throws an error |
321
- | breakRetry(error) | Stops retry loop immediately |
322
- | abort(error) | Aborts current request |
323
-
324
- ---
325
-
326
- ### after
327
-
328
- Runs after successful task execution.
329
-
330
- ```ts
331
- .after(async (ctx, api) => {
332
- api.setResult({
333
- wrapped: ctx.result
334
- });
335
- })
336
- ```
337
-
338
- #### Available APIs
339
-
340
- | API | Description |
341
- |---|---|
342
- | setResult(value) | Replaces current result |
343
- | throwError(error) | Throws an error |
344
- | breakRetry(error) | Stops retry loop |
345
-
346
- ---
347
-
348
- ### result
349
-
350
- Runs before returning final result.
351
-
352
- ```ts
353
- .result(async (ctx, api) => {
354
- api.setResult(transform(ctx.result));
355
- })
356
- ```
357
-
358
- #### Available APIs
359
-
360
- | API | Description |
361
- |---|---|
362
- | setResult(value) | Replaces current result |
363
- | throwError(error) | Throws an error |
364
-
365
- ---
366
-
367
- ### retryIf
368
-
369
- Controls retry continuation.
370
-
371
- ```ts
372
- .retryIf(async (ctx, api) => {
373
- if (ctx.error instanceof TypeError) {
374
- api.proceedRetry();
375
- }
376
- else {
377
- api.cancelRetry();
378
- }
379
- })
380
- ```
381
-
382
- #### Available APIs
383
-
384
- | API | Description |
385
- |---|---|
386
- | proceedRetry() | Continues retry |
387
- | cancelRetry() | Cancels retry |
388
-
389
- ---
390
-
391
- ### onRetry
392
-
393
- Runs before retry delay.
394
-
395
- ```ts
396
- .onRetry(async (ctx, api) => {
397
- api.setDelay(5000);
398
- })
399
- ```
400
-
401
- #### Available APIs
402
-
403
- | API | Description |
404
- |---|---|
405
- | throwError(error) | Throws an error |
406
- | setDelay(ms) | Overrides retry delay |
407
- | setAttempt(num) | Overrides attempt count |
408
-
409
- ---
410
-
411
- ### onError
412
-
413
- Runs after retry exhaustion.
414
-
415
- ```ts
416
- .onError(async (ctx, api) => {
417
- console.error(ctx.error);
418
- api.restart();
419
- })
420
- ```
421
-
422
- #### Available APIs
423
-
424
- | API | Description |
425
- |---|---|
426
- | setResult(value) | Returns fallback value |
427
- | throwError(error) | Throws an error |
428
- | restart() | Restarts retry pipeline |
429
-
430
- ---
431
-
432
- ## vigor.parse
433
-
434
- ### Methods
435
-
436
- | Method | Description |
437
- |---|---|
438
- | target(response) | Sets target Response object |
439
- | settings(fn \| config) | Configures parser settings |
440
- | strategies(fn) | Configures parser strategies |
441
- | parsers(fn \| config) | Registers custom parsers |
442
- | interceptors(fn \| config) | Configures parser interceptors |
443
- | request(config?) | Executes parse pipeline |
444
-
445
- ---
446
-
447
- ### target
448
-
449
- Sets the target `Response`.
450
-
451
- ```ts
452
- const response = await fetch("/api");
453
-
454
- await vigor
455
- .parse(response)
456
- .request();
457
- ```
458
-
459
- ---
460
-
461
- ### settings
462
-
463
- Configures parser behavior.
464
-
465
- ```ts
466
- .settings(s =>
467
- s
468
- .original(false)
469
- .fallback(null)
470
- )
471
- ```
472
-
473
- #### Settings API
474
-
475
- | Method | Description | Default |
476
- |---|---|---|
477
- | original(boolean) | Returns original Response object | `false` |
478
- | fallback(value) | Fallback return value on parse failure | `throws` |
479
-
480
- ---
481
-
482
- ### strategies
483
-
484
- Configures parser strategy.
485
-
486
- ```ts
487
- .strategies(s =>
488
- s.contentType()
489
- )
490
- ```
491
-
492
- ---
493
-
494
- ## Parse Strategies
495
-
496
- ### contentType
497
-
498
- Parses response using `content-type` header.
499
-
500
- ```ts
501
- .strategies(s =>
502
- s.contentType()
503
- )
504
- ```
505
-
506
- Supported:
507
-
508
- - JSON
509
- - Text
510
- - Blob
511
- - FormData
512
- - ArrayBuffer
513
- - Audio
514
- - Video
515
- - Image
516
-
517
- ---
518
-
519
- ### sniff
520
-
521
- Attempts all parsers until one succeeds.
522
-
523
- ```ts
524
- .strategies(s =>
525
- s.sniff()
526
- )
527
- ```
528
-
529
- Useful when:
530
-
531
- - content-type is invalid
532
- - server responses are inconsistent
533
- - APIs return malformed headers
534
-
535
- ---
536
-
537
- ### custom
538
-
539
- Uses custom parser strategy.
540
-
541
- ```ts
542
- .strategies(s =>
543
- s.custom()
544
- .func(async ({ response, parsers }) => {
545
- return await parsers.json();
546
- })
547
- )
548
- ```
549
-
550
- #### API
551
-
552
- | Method | Description |
553
- |---|---|
554
- | func(fn) | Custom parse resolver |
555
-
556
- ---
557
-
558
- ### parsers
559
-
560
- Registers custom parsers.
561
-
562
- ```ts
563
- .parsers(p =>
564
- p.add("csv", async response => {
565
- return parseCSV(await response.text());
566
- })
567
- )
568
- ```
569
-
570
- #### Parser API
571
-
572
- | Method | Description |
573
- |---|---|
574
- | add(name, parser) | Adds custom parser |
575
- | remove(name) | Removes parser |
576
- | clear() | Removes all parsers |
577
-
578
- ---
579
-
580
- ### request
581
-
582
- Executes parse workflow.
583
-
584
- ```ts
585
- const result = await vigor
586
- .parse(response)
587
- .request();
588
- ```
589
-
590
- ---
591
-
592
- ## Interceptors
593
-
594
- | Name | API |
595
- |---|:---:|
596
- | before | throwError |
597
- | after | setResult / throwError |
598
- | result | setResult / throwError |
599
- | onError | setResult / throwError |
600
-
601
- ---
602
-
603
- ### before
604
-
605
- Runs before parsing starts.
606
-
607
- ```ts
608
- .before(async (ctx, api) => {
609
- console.log(ctx.response.headers);
610
- })
611
- ```
612
-
613
- #### Available APIs
614
-
615
- | API | Description |
616
- |---|---|
617
- | throwError(error) | Immediately throws error |
618
-
619
- ---
620
-
621
- ### after
622
-
623
- Runs after parser succeeds.
624
-
625
- ```ts
626
- .after(async (ctx, api) => {
627
- api.setResult({
628
- wrapped: ctx.result
629
- });
630
- })
631
- ```
632
-
633
- #### Available APIs
634
-
635
- | API | Description |
636
- |---|---|
637
- | setResult(value) | Replaces parsed result |
638
- | throwError(error) | Throws error |
639
-
640
- ---
641
-
642
- ### result
643
-
644
- Runs before returning final result.
645
-
646
- ```ts
647
- .result(async (ctx, api) => {
648
- api.setResult(transform(ctx.result));
649
- })
650
- ```
651
-
652
- #### Available APIs
653
-
654
- | API | Description |
655
- |---|---|
656
- | setResult(value) | Replaces final result |
657
- | throwError(error) | Throws error |
658
-
659
- ---
660
-
661
- ### onError
662
-
663
- Runs when parsing fails.
664
-
665
- ```ts
666
- .onError(async (ctx, api) => {
667
- api.setResult(null);
668
- })
669
- ```
670
-
671
- #### Available APIs
672
-
673
- | API | Description |
674
- |---|---|
675
- | setResult(value) | Returns fallback value |
676
- | throwError(error) | Throws error |
677
-
678
- ---
679
-
680
- ## Example
681
-
682
- ```ts
683
- const response = await fetch("/api");
684
-
685
- const data = await vigor
686
- .parse(response)
687
- .strategies(s =>
688
- s.sniff()
689
- )
690
- .interceptors(i =>
691
- i.onError((ctx, api) => {
692
- console.log(ctx.error);
693
-
694
- api.setResult(null);
695
- })
696
- )
697
- .request();
698
- ```
699
-
700
- ## vigor.fetch
701
-
702
- ### Methods
703
-
704
- | Method | Description |
705
- |---|---|
706
- | method(http method) | Forces the http method |
707
- | origin(...paths) | Sets base URL and origin paths |
708
- | path(...paths) | Appends request paths |
709
- | query(params) | Sets query parameters |
710
- | headers(headers) | Sets request headers |
711
- | options(options) | Sets fetch options |
712
- | retryConfig(fn \| config) | Configures retry engine |
713
- | parseConfig(fn \| config) | Configures parse engine |
714
- | interceptors(fn \| config) | Configures fetch interceptors |
715
- | abortSignals(...signals) | Attaches external AbortSignals |
716
- | request(config?) | Executes fetch pipeline |
717
-
718
- ---
719
-
720
- ### origin
721
-
722
- Sets base URL and origin paths.
723
-
724
- ```ts
725
- .fetch("https://api.example.com/", "/api")
726
- ```
727
-
728
- Produces:
729
-
730
- ```txt
731
- https://api.example.com/api
732
- ```
733
-
734
- ---
735
-
736
- ### path
737
-
738
- Appends request paths.
739
-
740
- ```ts
741
- .path("v1", "users")
742
- ```
743
-
744
- Produces:
745
-
746
- ```txt
747
- https://api.example.com/api/v1/users
748
- ```
749
-
750
- ---
751
-
752
- ### query
753
-
754
- Adds query parameters.
755
-
756
- ```ts
757
- .query({
758
- page: 1,
759
- limit: 10
760
- })
761
- ```
762
-
763
- Produces:
764
-
765
- ```txt
766
- ?page=1&limit=10
767
- ```
768
-
769
- ---
770
-
771
- ### headers
772
-
773
- Sets request headers.
774
-
775
- ```ts
776
- .headers({
777
- Authorization: "Bearer token"
778
- })
779
- ```
780
-
781
- ---
782
-
783
- ### options
784
-
785
- Sets native fetch options.
786
-
787
- ```ts
788
- .options({
789
- method: "POST",
790
- body: JSON.stringify({
791
- username: "john"
792
- })
793
- })
794
- ```
795
-
796
- ---
797
-
798
- ### retryConfig
799
-
800
- Configures internal retry engine.
801
-
802
- ```ts
803
- .retryConfig(r => r
804
- .settings(s => s
805
- .attempt(5)
806
- )
807
- )
808
- ```
809
-
810
- ---
811
-
812
- ### parseConfig
813
-
814
- Configures internal parse engine.
815
-
816
- ```ts
817
- .parseConfig(p => p
818
- .strategies(s => s
819
- .sniff()
820
- )
821
- )
822
- ```
823
-
824
- ---
825
-
826
- ### abortSignals
827
-
828
- Attaches external AbortSignals.
829
-
830
- ```ts
831
- const controller = new AbortController();
832
-
833
- await vigor
834
- .fetch("/api")
835
- .abortSignals(controller.signal)
836
- .request();
837
- ```
838
-
839
- ---
840
-
841
- ### request
842
-
843
- Executes fetch workflow.
844
-
845
- ```ts
846
- const data = await vigor
847
- .fetch("https://api.example.com")
848
- .request();
849
- ```
850
-
851
- ---
852
-
853
- ## Interceptors
854
-
855
- | Name | API |
856
- |---|:---:|
857
- | before | throwError / abort |
858
- | after | setResponse / throwError |
859
- | result | setResult / throwError |
860
- | onError | setResult / throwError / retry |
861
-
862
- ---
863
-
864
- ### before
865
-
866
- Runs before fetch execution.
867
-
868
- ```ts
869
- .before(async (ctx, api) => {
870
- console.log(ctx.url);
871
- })
872
- ```
873
-
874
- #### Available APIs
875
-
876
- | API | Description |
877
- |---|---|
878
- | throwError(error) | Immediately throws error |
879
- | abort(error) | Aborts request |
880
-
881
- ---
882
-
883
- ### after
884
-
885
- Runs after receiving Response.
886
-
887
- ```ts
888
- .after(async (ctx, api) => {
889
- console.log(ctx.response.status);
890
- })
891
- ```
892
-
893
- #### Available APIs
894
-
895
- | API | Description |
896
- |---|---|
897
- | setResponse(response) | Replaces Response object |
898
- | throwError(error) | Throws error |
899
-
900
- ---
901
-
902
- ### result
903
-
904
- Runs before returning parsed result.
905
-
906
- ```ts
907
- .result(async (ctx, api) => {
908
- api.setResult({
909
- data: ctx.result
910
- });
911
- })
912
- ```
913
-
914
- #### Available APIs
915
-
916
- | API | Description |
917
- |---|---|
918
- | setResult(value) | Replaces final result |
919
- | throwError(error) | Throws error |
920
-
921
- ---
922
-
923
- ### onError
924
-
925
- Runs when fetch fails.
926
-
927
- ```ts
928
- .onError(async (ctx, api) => {
929
- api.retry();
930
- })
931
- ```
932
-
933
- #### Available APIs
934
-
935
- | API | Description |
936
- |---|---|
937
- | setResult(value) | Returns fallback value |
938
- | throwError(error) | Throws error |
939
- | restart() | Re-executes fetch pipeline |
940
-
941
- ---
942
-
943
- ## Example
944
-
945
- ```ts
946
- const data = await vigor
947
- .fetch("https://api.example.com", "api")
948
- .path("v1", "users")
949
- .query({
950
- page: 1
951
- })
952
- .headers({
953
- Authorization: "Bearer token"
954
- })
955
- .retryConfig(r =>
956
- r.settings(s =>
957
- s.attempt(5)
958
- )
959
- )
960
- .request();
961
- ```
962
-
963
- ## vigor.all
964
-
965
- ### Methods
966
-
967
- | Method | Description |
968
- |---|---|
969
- | target(...tasks) | Sets async task list |
970
- | settings(fn \| config) | Configures concurrency settings |
971
- | interceptors(fn \| config) | Configures concurrency interceptors |
972
- | abortSignals(...signals) | Attaches external AbortSignals |
973
- | request(config?) | Executes concurrency pipeline |
974
-
975
- ---
976
-
977
- ### target
978
-
979
- Sets async task list.
980
-
981
- ```ts
982
- .all(
983
- async () => fetch("/api/1"),
984
- async () => fetch("/api/2")
985
- )
986
- ```
987
-
988
- ---
989
-
990
- ### settings
991
-
992
- Configures concurrency behavior.
993
-
994
- ```ts
995
- .settings(s => s
996
- .concurrency(2)
997
- .onlySuccess(true)
998
- )
999
- ```
1000
-
1001
- #### Settings API
1002
-
1003
- | Method | Description | Default |
1004
- |---|---|---|
1005
- | concurrency(number) | Maximum concurrent tasks | `Infinity` |
1006
- | onlySuccess(boolean) | Returns only successful results | `false` |
1007
- | fallback(value) | Fallback return value | `throws` |
1008
-
1009
- ---
1010
-
1011
- ### abortSignals
1012
-
1013
- Attaches external AbortSignals.
1014
-
1015
- ```ts
1016
- const controller = new AbortController();
1017
-
1018
- await vigor
1019
- .all(task1, task2)
1020
- .abortSignals(controller.signal)
1021
- .request();
1022
- ```
1023
-
1024
- ---
1025
-
1026
- ### request
1027
-
1028
- Executes concurrency workflow.
1029
-
1030
- ```ts
1031
- const results = await vigor
1032
- .all(task1, task2)
1033
- .request();
1034
- ```
1035
-
1036
- ---
1037
-
1038
- ## Interceptors
1039
-
1040
- | Name | API |
1041
- |---|:---:|
1042
- | before | throwError / abort |
1043
- | afterEach | setResult / throwError |
1044
- | after | setResults / throwError |
1045
- | result | setResults / throwError |
1046
- | onError | setResults / throwError |
1047
-
1048
- ---
1049
-
1050
- ### before
1051
-
1052
- Runs before task execution starts.
1053
-
1054
- ```ts
1055
- .before(async (ctx, api) => {
1056
- console.log(ctx.tasks.length);
1057
- })
1058
- ```
1059
-
1060
- #### Available APIs
1061
-
1062
- | API | Description |
1063
- |---|---|
1064
- | throwError(error) | Immediately throws error |
1065
- | abort(error) | Aborts execution |
1066
-
1067
- ---
1068
-
1069
- ### after
1070
-
1071
- Runs after all tasks complete.
1072
-
1073
- ```ts
1074
- .after(async (ctx, api) => {
1075
- console.log(ctx.results);
1076
- })
1077
- ```
1078
-
1079
- #### Available APIs
1080
-
1081
- | API | Description |
1082
- |---|---|
1083
- | setResults(value) | Replaces result array |
1084
- | throwError(error) | Throws error |
1085
-
1086
- ---
1087
-
1088
- ### result
1089
-
1090
- Runs before returning final results.
1091
-
1092
- ```ts
1093
- .result(async (ctx, api) => {
1094
- api.setResults(
1095
- ctx.results.filter(Boolean)
1096
- );
1097
- })
1098
- ```
1099
-
1100
- #### Available APIs
1101
-
1102
- | API | Description |
1103
- |---|---|
1104
- | setResults(value) | Replaces final results |
1105
- | throwError(error) | Throws error |
1106
-
1107
- ---
1108
-
1109
- ### onError
1110
-
1111
- Runs when concurrency execution fails.
1112
-
1113
- ```ts
1114
- .onError(async (ctx, api) => {
1115
- api.setResults([]);
1116
- })
1117
- ```
1118
-
1119
- #### Available APIs
1120
-
1121
- | API | Description |
1122
- |---|---|
1123
- | setResults(value) | Returns fallback results |
1124
- | throwError(error) | Throws error |
1125
-
1126
- ---
1127
-
1128
- ## Example
1129
-
1130
- ```ts
1131
- const results = await vigor
1132
- .all(
1133
- async () => fetch("/api/1"),
1134
- async () => fetch("/api/2"),
1135
- async () => fetch("/api/3")
1136
- )
1137
- .settings(s =>
1138
- s
1139
- .concurrency(2)
1140
- .onlySuccess(true)
1141
- )
1142
- .request();
1143
- ```
1
+ # vigor-fetch
2
+
3
+ **A composable, lightweight network workflow toolkit built on native Fetch.**
4
+
5
+ Vigor gives you an immutable, chainable API for building robust network logic: retry with configurable backoff, content-aware response parsing, and concurrency-limited task running — all composed together, or used standalone.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - 🧩 **Immutable, fluent builders** — every call returns a new instance; nothing is mutated in place
12
+ - 🔁 **Two-level retry** — per-attempt retry/backoff *and* whole-pipeline restart, each independently configurable
13
+ - 🌐 **Smart fetch layer** — automatic `429 Retry-After` handling, configurable "never retry" status codes
14
+ - 🔌 **Content-aware parsing** — auto-detects `Content-Type`, with a sniffing fallback and custom strategies
15
+ - **Bounded concurrency runner** — run many tasks with a concurrency cap and per-task hooks
16
+ - 🪝 **Two middleware modes** — `"fluent"` (return a value) or `"intercept"` (get an explicit control API)
17
+ - 🧠 **TypeScript-first** — config shape is inferred and carried through the whole chain
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install vigor-fetch
25
+ ```
26
+
27
+ ## Import
28
+
29
+ ```ts
30
+ import vigor from "vigor-fetch";
31
+ // or
32
+ import { vigor } from "vigor-fetch";
33
+ ```
34
+
35
+ Both work `vigor` is exported as both the default and a named export. Every internal class (`VigorFetch`, `VigorRetry`, `VigorParse`, `VigorAll`, their settings/middleware/strategy/algorithm builders, and error classes) is also exported by name if you want to construct them directly instead of going through `vigor.*`.
36
+
37
+ ---
38
+
39
+ ## Quick start
40
+
41
+ ### Fetch
42
+
43
+ ```ts
44
+ import vigor from "vigor-fetch";
45
+
46
+ const data = await vigor
47
+ .fetch("https://api.example.com")
48
+ .path("v1", "users")
49
+ .query({ page: 1, limit: 10 })
50
+ .request();
51
+ // -> GET https://api.example.com/v1/users?page=1&limit=10
52
+ ```
53
+
54
+ ### Retry (standalone)
55
+
56
+ ```ts
57
+ const data = await vigor
58
+ .retry(async (signal) => {
59
+ return await db.select("...");
60
+ })
61
+ .request();
62
+ ```
63
+
64
+ ### Parse (standalone)
65
+
66
+ ```ts
67
+ const response = await fetch("https://api.example.com");
68
+
69
+ const data = await vigor
70
+ .parse(response)
71
+ .request();
72
+ ```
73
+
74
+ ### Concurrency
75
+
76
+ ```ts
77
+ const results = await vigor
78
+ .all(
79
+ () => fetch("/api/1"),
80
+ () => fetch("/api/2")
81
+ )
82
+ .request();
83
+ ```
84
+
85
+ ---
86
+
87
+ ## The `vigor` object
88
+
89
+ ```ts
90
+ vigor.fetch(origin?) // -> VigorFetch, origin pre-set if given
91
+ vigor.retry(task?) // -> VigorRetry, target pre-set if given
92
+ vigor.parse(response?) // -> VigorParse, target pre-set if given
93
+ vigor.all(...tasks) // -> VigorAll, target pre-set if any tasks given
94
+ vigor.use(plugin, options?)
95
+ ```
96
+
97
+ Every module follows the same shape: an **immutable builder** with `.settings()`, `.middlewares()`, and a terminal `.request()` / `.requestVerbose()` pair.
98
+
99
+ - `.request()` returns the result directly and **throws** on final failure.
100
+ - `.requestVerbose()` never throws — it returns `{ success: true, data, error: null }` or `{ success: false, data: null, error }`.
101
+
102
+ ---
103
+
104
+ ## `vigor.fetch`
105
+
106
+ ### Methods
107
+
108
+ | Method | Description |
109
+ |---|---|
110
+ | `method(str)` | Forces the HTTP method. Default: `POST` if a body is set, otherwise `GET` |
111
+ | `origin(str)` | Sets the base URL (absolute) or a path relative to the current page |
112
+ | `path(...strs)` | Appends path segments |
113
+ | `query(...objs)` | Adds query-parameter objects (each call appends another object to the list) |
114
+ | `hash(str)` | Sets the URL fragment, replacing any previous value |
115
+ | `headers(mode, obj)` | Sets headers. `mode`: `"overwrite"` (discard previous) or `"merge"` (`Object.assign`-style) |
116
+ | `body(mode, obj)` | Sets the body. `"overwrite"` replaces it entirely; `"merge"` shallow-merges when both old and new body are plain objects, otherwise replaces |
117
+ | `options(mode, obj)` | Sets extra native `RequestInit` fields other than `method`/`headers`/`body`/`signal` (e.g. `credentials`, `mode`, `cache`) |
118
+ | `settings(fn \| config)` | Configures fetch-level settings (below) |
119
+ | `middlewares(fn \| config)` | Configures the fetch middleware pipeline (below) |
120
+ | `retry(fn \| config \| VigorRetry)` | Configures the **internal retry engine** used to send the request |
121
+ | `parse(fn \| config \| VigorParse)` | Configures the **internal parse engine** used to interpret the response |
122
+ | `request()` | Executes and returns the parsed result, throwing on failure |
123
+ | `requestVerbose()` | Executes and returns `{ success, data, error }` |
124
+
125
+ > `path()`, `query()`, `headers()` in merge mode, etc. are all additive per call — chain them incrementally rather than passing everything in one call.
126
+
127
+ ### origin / path / query / hash
128
+
129
+ ```ts
130
+ vigor.fetch("https://api.example.com/", "/api")
131
+ .path("v1", "users")
132
+ .query({ page: 1 })
133
+ .hash("section-2")
134
+ // -> https://api.example.com/api/v1/users?page=1#section-2
135
+ ```
136
+
137
+ `origin()` only accepts one argument — pass origin + first path segments as separate calls, or fold extra segments into `path()`.
138
+
139
+ ### headers / body / options
140
+
141
+ ```ts
142
+ vigor.fetch("/api")
143
+ .headers("merge", { Authorization: "Bearer token" })
144
+ .body("overwrite", { username: "john" })
145
+ .options("merge", { credentials: "include" })
146
+ ```
147
+
148
+ Body is auto-normalized based on type (string, `Blob`, `ArrayBuffer`, `URLSearchParams`, `FormData`, or plain object → JSON), and a matching `Content-Type` header is set automatically unless you've already set one via `.headers()`.
149
+
150
+ ### settings
151
+
152
+ ```ts
153
+ .settings(s => s
154
+ .unretryStatus(400, 401, 403, 404, 405, 413, 422)
155
+ .retryHeaders("retry-after", "x-ratelimit-reset")
156
+ .maxRestarts(3)
157
+ .default(ctx => ({ fallback: true }))
158
+ )
159
+ ```
160
+
161
+ | Method | Description | Default |
162
+ |---|---|---|
163
+ | `retryHeaders(...strs)` | Response headers checked for a `429` retry delay | `["retry-after", "ratelimit-reset", "x-ratelimit-reset", "x-retry-after", "x-amz-retry-after", "chrome-proxy-next-link"]` |
164
+ | `unretryStatus(...nums)` | HTTP status codes that are never retried, regardless of the retry engine's attempt budget | `[400, 401, 403, 404, 405, 413, 422]` |
165
+ | `maxRestarts(num)` | Max number of full pipeline restarts (see `onError` below) | `3` |
166
+ | `default(fn)` | Fallback value/factory used only if the request ultimately fails and no restart/override happened | throws |
167
+
168
+ On any non-`unretryStatus` failure response, the internal retry engine retries per its own `settings`/`algorithms` (defaults: 5 attempts, 20s timeout, exponential-ish backoff jitter — see `vigor.retry` below). On a `429`, Vigor reads `retryHeaders` off the response and uses that value (seconds or an HTTP date) as the next delay automatically.
169
+
170
+ ### interceptors (`middlewares`)
171
+
172
+ Every stage takes a mode as the first argument: `"fluent"` (return a plain value) or `"intercept"` (receive an explicit API object and return the context).
173
+
174
+ ```ts
175
+ .middlewares(m => m
176
+ .before("intercept", async (ctx, api) => {
177
+ api.setHeaders({ ...ctx.options.headers, "X-Trace": crypto.randomUUID() });
178
+ return ctx;
179
+ })
180
+ .onError("intercept", async (ctx, api) => {
181
+ if (ctx.error instanceof SomeRecoverableError) api.proceedRestart();
182
+ return ctx;
183
+ })
184
+ )
185
+ ```
186
+
187
+ | Stage | Runs | Intercept API |
188
+ |---|---|---|
189
+ | `before` | Before the request is sent | `throwError`, `setOptions`, `setHeaders`, `setBody` |
190
+ | `after` | After the response is parsed | `setResult`, `throwError` |
191
+ | `onResult` | Right before returning the final result | `setResult`, `throwError` |
192
+ | `onError` | After the request has finally failed | `setResult`, `throwError`, `proceedRestart`, `cancelRestart` |
193
+
194
+ Fluent-mode signatures: `before`/`after` take `(ctx) => void`, `onResult` takes `(res) => newRes`, `onError` takes `(err) => void`.
195
+
196
+ `onError` → `proceedRestart()` re-runs the **entire** fetch pipeline from scratch (up to `settings.maxRestarts` times) — this is separate from, and on top of, the internal retry engine's per-attempt retries.
197
+
198
+ ### retry / parse (nested engines)
199
+
200
+ ```ts
201
+ const data = await vigor
202
+ .fetch("/api/users")
203
+ .retry(r => r.settings(s => s.maxAttempts(8)).algorithms(a => a.backoff({ multiplier: 2 })))
204
+ .parse(p => p.strategies(s => s.sniff()))
205
+ .request();
206
+ ```
207
+
208
+ Both accept a config object, a builder callback, or an already-built `VigorRetry`/`VigorParse` instance. The target function you'd normally set with `.target()` is ignored here — fetch always supplies its own target (the actual `fetch()` call) and its own `.target()` (`response.clone()` fed through your parse config).
209
+
210
+ ---
211
+
212
+ ## `vigor.retry`
213
+
214
+ ### Methods
215
+
216
+ | Method | Description |
217
+ |---|---|
218
+ | `target(fn)` | Sets the function to run: `(signal: AbortSignal) => any \| Promise<any>` |
219
+ | `abortSignals(...sigs)` | Attaches external `AbortSignal`s that cancel the current attempt when aborted |
220
+ | `settings(fn \| config)` | General retry settings (below) |
221
+ | `middlewares(fn \| config)` | Middleware pipeline (below) |
222
+ | `algorithms(fn)` | Selects and configures the delay algorithm (below) |
223
+ | `request()` / `requestVerbose()` | Executes |
224
+
225
+ ```ts
226
+ const controller = new AbortController();
227
+
228
+ await vigor
229
+ .retry(async (signal) => fetch("/api", { signal }))
230
+ .abortSignals(controller.signal)
231
+ .settings(s => s.maxAttempts(5).timeout(5000))
232
+ .request();
233
+ ```
234
+
235
+ ### settings
236
+
237
+ | Method | Description | Default |
238
+ |---|---|---|
239
+ | `default(fn)` | Fallback value/factory when the target ultimately fails | throws |
240
+ | `maxAttempts(num)` | Max attempts before giving up on this run | `5` |
241
+ | `maxRestarts(num)` | Max full pipeline restarts (see `onError`) | `3` |
242
+ | `timeout(ms)` | Timeout per individual attempt | `20000` |
243
+
244
+ ### algorithms
245
+
246
+ Delay algorithm is a **replace**, not merge — switching algorithms discards the previous one's config.
247
+
248
+ ```ts
249
+ .algorithms(a => a.backoff({ initial: 1000, multiplier: 1.7 }))
250
+ ```
251
+
252
+ | Algorithm | Fields (with defaults) |
253
+ |---|---|
254
+ | `constant(cfg?)` | `interval` (`2000`), `jitter` (`1000`) |
255
+ | `linear(cfg?)` | `initial` (`500`), `increment` (`1000`), `minDelay` (`0`), `maxDelay` (`10000`), `jitter` (`1000`) |
256
+ | `backoff(cfg?)` | `initial` (`0`), `multiplier` (`1.7`), `unit` (`1000`), `minDelay` (`500`), `maxDelay` (`10000`), `jitter` (`800`) |
257
+ | `custom(cfg?)` | `target: (attempt) => number` (required), `minDelay` (`500`), `maxDelay` (`10000`), `jitter` (`800`) |
258
+
259
+ Each computed delay is clamped to `[minDelay, maxDelay]` (except `constant`, which has no bounds) and then perturbed by `±jitter` ms.
260
+
261
+ ```ts
262
+ .algorithms(a => a.custom({
263
+ target: (attempt) => attempt * 3000
264
+ }))
265
+ ```
266
+
267
+ ### interceptors (`middlewares`)
268
+
269
+ ```ts
270
+ .middlewares(m => m
271
+ .retryIf("intercept", async (ctx, api) => {
272
+ if (ctx.error instanceof TypeError) api.proceedRetry();
273
+ else api.cancelRetry();
274
+ return ctx;
275
+ })
276
+ .onRetry("intercept", async (ctx, api) => {
277
+ api.setDelay(5000);
278
+ return ctx;
279
+ })
280
+ )
281
+ ```
282
+
283
+ | Stage | Runs | Intercept API |
284
+ |---|---|---|
285
+ | `before` | Before each attempt | `throwError`, `abort`, `breakRetry` |
286
+ | `after` | After the target settles successfully | `setResult`, `throwError` |
287
+ | `onResult` | Right before returning the attempt's result | `setResult`, `throwError` |
288
+ | `retryIf` | After a failed attempt, decides whether to retry | `proceedRetry`, `cancelRetry` |
289
+ | `onRetry` | Right before the retry delay is awaited | `throwError`, `setDelay` |
290
+ | `onError` | After attempts are exhausted (or `breakRetry` was called) | `setResult`, `throwError`, `proceedRestart`, `cancelRestart` |
291
+
292
+ Fluent-mode signatures: `before`/`after` take `(ctx) => void`, `onResult` takes `(res) => newRes`, `retryIf` takes `(err) => boolean`, `onRetry`/`onError` take `(err) => void`.
293
+
294
+ There are two independent resilience layers here:
295
+ - **`retryIf`/`onRetry`** loop through attempts (bounded by `settings.maxAttempts`) without ever fully failing.
296
+ - **`onError`** fires once attempts are exhausted; calling `api.proceedRestart()` there resets the attempt counter and starts over (bounded by `settings.maxRestarts`).
297
+
298
+ ---
299
+
300
+ ## `vigor.parse`
301
+
302
+ ### Methods
303
+
304
+ | Method | Description |
305
+ |---|---|
306
+ | `target(response)` | Sets the `Response` to parse |
307
+ | `settings(fn \| config)` | General settings (below) |
308
+ | `middlewares(fn \| config)` | Middleware pipeline (below) |
309
+ | `strategies(fn \| config)` | Configures the ordered fallback chain of parsing strategies |
310
+ | `request()` / `requestVerbose()` | Executes |
311
+
312
+ ### settings
313
+
314
+ | Method | Description | Default |
315
+ |---|---|---|
316
+ | `raw(bool)` | When `true`, skips parsing entirely and returns the raw `Response` object | `false` |
317
+ | `default(fn)` | Fallback value/factory when parsing ultimately fails | throws |
318
+ | `maxRestarts(num)` | Max full pipeline restarts | `3` |
319
+
320
+ ### strategies
321
+
322
+ Strategies form an ordered list that's tried in sequence until one succeeds (each attempt but the last clones the response first, so a failed strategy doesn't consume the body). If you never configure any, Vigor defaults to content-type detection.
323
+
324
+ ```ts
325
+ .strategies(s => s.sniff())
326
+ ```
327
+
328
+ | Method | Behavior |
329
+ |---|---|
330
+ | `contentType(replace?)` | Parses based on the response's `Content-Type` header (JSON / XML / form-urlencoded / octet-stream / image / audio / video / multipart / text) |
331
+ | `sniff(replace?)` | Tries `json → formData → text → blob` in order until one parses |
332
+ | `json(replace?)` | Always parses as JSON |
333
+ | `text(replace?)` | Always parses as text |
334
+ | `arrayBuffer(replace?)` | Always parses as `ArrayBuffer` |
335
+ | `blob(replace?)` | Always parses as `Blob` |
336
+ | `bytes(replace?)` | Always parses as `Uint8Array` |
337
+ | `formData(replace?)` | Always parses as `FormData` |
338
+ | `custom(func, replace?)` | Uses a custom `(response: Response) => Promise<any>` |
339
+
340
+ By default each call **appends** to the chain (they concatenate, so you can layer fallbacks: e.g. `contentType()` then `sniff()` as a last resort). Pass `replace: true` as the trailing argument to replace the whole chain instead.
341
+
342
+ ### interceptors (`middlewares`)
343
+
344
+ | Stage | Runs | Intercept API |
345
+ |---|---|---|
346
+ | `before` | Before parsing starts | `throwError` |
347
+ | `after` | After a strategy succeeds | `setResult`, `throwError` |
348
+ | `onResult` | Right before returning the final result | `setResult`, `throwError` |
349
+ | `onError` | When every strategy in the chain failed | `setResult`, `throwError`, `proceedRestart`, `cancelRestart` |
350
+
351
+ ```ts
352
+ const data = await vigor
353
+ .parse(response)
354
+ .strategies(s => s.sniff())
355
+ .middlewares(m => m.onError("intercept", async (ctx, api) => {
356
+ api.setResult(null);
357
+ return ctx;
358
+ }))
359
+ .request();
360
+ ```
361
+
362
+ ---
363
+
364
+ ## `vigor.all`
365
+
366
+ Runs a fixed list of zero-argument tasks with bounded concurrency.
367
+
368
+ ### Methods
369
+
370
+ | Method | Description |
371
+ |---|---|
372
+ | `target(...funcs)` | Sets the task list — each is `() => unknown \| Promise<unknown>` |
373
+ | `settings(fn \| config)` | Concurrency settings (below) |
374
+ | `middlewares(fn \| config)` | Per-task hooks (below) |
375
+ | `request()` / `requestVerbose()` | Executes |
376
+
377
+ ```ts
378
+ const results = await vigor
379
+ .all(
380
+ () => fetch("/api/1"),
381
+ () => fetch("/api/2"),
382
+ () => fetch("/api/3")
383
+ )
384
+ .settings(s => s.concurrency(2).onlySuccess(true))
385
+ .request();
386
+ ```
387
+
388
+ ### settings
389
+
390
+ | Method | Description | Default |
391
+ |---|---|---|
392
+ | `concurrency(num)` | Max number of tasks running at once | `5` |
393
+ | `onlySuccess(bool)` | When `true`, filters the final array down to only tasks whose middleware chain resolved successfully | `false` |
394
+
395
+ > Unlike `vigor.retry`/`vigor.fetch`/`vigor.parse`, `vigor.all` has no `default`/fallback setting, and per-task failures don't throw the whole call by themselves — a task that fails (and whose `onError` middleware doesn't call `setResult`) simply contributes its error value into the results array (or is dropped, if `onlySuccess` is on). The only thing that makes `.request()` throw is an **empty** task list.
396
+
397
+ ### hooks (`middlewares`)
398
+
399
+ Unlike the other modules, `all`'s per-task hooks are plain functions — there's no `"fluent"`/`"intercept"` mode split.
400
+
401
+ | Stage | Signature | Runs |
402
+ |---|---|---|
403
+ | `before(fn)` | `(ctx) => void \| Promise<void>` | Before each task is invoked |
404
+ | `after(fn)` | `(ctx) => void \| Promise<void>` | After a task settles successfully |
405
+ | `onError(fn)` | `(ctx, { setResult }) => void \| Promise<void>` | When an individual task throws |
406
+
407
+ ```ts
408
+ .middlewares(m => m
409
+ .onError(async (ctx, api) => {
410
+ console.error(ctx.error);
411
+ api.setResult(null); // recover instead of letting this task register as failed
412
+ })
413
+ )
414
+ ```
415
+
416
+ ---
417
+
418
+ ## Plugins
419
+
420
+ ```ts
421
+ export type VigorPlugin<O> = {
422
+ options: O // type-only anchor for inferring O — not used at runtime
423
+ func: (v: typeof vigor, options: O) => void
424
+ }
425
+
426
+ vigor.use(plugin, options?)
427
+ ```
428
+
429
+ `vigor.use(plugin, options)` simply invokes `plugin.func(vigor, options)` — a lightweight hook for setup-time side effects (e.g. registering shared config, wiring up logging) rather than a full plugin/extension system.
430
+
431
+ ---
432
+
433
+ ## Error handling
434
+
435
+ Every module throws its own error subclass (`VigorFetchError`, `VigorRetryError`, `VigorParseError`, `VigorAllError`) — all extending a common `VigorError` with `.code`, `.data`, `.method`, `.context`, and `.timestamp`.
436
+
437
+ ```ts
438
+ try {
439
+ await vigor.fetch("/api/users").request();
440
+ } catch (err) {
441
+ if (err instanceof VigorFetchError && err.code === "FETCH_FAILED") {
442
+ console.log(err.data.status, err.data.parsed); // failed response's status + best-effort parsed body
443
+ }
444
+ }
445
+ ```
446
+
447
+ Or avoid try/catch entirely with `requestVerbose()`:
448
+
449
+ ```ts
450
+ const res = await vigor.fetch("/api/users").requestVerbose();
451
+ if (!res.success) {
452
+ // res.error is typed, res.data is null
453
+ }
454
+ ```
455
+
456
+ | Module | Error codes |
457
+ |---|---|
458
+ | `VigorFetchError` | `VALUE_REQUIRED`, `INVALID_BODY`, `INVALID_PROTOCOL`, `FETCH_FAILED`, `RESTART_EXHAUSTED` |
459
+ | `VigorRetryError` | `VALUE_REQUIRED`, `RETRY_EXHAUSTED`, `RESTART_EXHAUSTED`, `TIMED_OUT` |
460
+ | `VigorParseError` | `VALUE_REQUIRED`, `INVALID_CONTENT_TYPE`, `PARSER_NOT_FOUND`, `PARSER_ALL_FAILED`, `RESTART_EXHAUSTED` |
461
+ | `VigorAllError` | `EMPTY_TARGET` |
462
+
463
+ `FETCH_FAILED`'s `data` includes `parsed`: Vigor best-effort parses the failed response's body (using your configured parse strategies) before throwing, so you can inspect an API's error payload without re-fetching. If parsing that body also fails, `parsed` is simply `undefined`.
464
+
465
+ ---
466
+
467
+ ## TypeScript notes
468
+
469
+ - Every builder is **immutable** — `.settings(...)`, `.path(...)`, etc. all return a *new* instance with the change merged in; the original is untouched.
470
+ - Array-valued config (`path`, `query`, parse `strategies`, etc.) **concatenates** across calls by default; object-valued config deep-merges. A handful of methods (`hash`, `headers("overwrite", ...)`, `body("overwrite", ...)`, `options("overwrite", ...)`, `retry(...)`, `parse(...)`, `algorithms(...)`) force a full **replace** instead, since merging wouldn't make sense there.
471
+ - Config shape is tracked through the whole chain via TypeScript generics, so `.settings()`/`.middlewares()` callbacks and `.request()`'s inferred return type stay accurate as you chain.