@indra211/httpease 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 +40 -0
- package/README.md +425 -0
- package/package.json +35 -0
- package/src/HttpEase.js +372 -0
- package/src/errors/HttpError.js +27 -0
- package/src/index.js +19 -0
- package/src/interceptors/InterceptorManager.js +67 -0
- package/src/types/index.d.ts +221 -0
- package/src/utils/helpers.js +86 -0
- package/src/utils/progress.js +166 -0
- package/src/utils/transformers.js +45 -0
- package/src/utils/validators.js +0 -0
package/src/HttpEase.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
const InterceptorManager = require('./interceptors/InterceptorManager');
|
|
2
|
+
const HttpError = require('./errors/HttpError');
|
|
3
|
+
const {
|
|
4
|
+
mergeConfig,
|
|
5
|
+
buildURL,
|
|
6
|
+
sleep,
|
|
7
|
+
calculateBackoff,
|
|
8
|
+
isObject
|
|
9
|
+
} = require('./utils/helpers');
|
|
10
|
+
const { transformRequest, transformResponse } = require('./utils/transformers');
|
|
11
|
+
const { wrapBodyWithProgress, readBodyWithProgress } = require('./utils/progress');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HttpEase - A powerful HTTP client built on fetch
|
|
15
|
+
* Supports TypeScript, interceptors, retry, timeout and progress tracking.
|
|
16
|
+
*/
|
|
17
|
+
class HttpEase {
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
// Default configuration
|
|
20
|
+
this.defaults = {
|
|
21
|
+
baseURL: '',
|
|
22
|
+
timeout: 30000,
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
},
|
|
26
|
+
retries: 0,
|
|
27
|
+
retryDelay: 1000,
|
|
28
|
+
retryCondition: this.defaultRetryCondition,
|
|
29
|
+
transformRequest: [...transformRequest],
|
|
30
|
+
transformResponse: [...transformResponse],
|
|
31
|
+
validateStatus: (status) => status >= 200 && status < 300,
|
|
32
|
+
maxRedirects: 5,
|
|
33
|
+
withCredentials: false,
|
|
34
|
+
...config
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Initialize interceptors
|
|
38
|
+
this.interceptors = {
|
|
39
|
+
request: new InterceptorManager(),
|
|
40
|
+
response: new InterceptorManager()
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default retry condition
|
|
46
|
+
*/
|
|
47
|
+
defaultRetryCondition(error, attempt) {
|
|
48
|
+
// Don't retry if we have a response (server responded)
|
|
49
|
+
if (error.response) {
|
|
50
|
+
const status = error.response.status;
|
|
51
|
+
// Retry on 5xx errors and 429 (rate limit)
|
|
52
|
+
return status >= 500 || status === 429 || status === 408;
|
|
53
|
+
}
|
|
54
|
+
// Retry on network errors (no response)
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Apply request transformers
|
|
60
|
+
*/
|
|
61
|
+
transformRequestData(data, headers) {
|
|
62
|
+
let result = data;
|
|
63
|
+
|
|
64
|
+
for (const transformer of this.defaults.transformRequest) {
|
|
65
|
+
result = transformer(result, headers);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Apply response transformers to already-consumed body text.
|
|
73
|
+
* Used by the download-progress path, which has already read and
|
|
74
|
+
* collected all bytes from the response stream.
|
|
75
|
+
*
|
|
76
|
+
* The built-in default transformer at index 0 expects a raw `Response`
|
|
77
|
+
* object, so we skip it here and do our own JSON parsing inline.
|
|
78
|
+
* Any user-added transformers (added after the default) are run normally
|
|
79
|
+
* because they receive already-parsed data in both code paths.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} rawText - Raw body text already read from the stream
|
|
82
|
+
* @param {string | null} contentType - Content-Type header value
|
|
83
|
+
*/
|
|
84
|
+
async transformResponseText(rawText, contentType) {
|
|
85
|
+
// Inline JSON parsing (matches what the default transformer does)
|
|
86
|
+
let parsed = rawText;
|
|
87
|
+
if (contentType && contentType.includes('application/json') && typeof rawText === 'string') {
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(rawText);
|
|
90
|
+
} catch (_) {
|
|
91
|
+
parsed = rawText;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Run user-added transformers only (skip index 0 — the built-in default
|
|
96
|
+
// which expects a Response object, not pre-parsed data).
|
|
97
|
+
let result = parsed;
|
|
98
|
+
const userTransformers = this.defaults.transformResponse.slice(1);
|
|
99
|
+
for (const transformer of userTransformers) {
|
|
100
|
+
result = await transformer(result);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Apply response transformers (original path — via Response clone)
|
|
108
|
+
*/
|
|
109
|
+
async transformResponseData(response) {
|
|
110
|
+
let result = response;
|
|
111
|
+
|
|
112
|
+
for (const transformer of this.defaults.transformResponse) {
|
|
113
|
+
result = await transformer(result);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Core request method with retry logic and optional progress tracking.
|
|
121
|
+
*/
|
|
122
|
+
async request(config = {}) {
|
|
123
|
+
// Merge configurations
|
|
124
|
+
const finalConfig = mergeConfig(this.defaults, config);
|
|
125
|
+
|
|
126
|
+
// Build full URL
|
|
127
|
+
const url = buildURL(
|
|
128
|
+
finalConfig.baseURL + finalConfig.url,
|
|
129
|
+
finalConfig.params
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const maxRetries = finalConfig.retries;
|
|
133
|
+
let lastError;
|
|
134
|
+
|
|
135
|
+
// Retry loop
|
|
136
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
137
|
+
try {
|
|
138
|
+
// Apply request interceptors
|
|
139
|
+
let requestConfig = { ...finalConfig, url };
|
|
140
|
+
requestConfig = await this.interceptors.request.forEach(requestConfig);
|
|
141
|
+
|
|
142
|
+
// Transform request data
|
|
143
|
+
const headers = { ...requestConfig.headers };
|
|
144
|
+
let body = requestConfig.data
|
|
145
|
+
? this.transformRequestData(requestConfig.data, headers)
|
|
146
|
+
: undefined;
|
|
147
|
+
|
|
148
|
+
// ── Upload Progress ──────────────────────────────────────────
|
|
149
|
+
let duplex;
|
|
150
|
+
if (body !== undefined && requestConfig.onUploadProgress) {
|
|
151
|
+
const wrapped = wrapBodyWithProgress(body, requestConfig.onUploadProgress);
|
|
152
|
+
body = wrapped.body;
|
|
153
|
+
duplex = wrapped.duplex;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Setup abort controller for timeout
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const timeoutId = setTimeout(
|
|
159
|
+
() => controller.abort(),
|
|
160
|
+
requestConfig.timeout
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Build fetch options
|
|
164
|
+
let fetchOptions = {
|
|
165
|
+
method: requestConfig.method?.toUpperCase() || 'GET',
|
|
166
|
+
headers,
|
|
167
|
+
body,
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
credentials: requestConfig.withCredentials ? 'include' : 'same-origin'
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Required by the Fetch spec when sending a ReadableStream body
|
|
173
|
+
if (duplex) {
|
|
174
|
+
fetchOptions.duplex = duplex;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Make the fetch request
|
|
178
|
+
const startTime = Date.now();
|
|
179
|
+
const response = await fetch(requestConfig.url, fetchOptions);
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
|
|
182
|
+
// ── Download Progress ────────────────────────────────────────
|
|
183
|
+
let responseData;
|
|
184
|
+
if (requestConfig.onDownloadProgress) {
|
|
185
|
+
// Stream the body through progress tracking, then parse
|
|
186
|
+
const rawText = await readBodyWithProgress(
|
|
187
|
+
response,
|
|
188
|
+
requestConfig.onDownloadProgress
|
|
189
|
+
);
|
|
190
|
+
responseData = await this.transformResponseText(
|
|
191
|
+
rawText,
|
|
192
|
+
response.headers.get('content-type')
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
// Standard path — clone & transform via default transformers
|
|
196
|
+
responseData = await this.transformResponseData(response.clone());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Build response object
|
|
200
|
+
let responseObj = {
|
|
201
|
+
data: responseData,
|
|
202
|
+
status: response.status,
|
|
203
|
+
statusText: response.statusText,
|
|
204
|
+
headers: this.parseHeaders(response.headers),
|
|
205
|
+
config: requestConfig,
|
|
206
|
+
request: {
|
|
207
|
+
url: requestConfig.url,
|
|
208
|
+
method: requestConfig.method
|
|
209
|
+
},
|
|
210
|
+
duration: Date.now() - startTime
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Check if status is valid
|
|
214
|
+
const isValid = requestConfig.validateStatus(response.status);
|
|
215
|
+
|
|
216
|
+
if (!isValid) {
|
|
217
|
+
const error = new HttpError(
|
|
218
|
+
`Request failed with status code ${response.status}`,
|
|
219
|
+
requestConfig,
|
|
220
|
+
null,
|
|
221
|
+
fetchOptions,
|
|
222
|
+
responseObj
|
|
223
|
+
);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Apply response interceptors
|
|
228
|
+
responseObj = await this.interceptors.response.forEach(responseObj);
|
|
229
|
+
|
|
230
|
+
return responseObj;
|
|
231
|
+
|
|
232
|
+
} catch (error) {
|
|
233
|
+
lastError = error;
|
|
234
|
+
|
|
235
|
+
// Handle abort/timeout
|
|
236
|
+
if (error.name === 'AbortError') {
|
|
237
|
+
const timeoutError = new HttpError(
|
|
238
|
+
`Request timeout after ${finalConfig.timeout}ms`,
|
|
239
|
+
finalConfig,
|
|
240
|
+
'ETIMEDOUT',
|
|
241
|
+
null,
|
|
242
|
+
null
|
|
243
|
+
);
|
|
244
|
+
lastError = timeoutError;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check if we should retry
|
|
248
|
+
const shouldRetry = attempt < maxRetries &&
|
|
249
|
+
finalConfig.retryCondition(lastError, attempt + 1);
|
|
250
|
+
|
|
251
|
+
if (shouldRetry) {
|
|
252
|
+
const delay = typeof finalConfig.retryDelay === 'function'
|
|
253
|
+
? finalConfig.retryDelay(attempt + 1)
|
|
254
|
+
: calculateBackoff(attempt + 1, finalConfig.retryDelay);
|
|
255
|
+
|
|
256
|
+
if (finalConfig.onRetry) {
|
|
257
|
+
finalConfig.onRetry(attempt + 1, delay, lastError);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await sleep(delay);
|
|
261
|
+
continue; // Try again
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// No more retries
|
|
265
|
+
throw lastError;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
throw lastError;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Parse response headers into a plain object
|
|
274
|
+
*/
|
|
275
|
+
parseHeaders(headers) {
|
|
276
|
+
const parsed = {};
|
|
277
|
+
headers.forEach((value, key) => {
|
|
278
|
+
parsed[key] = value;
|
|
279
|
+
});
|
|
280
|
+
return parsed;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* GET request
|
|
285
|
+
*/
|
|
286
|
+
get(url, config = {}) {
|
|
287
|
+
return this.request({
|
|
288
|
+
...config,
|
|
289
|
+
method: 'GET',
|
|
290
|
+
url
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* DELETE request
|
|
296
|
+
*/
|
|
297
|
+
delete(url, config = {}) {
|
|
298
|
+
return this.request({
|
|
299
|
+
...config,
|
|
300
|
+
method: 'DELETE',
|
|
301
|
+
url
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* HEAD request
|
|
307
|
+
*/
|
|
308
|
+
head(url, config = {}) {
|
|
309
|
+
return this.request({
|
|
310
|
+
...config,
|
|
311
|
+
method: 'HEAD',
|
|
312
|
+
url
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* OPTIONS request
|
|
318
|
+
*/
|
|
319
|
+
options(url, config = {}) {
|
|
320
|
+
return this.request({
|
|
321
|
+
...config,
|
|
322
|
+
method: 'OPTIONS',
|
|
323
|
+
url
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* POST request
|
|
329
|
+
*/
|
|
330
|
+
post(url, data, config = {}) {
|
|
331
|
+
return this.request({
|
|
332
|
+
...config,
|
|
333
|
+
method: 'POST',
|
|
334
|
+
url,
|
|
335
|
+
data
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* PUT request
|
|
341
|
+
*/
|
|
342
|
+
put(url, data, config = {}) {
|
|
343
|
+
return this.request({
|
|
344
|
+
...config,
|
|
345
|
+
method: 'PUT',
|
|
346
|
+
url,
|
|
347
|
+
data
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* PATCH request
|
|
353
|
+
*/
|
|
354
|
+
patch(url, data, config = {}) {
|
|
355
|
+
return this.request({
|
|
356
|
+
...config,
|
|
357
|
+
method: 'PATCH',
|
|
358
|
+
url,
|
|
359
|
+
data
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get URI for request (without making it)
|
|
365
|
+
*/
|
|
366
|
+
getUri(config = {}) {
|
|
367
|
+
const finalConfig = mergeConfig(this.defaults, config);
|
|
368
|
+
return buildURL(finalConfig.baseURL + finalConfig.url, finalConfig.params);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = HttpEase;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom HTTP Error class with detailed information
|
|
3
|
+
*/
|
|
4
|
+
class HttpError extends Error {
|
|
5
|
+
constructor(message, config, code, request, response) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'HttpError';
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.request = request;
|
|
11
|
+
this.response = response;
|
|
12
|
+
this.isHttpError = true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toJSON() {
|
|
16
|
+
return {
|
|
17
|
+
message: this.message,
|
|
18
|
+
name: this.name,
|
|
19
|
+
code: this.code,
|
|
20
|
+
status: this.response?.status,
|
|
21
|
+
statusText: this.response?.statusText,
|
|
22
|
+
config: this.config
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = HttpError;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const HttpEase = require('./HttpEase');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a new instance of HttpEase
|
|
5
|
+
*/
|
|
6
|
+
function create(config) {
|
|
7
|
+
return new HttpEase(config);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Create default instance
|
|
11
|
+
const httpEase = new HttpEase();
|
|
12
|
+
|
|
13
|
+
// Export
|
|
14
|
+
module.exports = httpEase;
|
|
15
|
+
module.exports.HttpEase = HttpEase;
|
|
16
|
+
module.exports.create = create;
|
|
17
|
+
|
|
18
|
+
// Expose error class
|
|
19
|
+
module.exports.HttpError = require('./errors/HttpError');
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interceptor Manager - handles request and response interceptors
|
|
3
|
+
*/
|
|
4
|
+
class InterceptorManager {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.handlers = [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Add a new interceptor
|
|
11
|
+
* @param {Function} fulfilled - Success handler
|
|
12
|
+
* @param {Function} rejected - Error handler
|
|
13
|
+
* @returns {Number} - ID for removing the interceptor
|
|
14
|
+
*/
|
|
15
|
+
use(fulfilled, rejected) {
|
|
16
|
+
this.handlers.push({
|
|
17
|
+
fulfilled,
|
|
18
|
+
rejected
|
|
19
|
+
});
|
|
20
|
+
return this.handlers.length - 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Remove an interceptor by ID
|
|
25
|
+
* @param {Number} id - Interceptor ID
|
|
26
|
+
*/
|
|
27
|
+
eject(id) {
|
|
28
|
+
if (this.handlers[id]) {
|
|
29
|
+
this.handlers[id] = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clear all interceptors
|
|
35
|
+
*/
|
|
36
|
+
clear() {
|
|
37
|
+
this.handlers = [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Execute all interceptors
|
|
42
|
+
* @param {*} data - Data to pass through interceptors
|
|
43
|
+
*/
|
|
44
|
+
async forEach(data) {
|
|
45
|
+
let result = data;
|
|
46
|
+
|
|
47
|
+
for (const handler of this.handlers) {
|
|
48
|
+
if (handler === null) continue;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (handler.fulfilled) {
|
|
52
|
+
result = await handler.fulfilled(result);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (handler.rejected) {
|
|
56
|
+
result = await handler.rejected(error);
|
|
57
|
+
} else {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = InterceptorManager;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// HttpEase — TypeScript Type Definitions
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export interface ProgressEvent {
|
|
6
|
+
/** Bytes transferred so far */
|
|
7
|
+
loaded: number;
|
|
8
|
+
/** Total bytes (0 if unknown, e.g. no Content-Length header) */
|
|
9
|
+
total: number;
|
|
10
|
+
/** Progress percentage 0–100 (0 if total is unknown) */
|
|
11
|
+
percentage: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TransformRequestFn = (data: unknown, headers: Record<string, string>) => unknown;
|
|
15
|
+
export type TransformResponseFn = (data: unknown) => unknown | Promise<unknown>;
|
|
16
|
+
|
|
17
|
+
export interface HttpEaseConfig {
|
|
18
|
+
/** Base URL prepended to every request URL */
|
|
19
|
+
baseURL?: string;
|
|
20
|
+
|
|
21
|
+
/** Request URL (relative or absolute) */
|
|
22
|
+
url?: string;
|
|
23
|
+
|
|
24
|
+
/** HTTP method */
|
|
25
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | string;
|
|
26
|
+
|
|
27
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
28
|
+
timeout?: number;
|
|
29
|
+
|
|
30
|
+
/** Custom request headers */
|
|
31
|
+
headers?: Record<string, string>;
|
|
32
|
+
|
|
33
|
+
/** Query parameters appended to the URL */
|
|
34
|
+
params?: Record<string, string | number | boolean | string[] | number[] | null | undefined>;
|
|
35
|
+
|
|
36
|
+
/** Request body data */
|
|
37
|
+
data?: unknown;
|
|
38
|
+
|
|
39
|
+
/** Number of retry attempts on failure (default: 0) */
|
|
40
|
+
retries?: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Delay between retries in ms, or a function returning the delay.
|
|
44
|
+
* @example 1000
|
|
45
|
+
* @example (attempt) => Math.pow(2, attempt - 1) * 1000
|
|
46
|
+
*/
|
|
47
|
+
retryDelay?: number | ((attempt: number) => number);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Custom condition to decide whether to retry.
|
|
51
|
+
* Return `true` to retry, `false` to stop.
|
|
52
|
+
*/
|
|
53
|
+
retryCondition?: (error: HttpEaseError, attempt: number) => boolean;
|
|
54
|
+
|
|
55
|
+
/** Callback fired before each retry */
|
|
56
|
+
onRetry?: (attempt: number, delay: number, error: HttpEaseError) => void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate whether a given HTTP status code should resolve or reject.
|
|
60
|
+
* Default: `status >= 200 && status < 300`
|
|
61
|
+
*/
|
|
62
|
+
validateStatus?: (status: number) => boolean;
|
|
63
|
+
|
|
64
|
+
/** Send cookies with cross-origin requests (default: false) */
|
|
65
|
+
withCredentials?: boolean;
|
|
66
|
+
|
|
67
|
+
/** Transform request data before sending */
|
|
68
|
+
transformRequest?: TransformRequestFn[];
|
|
69
|
+
|
|
70
|
+
/** Transform response data after receiving */
|
|
71
|
+
transformResponse?: TransformResponseFn[];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Callback fired during upload progress.
|
|
75
|
+
* Works with string/Buffer/Uint8Array bodies in Node.js 18+ and modern browsers.
|
|
76
|
+
*/
|
|
77
|
+
onUploadProgress?: (event: ProgressEvent) => void;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Callback fired during download progress.
|
|
81
|
+
* Requires the server to send a `content-length` header for percentage to work.
|
|
82
|
+
*/
|
|
83
|
+
onDownloadProgress?: (event: ProgressEvent) => void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface HttpEaseResponse<T = unknown> {
|
|
87
|
+
/** Parsed response data */
|
|
88
|
+
data: T;
|
|
89
|
+
/** HTTP status code */
|
|
90
|
+
status: number;
|
|
91
|
+
/** HTTP status text */
|
|
92
|
+
statusText: string;
|
|
93
|
+
/** Response headers as a plain object */
|
|
94
|
+
headers: Record<string, string>;
|
|
95
|
+
/** The config used for this request */
|
|
96
|
+
config: HttpEaseConfig;
|
|
97
|
+
/** Request info (url, method) */
|
|
98
|
+
request: { url: string; method: string };
|
|
99
|
+
/** Round-trip duration in milliseconds */
|
|
100
|
+
duration: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export declare class HttpEaseError extends Error {
|
|
104
|
+
name: 'HttpError';
|
|
105
|
+
/** Error code, e.g. `'ETIMEDOUT'` */
|
|
106
|
+
code: string | null;
|
|
107
|
+
/** Config used for this request */
|
|
108
|
+
config: HttpEaseConfig;
|
|
109
|
+
/** The fetch options used */
|
|
110
|
+
request: RequestInit | null;
|
|
111
|
+
/** The parsed response object (if server responded) */
|
|
112
|
+
response: HttpEaseResponse | null;
|
|
113
|
+
/** Always true — use to narrow error types */
|
|
114
|
+
isHttpError: true;
|
|
115
|
+
|
|
116
|
+
toJSON(): {
|
|
117
|
+
message: string;
|
|
118
|
+
name: string;
|
|
119
|
+
code: string | null;
|
|
120
|
+
status: number | undefined;
|
|
121
|
+
statusText: string | undefined;
|
|
122
|
+
config: HttpEaseConfig;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface InterceptorManager<T> {
|
|
127
|
+
/**
|
|
128
|
+
* Register an interceptor.
|
|
129
|
+
* @returns Interceptor ID (use with `eject`)
|
|
130
|
+
*/
|
|
131
|
+
use(
|
|
132
|
+
onFulfilled?: (value: T) => T | Promise<T>,
|
|
133
|
+
onRejected?: (error: HttpEaseError) => T | Promise<T>
|
|
134
|
+
): number;
|
|
135
|
+
|
|
136
|
+
/** Remove an interceptor by ID */
|
|
137
|
+
eject(id: number): void;
|
|
138
|
+
|
|
139
|
+
/** Remove all interceptors */
|
|
140
|
+
clear(): void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface HttpEaseInstance {
|
|
144
|
+
/** Default configuration for this instance */
|
|
145
|
+
defaults: HttpEaseConfig;
|
|
146
|
+
|
|
147
|
+
/** Request and response interceptors */
|
|
148
|
+
interceptors: {
|
|
149
|
+
request: InterceptorManager<HttpEaseConfig>;
|
|
150
|
+
response: InterceptorManager<HttpEaseResponse>;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Core request method */
|
|
154
|
+
request<T = unknown>(config: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
155
|
+
|
|
156
|
+
/** GET request */
|
|
157
|
+
get<T = unknown>(url: string, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
158
|
+
|
|
159
|
+
/** DELETE request */
|
|
160
|
+
delete<T = unknown>(url: string, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
161
|
+
|
|
162
|
+
/** HEAD request */
|
|
163
|
+
head<T = unknown>(url: string, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
164
|
+
|
|
165
|
+
/** OPTIONS request */
|
|
166
|
+
options<T = unknown>(url: string, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
167
|
+
|
|
168
|
+
/** POST request */
|
|
169
|
+
post<T = unknown>(url: string, data?: unknown, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
170
|
+
|
|
171
|
+
/** PUT request */
|
|
172
|
+
put<T = unknown>(url: string, data?: unknown, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
173
|
+
|
|
174
|
+
/** PATCH request */
|
|
175
|
+
patch<T = unknown>(url: string, data?: unknown, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
176
|
+
|
|
177
|
+
/** Resolve the full URI for a config without making a request */
|
|
178
|
+
getUri(config?: HttpEaseConfig): string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create a new HttpEase instance with custom defaults.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* import { create } from 'httpease';
|
|
187
|
+
*
|
|
188
|
+
* const api = create({
|
|
189
|
+
* baseURL: 'https://api.example.com',
|
|
190
|
+
* timeout: 10000,
|
|
191
|
+
* headers: { Authorization: 'Bearer TOKEN' },
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* const res = await api.get<User[]>('/users');
|
|
195
|
+
* console.log(res.data);
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export declare function create(config?: HttpEaseConfig): HttpEaseInstance;
|
|
199
|
+
|
|
200
|
+
/** The `HttpEase` class for extending */
|
|
201
|
+
export declare class HttpEase implements HttpEaseInstance {
|
|
202
|
+
constructor(config?: HttpEaseConfig);
|
|
203
|
+
defaults: HttpEaseConfig;
|
|
204
|
+
interceptors: {
|
|
205
|
+
request: InterceptorManager<HttpEaseConfig>;
|
|
206
|
+
response: InterceptorManager<HttpEaseResponse>;
|
|
207
|
+
};
|
|
208
|
+
request<T = unknown>(config: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
209
|
+
get<T = unknown>(url: string, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
210
|
+
delete<T = unknown>(url: string, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
211
|
+
head<T = unknown>(url: string, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
212
|
+
options<T = unknown>(url: string, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
213
|
+
post<T = unknown>(url: string, data?: unknown, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
214
|
+
put<T = unknown>(url: string, data?: unknown, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
215
|
+
patch<T = unknown>(url: string, data?: unknown, config?: HttpEaseConfig): Promise<HttpEaseResponse<T>>;
|
|
216
|
+
getUri(config?: HttpEaseConfig): string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The default HttpEase instance */
|
|
220
|
+
declare const httpEase: HttpEaseInstance;
|
|
221
|
+
export default httpEase;
|