@emmvish/stable-request 1.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.
- package/LICENSE +21 -0
- package/README.md +934 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/send-stable-request.d.ts +3 -0
- package/dist/core/send-stable-request.d.ts.map +1 -0
- package/dist/core/send-stable-request.js +107 -0
- package/dist/core/send-stable-request.js.map +1 -0
- package/dist/core/stable-api-gateway.d.ts +3 -0
- package/dist/core/stable-api-gateway.d.ts.map +1 -0
- package/dist/core/stable-api-gateway.js +30 -0
- package/dist/core/stable-api-gateway.js.map +1 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +101 -0
- package/dist/core.js.map +1 -0
- package/dist/enums/index.d.ts +28 -0
- package/dist/enums/index.d.ts.map +1 -0
- package/dist/enums/index.js +33 -0
- package/dist/enums/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +5 -0
- package/dist/test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +97 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utilities/delay.d.ts +2 -0
- package/dist/utilities/delay.d.ts.map +1 -0
- package/dist/utilities/delay.js +8 -0
- package/dist/utilities/delay.js.map +1 -0
- package/dist/utilities/execute-concurrently.d.ts +3 -0
- package/dist/utilities/execute-concurrently.d.ts.map +1 -0
- package/dist/utilities/execute-concurrently.js +36 -0
- package/dist/utilities/execute-concurrently.js.map +1 -0
- package/dist/utilities/execute-sequentially.d.ts +3 -0
- package/dist/utilities/execute-sequentially.d.ts.map +1 -0
- package/dist/utilities/execute-sequentially.js +32 -0
- package/dist/utilities/execute-sequentially.js.map +1 -0
- package/dist/utilities/generate-axios-request-config.d.ts +12 -0
- package/dist/utilities/generate-axios-request-config.d.ts.map +1 -0
- package/dist/utilities/generate-axios-request-config.js +14 -0
- package/dist/utilities/generate-axios-request-config.js.map +1 -0
- package/dist/utilities/get-new-delay-time.d.ts +3 -0
- package/dist/utilities/get-new-delay-time.d.ts.map +1 -0
- package/dist/utilities/get-new-delay-time.js +15 -0
- package/dist/utilities/get-new-delay-time.js.map +1 -0
- package/dist/utilities/index.d.ts +12 -0
- package/dist/utilities/index.d.ts.map +1 -0
- package/dist/utilities/index.js +12 -0
- package/dist/utilities/index.js.map +1 -0
- package/dist/utilities/is-retryable-error.d.ts +4 -0
- package/dist/utilities/is-retryable-error.d.ts.map +1 -0
- package/dist/utilities/is-retryable-error.js +22 -0
- package/dist/utilities/is-retryable-error.js.map +1 -0
- package/dist/utilities/prepare-api-request-options.d.ts +3 -0
- package/dist/utilities/prepare-api-request-options.d.ts.map +1 -0
- package/dist/utilities/prepare-api-request-options.js +19 -0
- package/dist/utilities/prepare-api-request-options.js.map +1 -0
- package/dist/utilities/req-fn.d.ts +4 -0
- package/dist/utilities/req-fn.d.ts.map +1 -0
- package/dist/utilities/req-fn.js +67 -0
- package/dist/utilities/req-fn.js.map +1 -0
- package/dist/utilities/safely-execute-unknown-function.d.ts +2 -0
- package/dist/utilities/safely-execute-unknown-function.d.ts.map +1 -0
- package/dist/utilities/safely-execute-unknown-function.js +8 -0
- package/dist/utilities/safely-execute-unknown-function.js.map +1 -0
- package/dist/utilities/safely-execute-unknown-functions.d.ts +2 -0
- package/dist/utilities/safely-execute-unknown-functions.d.ts.map +1 -0
- package/dist/utilities/safely-execute-unknown-functions.js +8 -0
- package/dist/utilities/safely-execute-unknown-functions.js.map +1 -0
- package/dist/utilities/safely-stringify.d.ts +4 -0
- package/dist/utilities/safely-stringify.d.ts.map +1 -0
- package/dist/utilities/safely-stringify.js +13 -0
- package/dist/utilities/safely-stringify.js.map +1 -0
- package/dist/utilities/validate-trial-mode-probabilities.d.ts +3 -0
- package/dist/utilities/validate-trial-mode-probabilities.d.ts.map +1 -0
- package/dist/utilities/validate-trial-mode-probabilities.js +13 -0
- package/dist/utilities/validate-trial-mode-probabilities.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
## stable-request
|
|
2
|
+
|
|
3
|
+
A robust HTTP request wrapper built on top of Axios with intelligent retry strategies, content validation, batch processing, and comprehensive observability features.
|
|
4
|
+
|
|
5
|
+
## Why stable-request?
|
|
6
|
+
|
|
7
|
+
Most HTTP client libraries only retry on network failures or specific HTTP status codes. **stable-request** goes further by providing:
|
|
8
|
+
|
|
9
|
+
- ✅ **Content-aware retries** - Validate response content and retry even on successful HTTP responses
|
|
10
|
+
- 🚀 **Batch processing** - Execute multiple requests concurrently or sequentially with shared configuration
|
|
11
|
+
- 🧪 **Trial mode** - Simulate failures to test your retry logic without depending on real network instability
|
|
12
|
+
- 📊 **Granular observability** - Monitor every attempt with detailed hooks
|
|
13
|
+
- ⚡ **Multiple retry strategies** - Fixed, linear, or exponential backoff
|
|
14
|
+
- 🎯 **Flexible error handling** - Custom error analysis and graceful degradation
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @emmvish/stable-request
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Single Request
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { stableRequest, REQUEST_METHODS, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
28
|
+
|
|
29
|
+
// Simple GET request with automatic retries
|
|
30
|
+
const response = await stableRequest({
|
|
31
|
+
reqData: {
|
|
32
|
+
hostname: 'api.example.com',
|
|
33
|
+
path: '/users',
|
|
34
|
+
method: REQUEST_METHODS.GET
|
|
35
|
+
},
|
|
36
|
+
resReq: true,
|
|
37
|
+
attempts: 3,
|
|
38
|
+
wait: 1000,
|
|
39
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Batch Requests
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { stableApiGateway } from '@emmvish/stable-request';
|
|
47
|
+
|
|
48
|
+
const requests = [
|
|
49
|
+
{
|
|
50
|
+
id: 'user-1',
|
|
51
|
+
requestOptions: {
|
|
52
|
+
reqData: { hostname: 'api.example.com', path: '/users/1' },
|
|
53
|
+
resReq: true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'user-2',
|
|
58
|
+
requestOptions: {
|
|
59
|
+
reqData: { hostname: 'api.example.com', path: '/users/2' },
|
|
60
|
+
resReq: true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const results = await stableApiGateway(requests, {
|
|
66
|
+
concurrentExecution: true,
|
|
67
|
+
commonAttempts: 3,
|
|
68
|
+
commonWait: 1000
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Core Features
|
|
73
|
+
|
|
74
|
+
### 1. Content-Aware Retries with `stableRequest`
|
|
75
|
+
|
|
76
|
+
Unlike traditional retry mechanisms, `stableRequest` validates the **content** of successful responses and retries if needed.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
await stableRequest({
|
|
80
|
+
reqData: {
|
|
81
|
+
hostname: 'api.example.com',
|
|
82
|
+
path: '/data',
|
|
83
|
+
},
|
|
84
|
+
resReq: true,
|
|
85
|
+
attempts: 5,
|
|
86
|
+
wait: 2000,
|
|
87
|
+
// Retry even on HTTP 200 if data is invalid
|
|
88
|
+
responseAnalyzer: async (reqConfig, data) => {
|
|
89
|
+
return data?.status === 'ready' && data?.items?.length > 0;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Use Cases:**
|
|
95
|
+
- Wait for async processing to complete
|
|
96
|
+
- Ensure data quality before proceeding
|
|
97
|
+
- Handle eventually-consistent systems
|
|
98
|
+
- Validate complex business rules in responses
|
|
99
|
+
|
|
100
|
+
### 2. Batch Processing with `stableApiGateway`
|
|
101
|
+
|
|
102
|
+
Process multiple requests efficiently with shared configuration and execution strategies.
|
|
103
|
+
|
|
104
|
+
#### Concurrent Execution
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { stableApiGateway, RETRY_STRATEGIES, REQUEST_METHODS } from '@emmvish/stable-request';
|
|
108
|
+
|
|
109
|
+
const requests = [
|
|
110
|
+
{
|
|
111
|
+
id: 'create-user-1',
|
|
112
|
+
requestOptions: {
|
|
113
|
+
reqData: {
|
|
114
|
+
hostname: 'api.example.com',
|
|
115
|
+
path: '/users',
|
|
116
|
+
method: REQUEST_METHODS.POST,
|
|
117
|
+
body: { name: 'John Doe', email: 'john@example.com' }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'create-user-2',
|
|
123
|
+
requestOptions: {
|
|
124
|
+
reqData: {
|
|
125
|
+
hostname: 'api.example.com',
|
|
126
|
+
path: '/users',
|
|
127
|
+
method: REQUEST_METHODS.POST,
|
|
128
|
+
body: { name: 'Jane Smith', email: 'jane@example.com' }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const results = await stableApiGateway(requests, {
|
|
135
|
+
concurrentExecution: true,
|
|
136
|
+
commonResReq: true,
|
|
137
|
+
commonAttempts: 3,
|
|
138
|
+
commonWait: 1000,
|
|
139
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Process results
|
|
143
|
+
results.forEach(result => {
|
|
144
|
+
if (result.success) {
|
|
145
|
+
console.log(`${result.id} succeeded:`, result.data);
|
|
146
|
+
} else {
|
|
147
|
+
console.error(`${result.id} failed:`, result.error);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### Sequential Execution
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const results = await stableApiGateway(requests, {
|
|
156
|
+
concurrentExecution: false,
|
|
157
|
+
stopOnFirstError: true,
|
|
158
|
+
commonAttempts: 3
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 3. Trial Mode - Test Your Retry Logic
|
|
163
|
+
|
|
164
|
+
Simulate request and retry failures with configurable probabilities.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
await stableRequest({
|
|
168
|
+
reqData: {
|
|
169
|
+
hostname: 'api.example.com',
|
|
170
|
+
path: '/test',
|
|
171
|
+
},
|
|
172
|
+
resReq: true,
|
|
173
|
+
attempts: 5,
|
|
174
|
+
trialMode: {
|
|
175
|
+
enabled: true,
|
|
176
|
+
reqFailureProbability: 0.3, // 30% chance each request fails
|
|
177
|
+
retryFailureProbability: 0.2 // 20% chance retry is marked non-retryable
|
|
178
|
+
},
|
|
179
|
+
logAllErrors: true
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Use Cases:**
|
|
184
|
+
- Integration testing
|
|
185
|
+
- Chaos engineering
|
|
186
|
+
- Validating monitoring and alerting
|
|
187
|
+
- Testing circuit breaker patterns
|
|
188
|
+
|
|
189
|
+
### 4. Multiple Retry Strategies
|
|
190
|
+
|
|
191
|
+
Choose the backoff strategy that fits your use case.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
195
|
+
|
|
196
|
+
// Fixed delay: 1s, 1s, 1s, 1s...
|
|
197
|
+
await stableRequest({
|
|
198
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
199
|
+
resReq: true,
|
|
200
|
+
attempts: 5,
|
|
201
|
+
wait: 1000,
|
|
202
|
+
retryStrategy: RETRY_STRATEGIES.FIXED
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Linear backoff: 1s, 2s, 3s, 4s...
|
|
206
|
+
await stableRequest({
|
|
207
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
208
|
+
resReq: true,
|
|
209
|
+
attempts: 5,
|
|
210
|
+
wait: 1000,
|
|
211
|
+
retryStrategy: RETRY_STRATEGIES.LINEAR
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Exponential backoff: 1s, 2s, 4s, 8s...
|
|
215
|
+
await stableRequest({
|
|
216
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
217
|
+
resReq: true,
|
|
218
|
+
attempts: 5,
|
|
219
|
+
wait: 1000,
|
|
220
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 5. Comprehensive Observability
|
|
225
|
+
|
|
226
|
+
Monitor every request attempt with detailed logging hooks.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
await stableRequest({
|
|
230
|
+
reqData: {
|
|
231
|
+
hostname: 'api.example.com',
|
|
232
|
+
path: '/critical-endpoint',
|
|
233
|
+
},
|
|
234
|
+
resReq: true,
|
|
235
|
+
attempts: 3,
|
|
236
|
+
logAllErrors: true,
|
|
237
|
+
handleErrors: async (reqConfig, errorLog) => {
|
|
238
|
+
// Custom error handling - send to monitoring service
|
|
239
|
+
await monitoringService.logError({
|
|
240
|
+
endpoint: reqConfig.url,
|
|
241
|
+
attempt: errorLog.attempt,
|
|
242
|
+
error: errorLog.error,
|
|
243
|
+
isRetryable: errorLog.isRetryable,
|
|
244
|
+
type: errorLog.type, // 'HTTP_ERROR' or 'INVALID_CONTENT'
|
|
245
|
+
timestamp: errorLog.timestamp,
|
|
246
|
+
executionTime: errorLog.executionTime,
|
|
247
|
+
statusCode: errorLog.statusCode
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
logAllSuccessfulAttempts: true,
|
|
251
|
+
handleSuccessfulAttemptData: async (reqConfig, successData) => {
|
|
252
|
+
// Track successful attempts
|
|
253
|
+
analytics.track('request_success', {
|
|
254
|
+
endpoint: reqConfig.url,
|
|
255
|
+
attempt: successData.attempt,
|
|
256
|
+
executionTime: successData.executionTime,
|
|
257
|
+
statusCode: successData.statusCode
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### 6. Smart Retry Logic
|
|
264
|
+
|
|
265
|
+
Automatically retries on common transient errors:
|
|
266
|
+
|
|
267
|
+
- HTTP 5xx (Server Errors)
|
|
268
|
+
- HTTP 408 (Request Timeout)
|
|
269
|
+
- HTTP 429 (Too Many Requests)
|
|
270
|
+
- HTTP 409 (Conflict)
|
|
271
|
+
- Network errors: `ECONNRESET`, `ETIMEDOUT`, `ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN`
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
await stableRequest({
|
|
275
|
+
reqData: {
|
|
276
|
+
hostname: 'unreliable-api.com',
|
|
277
|
+
path: '/data',
|
|
278
|
+
},
|
|
279
|
+
resReq: true,
|
|
280
|
+
attempts: 5,
|
|
281
|
+
wait: 2000,
|
|
282
|
+
retryStrategy: RETRY_STRATEGIES.LINEAR
|
|
283
|
+
// Automatically retries on transient failures
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 7. Final Error Analysis
|
|
288
|
+
|
|
289
|
+
Decide whether to throw or return false based on error analysis.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const result = await stableRequest({
|
|
293
|
+
reqData: {
|
|
294
|
+
hostname: 'api.example.com',
|
|
295
|
+
path: '/optional-data',
|
|
296
|
+
},
|
|
297
|
+
resReq: true,
|
|
298
|
+
attempts: 3,
|
|
299
|
+
finalErrorAnalyzer: async (reqConfig, error) => {
|
|
300
|
+
// Return true to suppress error and return false instead of throwing
|
|
301
|
+
if (error.message.includes('404')) {
|
|
302
|
+
console.log('Resource not found, treating as non-critical');
|
|
303
|
+
return true; // Don't throw, return false
|
|
304
|
+
}
|
|
305
|
+
return false; // Throw the error
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// result will be false if finalErrorAnalyzer returned true
|
|
310
|
+
if (result === false) {
|
|
311
|
+
console.log('Request failed but was handled gracefully');
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### 8. Request Cancellation Support
|
|
316
|
+
|
|
317
|
+
Support for AbortController to cancel requests.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
const controller = new AbortController();
|
|
321
|
+
|
|
322
|
+
setTimeout(() => controller.abort(), 5000);
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
await stableRequest({
|
|
326
|
+
reqData: {
|
|
327
|
+
hostname: 'api.example.com',
|
|
328
|
+
path: '/slow-endpoint',
|
|
329
|
+
signal: controller.signal
|
|
330
|
+
},
|
|
331
|
+
resReq: true,
|
|
332
|
+
attempts: 3
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
// Request was cancelled
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### 9. Perform All Attempts Mode
|
|
340
|
+
|
|
341
|
+
Execute all retry attempts regardless of success, useful for warm-up scenarios.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
await stableRequest({
|
|
345
|
+
reqData: {
|
|
346
|
+
hostname: 'api.example.com',
|
|
347
|
+
path: '/cache-warmup',
|
|
348
|
+
},
|
|
349
|
+
attempts: 5,
|
|
350
|
+
performAllAttempts: true, // Always performs all 5 attempts
|
|
351
|
+
wait: 1000
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## API Reference
|
|
356
|
+
|
|
357
|
+
### `stableRequest<RequestDataType, ResponseDataType>(options)`
|
|
358
|
+
|
|
359
|
+
Execute a single HTTP request with retry logic.
|
|
360
|
+
|
|
361
|
+
#### Configuration Options
|
|
362
|
+
|
|
363
|
+
| Option | Type | Default | Description |
|
|
364
|
+
|--------|------|---------|-------------|
|
|
365
|
+
| `reqData` | `REQUEST_DATA` | **required** | Request configuration (hostname, path, method, etc.) |
|
|
366
|
+
| `resReq` | `boolean` | `false` | Return response data instead of just success boolean |
|
|
367
|
+
| `attempts` | `number` | `1` | Maximum number of retry attempts |
|
|
368
|
+
| `wait` | `number` | `1000` | Base delay in milliseconds between retries |
|
|
369
|
+
| `retryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Retry strategy: `'fixed'`, `'linear'`, or `'exponential'` |
|
|
370
|
+
| `responseAnalyzer` | `function` | `() => true` | Validates response content, return false to retry |
|
|
371
|
+
| `performAllAttempts` | `boolean` | `false` | Execute all attempts regardless of success |
|
|
372
|
+
| `logAllErrors` | `boolean` | `false` | Enable error logging for all failed attempts |
|
|
373
|
+
| `handleErrors` | `function` | console.log | Custom error handler |
|
|
374
|
+
| `logAllSuccessfulAttempts` | `boolean` | `false` | Log all successful attempts |
|
|
375
|
+
| `handleSuccessfulAttemptData` | `function` | console.log | Custom success handler |
|
|
376
|
+
| `maxSerializableChars` | `number` | `1000` | Max characters for serialized logs |
|
|
377
|
+
| `finalErrorAnalyzer` | `function` | `() => false` | Analyze final error, return true to suppress throwing |
|
|
378
|
+
| `trialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Simulate failures for testing |
|
|
379
|
+
|
|
380
|
+
#### Request Data Configuration
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
interface REQUEST_DATA<RequestDataType = any> {
|
|
384
|
+
hostname: string; // Required
|
|
385
|
+
protocol?: 'http' | 'https'; // Default: 'https'
|
|
386
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // Default: 'GET'
|
|
387
|
+
path?: `/${string}`; // Default: ''
|
|
388
|
+
port?: number; // Default: 443
|
|
389
|
+
headers?: Record<string, any>; // Default: {}
|
|
390
|
+
body?: RequestDataType; // Request body
|
|
391
|
+
query?: Record<string, any>; // Query parameters
|
|
392
|
+
timeout?: number; // Default: 15000ms
|
|
393
|
+
signal?: AbortSignal; // For request cancellation
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### `stableApiGateway<RequestDataType, ResponseDataType>(requests, options)`
|
|
398
|
+
|
|
399
|
+
Execute multiple HTTP requests with shared configuration.
|
|
400
|
+
|
|
401
|
+
#### Gateway Configuration Options
|
|
402
|
+
|
|
403
|
+
| Option | Type | Default | Description |
|
|
404
|
+
|--------|------|---------|-------------|
|
|
405
|
+
| `concurrentExecution` | `boolean` | `true` | Execute requests concurrently or sequentially |
|
|
406
|
+
| `stopOnFirstError` | `boolean` | `false` | Stop execution on first error (sequential only) |
|
|
407
|
+
| `commonAttempts` | `number` | `1` | Default attempts for all requests |
|
|
408
|
+
| `commonPerformAllAttempts` | `boolean` | `false` | Default performAllAttempts for all requests |
|
|
409
|
+
| `commonWait` | `number` | `1000` | Default wait time for all requests |
|
|
410
|
+
| `commonRetryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Default retry strategy for all requests |
|
|
411
|
+
| `commonLogAllErrors` | `boolean` | `false` | Default error logging for all requests |
|
|
412
|
+
| `commonLogAllSuccessfulAttempts` | `boolean` | `false` | Default success logging for all requests |
|
|
413
|
+
| `commonMaxSerializableChars` | `number` | `1000` | Default max chars for serialization |
|
|
414
|
+
| `commonTrialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Default trial mode for all requests |
|
|
415
|
+
| `commonResponseAnalyzer` | `function` | `() => true` | Default response analyzer for all requests |
|
|
416
|
+
| `commonResReq` | `boolean` | `false` | Default resReq for all requests |
|
|
417
|
+
| `commonFinalErrorAnalyzer` | `function` | `() => false` | Default final error analyzer for all requests |
|
|
418
|
+
| `commonHandleErrors` | `function` | console.log | Default error handler for all requests |
|
|
419
|
+
| `commonHandleSuccessfulAttemptData` | `function` | console.log | Default success handler for all requests |
|
|
420
|
+
|
|
421
|
+
#### Request Format
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
interface API_GATEWAY_REQUEST<RequestDataType, ResponseDataType> {
|
|
425
|
+
id: string; // Unique identifier for the request
|
|
426
|
+
requestOptions: STABLE_REQUEST<RequestDataType, ResponseDataType>;
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**Note:** Individual request options override common options. If a specific option is not provided in `requestOptions`, the corresponding `common*` option is used.
|
|
431
|
+
|
|
432
|
+
#### Response Format
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
interface API_GATEWAY_RESPONSE<ResponseDataType> {
|
|
436
|
+
id: string; // Request identifier
|
|
437
|
+
success: boolean; // Whether the request succeeded
|
|
438
|
+
data?: ResponseDataType; // Response data (if success is true)
|
|
439
|
+
error?: string; // Error message (if success is false)
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Real-World Use Cases
|
|
444
|
+
|
|
445
|
+
### 1. Polling for Async Job Completion
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
const jobResult = await stableRequest({
|
|
449
|
+
reqData: {
|
|
450
|
+
hostname: 'api.example.com',
|
|
451
|
+
path: '/jobs/123/status',
|
|
452
|
+
},
|
|
453
|
+
resReq: true,
|
|
454
|
+
attempts: 20,
|
|
455
|
+
wait: 3000,
|
|
456
|
+
retryStrategy: RETRY_STRATEGIES.FIXED,
|
|
457
|
+
responseAnalyzer: async (reqConfig, data) => {
|
|
458
|
+
return data.status === 'completed';
|
|
459
|
+
},
|
|
460
|
+
handleErrors: async (reqConfig, error) => {
|
|
461
|
+
console.log(`Job not ready yet (attempt ${error.attempt})`);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### 2. Resilient External API Integration
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
const weatherData = await stableRequest({
|
|
470
|
+
reqData: {
|
|
471
|
+
hostname: 'api.weather.com',
|
|
472
|
+
path: '/current',
|
|
473
|
+
query: { city: 'London' },
|
|
474
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
475
|
+
},
|
|
476
|
+
resReq: true,
|
|
477
|
+
attempts: 5,
|
|
478
|
+
wait: 2000,
|
|
479
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
480
|
+
logAllErrors: true,
|
|
481
|
+
handleErrors: async (reqConfig, error) => {
|
|
482
|
+
logger.warn('Weather API retry', {
|
|
483
|
+
attempt: error.attempt,
|
|
484
|
+
isRetryable: error.isRetryable
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 3. Database Replication Consistency Check
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
const consistentData = await stableRequest({
|
|
494
|
+
reqData: {
|
|
495
|
+
hostname: 'replica.db.example.com',
|
|
496
|
+
path: '/records/456',
|
|
497
|
+
},
|
|
498
|
+
resReq: true,
|
|
499
|
+
attempts: 10,
|
|
500
|
+
wait: 500,
|
|
501
|
+
retryStrategy: RETRY_STRATEGIES.LINEAR,
|
|
502
|
+
responseAnalyzer: async (reqConfig, data) => {
|
|
503
|
+
// Wait until replica has the latest version
|
|
504
|
+
return data.version >= expectedVersion;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### 4. Batch User Creation
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
const users = [
|
|
513
|
+
{ name: 'John Doe', email: 'john@example.com' },
|
|
514
|
+
{ name: 'Jane Smith', email: 'jane@example.com' },
|
|
515
|
+
{ name: 'Bob Johnson', email: 'bob@example.com' }
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
const requests = users.map((user, index) => ({
|
|
519
|
+
id: `create-user-${index}`,
|
|
520
|
+
requestOptions: {
|
|
521
|
+
reqData: {
|
|
522
|
+
hostname: 'api.example.com',
|
|
523
|
+
path: '/users',
|
|
524
|
+
method: REQUEST_METHODS.POST,
|
|
525
|
+
body: user
|
|
526
|
+
},
|
|
527
|
+
resReq: true
|
|
528
|
+
}
|
|
529
|
+
}));
|
|
530
|
+
|
|
531
|
+
const results = await stableApiGateway(requests, {
|
|
532
|
+
concurrentExecution: true,
|
|
533
|
+
commonAttempts: 3,
|
|
534
|
+
commonWait: 1000,
|
|
535
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
536
|
+
commonLogAllErrors: true,
|
|
537
|
+
commonHandleErrors: async (reqConfig, error) => {
|
|
538
|
+
console.log(`Failed to create user: ${error.error}`);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const successful = results.filter(r => r.success);
|
|
543
|
+
const failed = results.filter(r => !r.success);
|
|
544
|
+
|
|
545
|
+
console.log(`Created ${successful.length} users`);
|
|
546
|
+
console.log(`Failed to create ${failed.length} users`);
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### 5. Rate-Limited API with Backoff
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
const searchResults = await stableRequest({
|
|
553
|
+
reqData: {
|
|
554
|
+
hostname: 'api.ratelimited-service.com',
|
|
555
|
+
path: '/search',
|
|
556
|
+
query: { q: 'nodejs' }
|
|
557
|
+
},
|
|
558
|
+
resReq: true,
|
|
559
|
+
attempts: 10,
|
|
560
|
+
wait: 1000,
|
|
561
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL, // Exponential backoff for rate limits
|
|
562
|
+
handleErrors: async (reqConfig, error) => {
|
|
563
|
+
if (error.type === 'HTTP_ERROR' && error.error.includes('429')) {
|
|
564
|
+
console.log('Rate limited, backing off...');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### 6. Microservices Health Check
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
const services = ['auth', 'users', 'orders', 'payments'];
|
|
574
|
+
|
|
575
|
+
const healthChecks = services.map(service => ({
|
|
576
|
+
id: `health-${service}`,
|
|
577
|
+
requestOptions: {
|
|
578
|
+
reqData: {
|
|
579
|
+
hostname: `${service}.internal.example.com`,
|
|
580
|
+
path: '/health'
|
|
581
|
+
},
|
|
582
|
+
resReq: true,
|
|
583
|
+
attempts: 3,
|
|
584
|
+
wait: 500
|
|
585
|
+
}
|
|
586
|
+
}));
|
|
587
|
+
|
|
588
|
+
const results = await stableApiGateway(healthChecks, {
|
|
589
|
+
concurrentExecution: true,
|
|
590
|
+
commonRetryStrategy: RETRY_STRATEGIES.LINEAR
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const healthStatus = results.reduce((acc, result) => {
|
|
594
|
+
const serviceName = result.id.replace('health-', '');
|
|
595
|
+
acc[serviceName] = result.success ? 'healthy' : 'unhealthy';
|
|
596
|
+
return acc;
|
|
597
|
+
}, {});
|
|
598
|
+
|
|
599
|
+
console.log('Service health status:', healthStatus);
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### 7. Payment Processing with Idempotency
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
const payment = await stableRequest({
|
|
606
|
+
reqData: {
|
|
607
|
+
hostname: 'payment-gateway.com',
|
|
608
|
+
path: '/charge',
|
|
609
|
+
method: REQUEST_METHODS.POST,
|
|
610
|
+
headers: { 'Idempotency-Key': uniqueId },
|
|
611
|
+
body: { amount: 1000, currency: 'USD' }
|
|
612
|
+
},
|
|
613
|
+
resReq: true,
|
|
614
|
+
attempts: 3,
|
|
615
|
+
wait: 1000,
|
|
616
|
+
responseAnalyzer: async (reqConfig, data) => {
|
|
617
|
+
return data.status === 'succeeded';
|
|
618
|
+
},
|
|
619
|
+
finalErrorAnalyzer: async (reqConfig, error) => {
|
|
620
|
+
// Check if payment actually went through despite error
|
|
621
|
+
const status = await checkPaymentStatus(uniqueId);
|
|
622
|
+
return status === 'succeeded';
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### 8. Bulk Data Migration
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
const records = await fetchLegacyRecords();
|
|
631
|
+
|
|
632
|
+
const migrationRequests = records.map((record, index) => ({
|
|
633
|
+
id: `migrate-${record.id}`,
|
|
634
|
+
requestOptions: {
|
|
635
|
+
reqData: {
|
|
636
|
+
hostname: 'new-system.example.com',
|
|
637
|
+
path: '/import',
|
|
638
|
+
method: REQUEST_METHODS.POST,
|
|
639
|
+
body: record
|
|
640
|
+
},
|
|
641
|
+
resReq: true,
|
|
642
|
+
// Individual retry config for critical records
|
|
643
|
+
...(record.critical && {
|
|
644
|
+
attempts: 5,
|
|
645
|
+
wait: 2000,
|
|
646
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
}));
|
|
650
|
+
|
|
651
|
+
const results = await stableApiGateway(migrationRequests, {
|
|
652
|
+
concurrentExecution: true,
|
|
653
|
+
commonAttempts: 3,
|
|
654
|
+
commonWait: 1000,
|
|
655
|
+
commonRetryStrategy: RETRY_STRATEGIES.LINEAR,
|
|
656
|
+
commonHandleErrors: async (reqConfig, error) => {
|
|
657
|
+
await logMigrationError(reqConfig.data.id, error);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### 9. Multi-Source Data Aggregation
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
const sources = [
|
|
666
|
+
{ id: 'source-1', hostname: 'api1.example.com', path: '/data' },
|
|
667
|
+
{ id: 'source-2', hostname: 'api2.example.com', path: '/info' },
|
|
668
|
+
{ id: 'source-3', hostname: 'api3.example.com', path: '/stats' }
|
|
669
|
+
];
|
|
670
|
+
|
|
671
|
+
const requests = sources.map(source => ({
|
|
672
|
+
id: source.id,
|
|
673
|
+
requestOptions: {
|
|
674
|
+
reqData: {
|
|
675
|
+
hostname: source.hostname,
|
|
676
|
+
path: source.path
|
|
677
|
+
},
|
|
678
|
+
resReq: true,
|
|
679
|
+
attempts: 3,
|
|
680
|
+
finalErrorAnalyzer: async () => true // Don't fail if one source is down
|
|
681
|
+
}
|
|
682
|
+
}));
|
|
683
|
+
|
|
684
|
+
const results = await stableApiGateway(requests, {
|
|
685
|
+
concurrentExecution: true,
|
|
686
|
+
commonWait: 1000,
|
|
687
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const aggregatedData = results
|
|
691
|
+
.filter(r => r.success)
|
|
692
|
+
.map(r => r.data);
|
|
693
|
+
|
|
694
|
+
console.log(`Collected data from ${aggregatedData.length}/${sources.length} sources`);
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### 10. Sequential Workflow with Dependencies
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
const workflowSteps = [
|
|
701
|
+
{
|
|
702
|
+
id: 'step-1-init',
|
|
703
|
+
requestOptions: {
|
|
704
|
+
reqData: {
|
|
705
|
+
hostname: 'workflow.example.com',
|
|
706
|
+
path: '/init',
|
|
707
|
+
method: REQUEST_METHODS.POST,
|
|
708
|
+
body: { workflowId: 'wf-123' }
|
|
709
|
+
},
|
|
710
|
+
resReq: true
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
id: 'step-2-process',
|
|
715
|
+
requestOptions: {
|
|
716
|
+
reqData: {
|
|
717
|
+
hostname: 'workflow.example.com',
|
|
718
|
+
path: '/process',
|
|
719
|
+
method: REQUEST_METHODS.POST,
|
|
720
|
+
body: { workflowId: 'wf-123' }
|
|
721
|
+
},
|
|
722
|
+
resReq: true,
|
|
723
|
+
responseAnalyzer: async (reqConfig, data) => {
|
|
724
|
+
return data.status === 'completed';
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
id: 'step-3-finalize',
|
|
730
|
+
requestOptions: {
|
|
731
|
+
reqData: {
|
|
732
|
+
hostname: 'workflow.example.com',
|
|
733
|
+
path: '/finalize',
|
|
734
|
+
method: REQUEST_METHODS.POST,
|
|
735
|
+
body: { workflowId: 'wf-123' }
|
|
736
|
+
},
|
|
737
|
+
resReq: true
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
];
|
|
741
|
+
|
|
742
|
+
const results = await stableApiGateway(workflowSteps, {
|
|
743
|
+
concurrentExecution: false, // Execute sequentially
|
|
744
|
+
stopOnFirstError: true, // Stop if any step fails
|
|
745
|
+
commonAttempts: 5,
|
|
746
|
+
commonWait: 2000,
|
|
747
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (results.every(r => r.success)) {
|
|
751
|
+
console.log('Workflow completed successfully');
|
|
752
|
+
} else {
|
|
753
|
+
console.error('Workflow failed at step:', results.findIndex(r => !r.success) + 1);
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
## Advanced Patterns
|
|
758
|
+
|
|
759
|
+
### Circuit Breaker Pattern
|
|
760
|
+
|
|
761
|
+
```typescript
|
|
762
|
+
let failureCount = 0;
|
|
763
|
+
const CIRCUIT_THRESHOLD = 5;
|
|
764
|
+
|
|
765
|
+
async function resilientRequest(endpoint: string) {
|
|
766
|
+
if (failureCount >= CIRCUIT_THRESHOLD) {
|
|
767
|
+
throw new Error('Circuit breaker open');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
const result = await stableRequest({
|
|
772
|
+
reqData: { hostname: 'api.example.com', path: endpoint },
|
|
773
|
+
resReq: true,
|
|
774
|
+
attempts: 3,
|
|
775
|
+
handleErrors: async () => {
|
|
776
|
+
failureCount++;
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
failureCount = 0; // Reset on success
|
|
780
|
+
return result;
|
|
781
|
+
} catch (error) {
|
|
782
|
+
if (failureCount >= CIRCUIT_THRESHOLD) {
|
|
783
|
+
console.log('Circuit breaker activated');
|
|
784
|
+
setTimeout(() => { failureCount = 0; }, 60000); // Reset after 1 minute
|
|
785
|
+
}
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
### Dynamic Request Configuration
|
|
792
|
+
|
|
793
|
+
```typescript
|
|
794
|
+
const endpoints = await getEndpointsFromConfig();
|
|
795
|
+
|
|
796
|
+
const requests = endpoints.map(endpoint => ({
|
|
797
|
+
id: endpoint.id,
|
|
798
|
+
requestOptions: {
|
|
799
|
+
reqData: {
|
|
800
|
+
hostname: endpoint.hostname,
|
|
801
|
+
path: endpoint.path,
|
|
802
|
+
method: endpoint.method,
|
|
803
|
+
...(endpoint.auth && {
|
|
804
|
+
headers: { Authorization: `Bearer ${endpoint.auth}` }
|
|
805
|
+
})
|
|
806
|
+
},
|
|
807
|
+
resReq: true,
|
|
808
|
+
attempts: endpoint.critical ? 5 : 3,
|
|
809
|
+
retryStrategy: endpoint.critical ? RETRY_STRATEGIES.EXPONENTIAL : RETRY_STRATEGIES.FIXED
|
|
810
|
+
}
|
|
811
|
+
}));
|
|
812
|
+
|
|
813
|
+
const results = await stableApiGateway(requests, {
|
|
814
|
+
concurrentExecution: true,
|
|
815
|
+
commonWait: 1000
|
|
816
|
+
});
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Conditional Retry Based on Response
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
await stableRequest({
|
|
823
|
+
reqData: {
|
|
824
|
+
hostname: 'api.example.com',
|
|
825
|
+
path: '/data',
|
|
826
|
+
},
|
|
827
|
+
resReq: true,
|
|
828
|
+
attempts: 5,
|
|
829
|
+
responseAnalyzer: async (reqConfig, data) => {
|
|
830
|
+
// Only retry if data is incomplete
|
|
831
|
+
if (!data.complete) {
|
|
832
|
+
console.log('Data incomplete, retrying...');
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Don't retry if data is invalid (different from incomplete)
|
|
837
|
+
if (data.error) {
|
|
838
|
+
throw new Error('Invalid data, cannot retry');
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
## TypeScript Support
|
|
847
|
+
|
|
848
|
+
Full TypeScript support with generic types for request and response data:
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
interface CreateUserRequest {
|
|
852
|
+
name: string;
|
|
853
|
+
email: string;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
interface UserResponse {
|
|
857
|
+
id: string;
|
|
858
|
+
name: string;
|
|
859
|
+
email: string;
|
|
860
|
+
createdAt: string;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const user = await stableRequest<CreateUserRequest, UserResponse>({
|
|
864
|
+
reqData: {
|
|
865
|
+
hostname: 'api.example.com',
|
|
866
|
+
path: '/users',
|
|
867
|
+
method: REQUEST_METHODS.POST,
|
|
868
|
+
body: {
|
|
869
|
+
name: 'John Doe',
|
|
870
|
+
email: 'john@example.com'
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
resReq: true,
|
|
874
|
+
attempts: 3
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// user is fully typed as UserResponse
|
|
878
|
+
console.log(user.id);
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
## Comparison with Similar Libraries
|
|
882
|
+
|
|
883
|
+
### vs. axios-retry
|
|
884
|
+
|
|
885
|
+
| Feature | stable-request | axios-retry |
|
|
886
|
+
|---------|----------------|-------------|
|
|
887
|
+
| **Content validation** | ✅ Full support with `responseAnalyzer` | ❌ Only HTTP status codes |
|
|
888
|
+
| **Batch processing** | ✅ Built-in `stableApiGateway` | ❌ Manual implementation needed |
|
|
889
|
+
| **Trial mode** | ✅ Built-in failure simulation | ❌ No testing utilities |
|
|
890
|
+
| **Retry strategies** | ✅ Fixed, Linear, Exponential | ✅ Exponential only |
|
|
891
|
+
| **Observability** | ✅ Granular hooks for every attempt | ⚠️ Limited |
|
|
892
|
+
| **Final error analysis** | ✅ Custom error handling | ❌ No |
|
|
893
|
+
|
|
894
|
+
### vs. got
|
|
895
|
+
|
|
896
|
+
| Feature | stable-request | got |
|
|
897
|
+
|---------|----------------|-----|
|
|
898
|
+
| **Built on Axios** | ✅ Leverages Axios ecosystem | ❌ Standalone client |
|
|
899
|
+
| **Content validation** | ✅ Response analyzer | ❌ Only HTTP errors |
|
|
900
|
+
| **Batch processing** | ✅ Built-in gateway | ❌ Manual implementation |
|
|
901
|
+
| **Trial mode** | ✅ Simulation for testing | ❌ No |
|
|
902
|
+
| **Retry strategies** | ✅ 3 configurable strategies | ✅ Exponential with jitter |
|
|
903
|
+
|
|
904
|
+
### vs. p-retry + axios
|
|
905
|
+
|
|
906
|
+
| Feature | stable-request | p-retry + axios |
|
|
907
|
+
|---------|----------------|-----------------|
|
|
908
|
+
| **All-in-one** | ✅ Single package | ❌ Requires multiple packages |
|
|
909
|
+
| **HTTP-aware** | ✅ Built for HTTP | ❌ Generic retry wrapper |
|
|
910
|
+
| **Content validation** | ✅ Built-in | ❌ Manual implementation |
|
|
911
|
+
| **Batch processing** | ✅ Built-in | ❌ Manual implementation |
|
|
912
|
+
| **Observability** | ✅ Request-specific hooks | ⚠️ Generic callbacks |
|
|
913
|
+
|
|
914
|
+
## Best Practices
|
|
915
|
+
|
|
916
|
+
1. **Use exponential backoff for rate-limited APIs** to avoid overwhelming the server
|
|
917
|
+
2. **Set reasonable timeout values** in `reqData.timeout` to prevent hanging requests
|
|
918
|
+
3. **Implement responseAnalyzer** for APIs that return 200 OK with error details in the body
|
|
919
|
+
4. **Use concurrent execution** in `stableApiGateway` for independent requests
|
|
920
|
+
5. **Use sequential execution** when requests have dependencies or need to maintain order
|
|
921
|
+
6. **Leverage finalErrorAnalyzer** for graceful degradation in non-critical paths
|
|
922
|
+
7. **Enable logging in development** with `logAllErrors` and `logAllSuccessfulAttempts`
|
|
923
|
+
8. **Use Trial Mode** to test your error handling without relying on actual failures
|
|
924
|
+
|
|
925
|
+
## License
|
|
926
|
+
|
|
927
|
+
MIT © Manish Varma
|
|
928
|
+
|
|
929
|
+
[](https://www.npmjs.com/package/@emmvish/stable-request)
|
|
930
|
+
[](https://opensource.org/licenses/MIT)
|
|
931
|
+
|
|
932
|
+
---
|
|
933
|
+
|
|
934
|
+
**Made with ❤️ for developers who are sick of integrating apps with unreliable APIs**
|