hyperttp 0.1.5 → 0.1.7
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/dist/Hyperttp/Core/HttpClientImproved.d.ts +437 -109
- package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
- package/dist/Hyperttp/Core/HttpClientImproved.js +631 -215
- package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
- package/dist/Hyperttp/Core/RateLimiter.d.ts +21 -24
- package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
- package/dist/Hyperttp/Core/RateLimiter.js +48 -33
- package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
- package/dist/Types/request.d.ts +15 -15
- package/dist/Types/request.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -1,68 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.RateLimitError = exports.TimeoutError = exports.HttpClientError = void 0;
|
|
37
4
|
const tough_cookie_1 = require("tough-cookie");
|
|
38
5
|
const undici_1 = require("undici");
|
|
39
6
|
const undici_2 = require("http-cookie-agent/undici");
|
|
40
|
-
const zlib = __importStar(require("zlib"));
|
|
41
|
-
const util_1 = require("util");
|
|
42
7
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
43
|
-
const
|
|
8
|
+
const url_1 = require("url");
|
|
9
|
+
const crypto_1 = require("crypto");
|
|
44
10
|
const CacheManager_1 = require("./CacheManager");
|
|
45
11
|
const QueueManager_1 = require("./QueueManager");
|
|
46
12
|
const RateLimiter_1 = require("./RateLimiter");
|
|
47
|
-
|
|
48
|
-
const inflate = (0, util_1.promisify)(zlib.inflate);
|
|
49
|
-
const brotliDecompress = (0, util_1.promisify)(zlib.brotliDecompress);
|
|
13
|
+
let defaultClient = null;
|
|
50
14
|
/**
|
|
51
|
-
*
|
|
15
|
+
* Base error class for HTTP client operations.
|
|
16
|
+
* Contains additional context about the failed request including status code, URL, and method.
|
|
52
17
|
*/
|
|
53
18
|
class HttpClientError extends Error {
|
|
54
19
|
statusCode;
|
|
55
20
|
originalError;
|
|
56
21
|
url;
|
|
57
22
|
method;
|
|
58
|
-
/**
|
|
59
|
-
* Creates a new HttpClientError instance.
|
|
60
|
-
* @param message The error message
|
|
61
|
-
* @param statusCode Optional HTTP status code
|
|
62
|
-
* @param originalError Optional original error that caused this error
|
|
63
|
-
* @param url Optional request URL
|
|
64
|
-
* @param method Optional HTTP method
|
|
65
|
-
*/
|
|
66
23
|
constructor(message, statusCode, originalError, url, method) {
|
|
67
24
|
super(message);
|
|
68
25
|
this.statusCode = statusCode;
|
|
@@ -74,12 +31,11 @@ class HttpClientError extends Error {
|
|
|
74
31
|
}
|
|
75
32
|
}
|
|
76
33
|
exports.HttpClientError = HttpClientError;
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when an HTTP request exceeds the configured timeout duration.
|
|
36
|
+
* Contains information about the URL and timeout value that caused the failure.
|
|
37
|
+
*/
|
|
77
38
|
class TimeoutError extends HttpClientError {
|
|
78
|
-
/**
|
|
79
|
-
* Creates a new TimeoutError instance.
|
|
80
|
-
* @param url The request URL that timed out
|
|
81
|
-
* @param timeout The timeout duration in milliseconds
|
|
82
|
-
*/
|
|
83
39
|
constructor(url, timeout) {
|
|
84
40
|
super(`Request timeout after ${timeout}ms for ${url}`);
|
|
85
41
|
this.name = "TimeoutError";
|
|
@@ -87,13 +43,12 @@ class TimeoutError extends HttpClientError {
|
|
|
87
43
|
}
|
|
88
44
|
}
|
|
89
45
|
exports.TimeoutError = TimeoutError;
|
|
46
|
+
/**
|
|
47
|
+
* Error thrown when an HTTP request is rate limited by the server.
|
|
48
|
+
* Contains information about the URL and optional retry-after duration.
|
|
49
|
+
*/
|
|
90
50
|
class RateLimitError extends HttpClientError {
|
|
91
51
|
retryAfter;
|
|
92
|
-
/**
|
|
93
|
-
* Creates a new RateLimitError instance.
|
|
94
|
-
* @param url The request URL that was rate limited
|
|
95
|
-
* @param retryAfter Optional retry after duration in milliseconds
|
|
96
|
-
*/
|
|
97
52
|
constructor(url, retryAfter) {
|
|
98
53
|
super(`Rate limited for ${url}${retryAfter ? `, retry after ${retryAfter}ms` : ""}`);
|
|
99
54
|
this.retryAfter = retryAfter;
|
|
@@ -103,8 +58,58 @@ class RateLimitError extends HttpClientError {
|
|
|
103
58
|
}
|
|
104
59
|
exports.RateLimitError = RateLimitError;
|
|
105
60
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
61
|
+
* @ru
|
|
62
|
+
* Улучшенный HTTP-клиент с кэшированием, ограничением скорости, логикой повторных попыток и расширенными функциями.
|
|
63
|
+
* Предоставляет надежный интерфейс для выполнения HTTP-запросов с автоматической обработкой
|
|
64
|
+
* распространенных паттернов, таких как повторные попытки, кэширование и перехват запросов/ответов.
|
|
65
|
+
* @en
|
|
66
|
+
* Enhanced HTTP client with caching, rate limiting, retry logic, and advanced features.
|
|
67
|
+
* Provides a robust interface for making HTTP requests with automatic handling of
|
|
68
|
+
* common patterns like retries, caching, and request/response interception.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const client = new HttpClientImproved({
|
|
73
|
+
* timeout: 10000,
|
|
74
|
+
* maxRetries: 3,
|
|
75
|
+
* cacheTTL: 300000,
|
|
76
|
+
* rateLimit: { maxRequests: 100, windowMs: 60000 }
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* const response = await client.get('https://api.example.com/data');
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* // Using the fluent request builder
|
|
85
|
+
* const client = new HttpClientImproved();
|
|
86
|
+
* const response = await client.request('https://api.example.com/data')
|
|
87
|
+
* .headers({ 'Authorization': 'Bearer token' })
|
|
88
|
+
* .json()
|
|
89
|
+
* .send();
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* // Using RequestInterface for complex requests
|
|
95
|
+
* import { RequestInterface } from './src';
|
|
96
|
+
*
|
|
97
|
+
* class ApiRequest implements RequestInterface {
|
|
98
|
+
* constructor(
|
|
99
|
+
* private url: string,
|
|
100
|
+
* private headers: Record<string, string> = {},
|
|
101
|
+
* private body?: any
|
|
102
|
+
* ) {}
|
|
103
|
+
*
|
|
104
|
+
* getURL(): string { return this.url; }
|
|
105
|
+
* getHeaders(): Record<string, string> { return this.headers; }
|
|
106
|
+
* getBodyData(): any { return this.body; }
|
|
107
|
+
* }
|
|
108
|
+
*
|
|
109
|
+
* const client = new HttpClientImproved();
|
|
110
|
+
* const request = new ApiRequest('https://api.example.com/data');
|
|
111
|
+
* const response = await client.get(request);
|
|
112
|
+
* ```
|
|
108
113
|
*/
|
|
109
114
|
class HttpClientImproved {
|
|
110
115
|
cookieJar = new tough_cookie_1.CookieJar();
|
|
@@ -119,20 +124,43 @@ class HttpClientImproved {
|
|
|
119
124
|
requestInterceptors = [];
|
|
120
125
|
responseInterceptors = [];
|
|
121
126
|
requestMetrics = new Map();
|
|
122
|
-
/**
|
|
123
|
-
* Creates a new instance of HttpClientImproved.
|
|
124
|
-
* @param options Optional configuration options for the HTTP client
|
|
125
|
-
*/
|
|
126
127
|
constructor(options) {
|
|
127
|
-
this.options = {
|
|
128
|
+
this.options = {
|
|
129
|
+
timeout: options?.timeout ?? 15000,
|
|
130
|
+
maxConcurrent: options?.maxConcurrent ?? 50,
|
|
131
|
+
maxRetries: options?.maxRetries ?? 3,
|
|
132
|
+
cacheTTL: options?.cacheTTL ?? 300000,
|
|
133
|
+
cacheMaxSize: options?.cacheMaxSize ?? 500,
|
|
134
|
+
followRedirects: options?.followRedirects ?? true,
|
|
135
|
+
maxRedirects: options?.maxRedirects ?? 5,
|
|
136
|
+
validateStatus: options?.validateStatus ??
|
|
137
|
+
((status) => status >= 200 && status < 300),
|
|
138
|
+
cacheMethods: options?.cacheMethods ?? ["GET", "HEAD"],
|
|
139
|
+
maxMetricsSize: options?.maxMetricsSize ?? 10000,
|
|
140
|
+
rateLimit: options?.rateLimit ?? { maxRequests: 100, windowMs: 60000 },
|
|
141
|
+
userAgent: options?.userAgent ?? "Hyperttp/0.1.0 Node.js",
|
|
142
|
+
logger: options?.logger ??
|
|
143
|
+
((level, message, meta) => {
|
|
144
|
+
const methods = {
|
|
145
|
+
debug: console.debug,
|
|
146
|
+
info: console.info,
|
|
147
|
+
warn: console.warn,
|
|
148
|
+
error: console.error,
|
|
149
|
+
};
|
|
150
|
+
(methods[level] || console.log)(`[${level.toUpperCase()}] ${message}`, meta || "");
|
|
151
|
+
}),
|
|
152
|
+
retryOptions: options?.retryOptions ?? {},
|
|
153
|
+
maxResponseBytes: options?.maxResponseBytes ?? 1024 * 1024,
|
|
154
|
+
verbose: false,
|
|
155
|
+
};
|
|
128
156
|
this.cache = new CacheManager_1.CacheManager({
|
|
129
157
|
cacheTTL: this.options.cacheTTL,
|
|
130
158
|
cacheMaxSize: this.options.cacheMaxSize,
|
|
131
159
|
});
|
|
132
|
-
this.queue = new QueueManager_1.QueueManager(this.options.maxConcurrent ??
|
|
160
|
+
this.queue = new QueueManager_1.QueueManager(this.options.maxConcurrent ?? 100);
|
|
133
161
|
this.limiter = new RateLimiter_1.RateLimiter(this.options.rateLimit);
|
|
134
162
|
this.retryOptions = {
|
|
135
|
-
maxRetries: this.options.maxRetries
|
|
163
|
+
maxRetries: this.options.maxRetries,
|
|
136
164
|
baseDelay: this.options.retryOptions?.baseDelay ?? 1000,
|
|
137
165
|
maxDelay: this.options.retryOptions?.maxDelay ?? 30000,
|
|
138
166
|
retryStatusCodes: this.options.retryOptions?.retryStatusCodes ?? [
|
|
@@ -142,94 +170,107 @@ class HttpClientImproved {
|
|
|
142
170
|
};
|
|
143
171
|
this.defaultHeaders = {
|
|
144
172
|
Accept: "application/json, text/plain, */*",
|
|
145
|
-
"Accept-Encoding": "gzip, deflate, br",
|
|
146
173
|
"User-Agent": this.options.userAgent ?? "Hyperttp/0.1.0 Node.js",
|
|
147
174
|
};
|
|
148
175
|
this.agent = new undici_1.Agent({
|
|
149
|
-
connections:
|
|
150
|
-
pipelining:
|
|
176
|
+
connections: 256,
|
|
177
|
+
pipelining: 1,
|
|
151
178
|
interceptors: {
|
|
152
179
|
Client: [(0, undici_2.cookie)({ jar: this.cookieJar })],
|
|
153
180
|
},
|
|
154
181
|
});
|
|
155
182
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return this.cookieJar;
|
|
183
|
+
log(level, msg, meta) {
|
|
184
|
+
if (!this.options.logger)
|
|
185
|
+
return;
|
|
186
|
+
const minLevel = process.env.NODE_ENV === "production" ? "warn" : "info";
|
|
187
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
188
|
+
const currentLevelIndex = levels.indexOf(level);
|
|
189
|
+
const minLevelIndex = levels.indexOf(minLevel);
|
|
190
|
+
if (currentLevelIndex < minLevelIndex)
|
|
191
|
+
return;
|
|
192
|
+
if (this.options.verbose || level !== "info") {
|
|
193
|
+
this.options.logger(level, msg, meta);
|
|
194
|
+
}
|
|
169
195
|
}
|
|
170
196
|
/**
|
|
171
|
-
*
|
|
172
|
-
* @param
|
|
197
|
+
* Creates a hash of the request body for cache key generation.
|
|
198
|
+
* @param body - Request body (string or Buffer)
|
|
199
|
+
* @returns SHA1 hash of the body, truncated to 8 characters
|
|
173
200
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
hashBody(body) {
|
|
202
|
+
if (!body)
|
|
203
|
+
return "";
|
|
204
|
+
if (typeof body === "string")
|
|
205
|
+
body = Buffer.from(body, "utf-8");
|
|
206
|
+
return (0, crypto_1.createHash)("sha1").update(body).digest("hex").slice(0, 8);
|
|
176
207
|
}
|
|
177
208
|
/**
|
|
178
|
-
*
|
|
179
|
-
* @param
|
|
209
|
+
* Calculates the delay for retry attempts using exponential backoff.
|
|
210
|
+
* @param attempt - Current retry attempt number (0-based)
|
|
211
|
+
* @returns Delay in milliseconds
|
|
180
212
|
*/
|
|
181
|
-
addResponseInterceptor(interceptor) {
|
|
182
|
-
this.responseInterceptors.push(interceptor);
|
|
183
|
-
}
|
|
184
|
-
/** Closes the HTTP agent to properly terminate keep-alive connections. */
|
|
185
|
-
close() {
|
|
186
|
-
this.agent.close();
|
|
187
|
-
}
|
|
188
|
-
log(level, msg, meta) {
|
|
189
|
-
if (this.options.logger)
|
|
190
|
-
this.options.logger(level, msg, meta);
|
|
191
|
-
}
|
|
192
|
-
async decompress(buf, enc, charset = "utf-8") {
|
|
193
|
-
if (!enc)
|
|
194
|
-
return buf.toString(charset);
|
|
195
|
-
try {
|
|
196
|
-
switch (enc.toLowerCase()) {
|
|
197
|
-
case "gzip":
|
|
198
|
-
return (await gunzip(buf)).toString(charset);
|
|
199
|
-
case "deflate":
|
|
200
|
-
return (await inflate(buf)).toString(charset);
|
|
201
|
-
case "br":
|
|
202
|
-
return (await brotliDecompress(buf)).toString(charset);
|
|
203
|
-
default:
|
|
204
|
-
return buf.toString(charset);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
this.log("error", `Decompression failed for encoding ${enc}`, error);
|
|
209
|
-
return buf.toString(charset);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
213
|
calcDelay(attempt) {
|
|
213
214
|
const base = Math.min(this.retryOptions.baseDelay * 2 ** attempt, this.retryOptions.maxDelay);
|
|
214
215
|
return this.retryOptions.jitter
|
|
215
216
|
? base * (0.75 + Math.random() * 0.5)
|
|
216
217
|
: base;
|
|
217
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Creates a promise that resolves after the specified delay.
|
|
221
|
+
* @param ms - Delay in milliseconds
|
|
222
|
+
* @returns Promise that resolves after the delay
|
|
223
|
+
*/
|
|
218
224
|
sleep(ms) {
|
|
219
225
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
220
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Applies all registered request interceptors to modify the request configuration.
|
|
229
|
+
* Interceptors are executed in sequence, with each one receiving the output of the previous.
|
|
230
|
+
* @param config - Original request configuration
|
|
231
|
+
* @returns Modified request configuration
|
|
232
|
+
*/
|
|
221
233
|
async applyRequestInterceptors(config) {
|
|
234
|
+
if (!this.requestInterceptors.length)
|
|
235
|
+
return config;
|
|
222
236
|
let result = config;
|
|
223
|
-
for (const interceptor of this.requestInterceptors)
|
|
224
|
-
|
|
237
|
+
for (const interceptor of this.requestInterceptors) {
|
|
238
|
+
try {
|
|
239
|
+
result = await interceptor(result);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
this.log("error", "Request interceptor failed", { error });
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
225
246
|
return result;
|
|
226
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Applies all registered response interceptors to modify the response data.
|
|
250
|
+
* Interceptors are executed in sequence, with each one receiving the output of the previous.
|
|
251
|
+
* @param response - Original response data
|
|
252
|
+
* @returns Modified response data
|
|
253
|
+
*/
|
|
227
254
|
async applyResponseInterceptors(response) {
|
|
228
255
|
let result = response;
|
|
229
|
-
for (const interceptor of this.responseInterceptors)
|
|
230
|
-
|
|
256
|
+
for (const interceptor of this.responseInterceptors) {
|
|
257
|
+
try {
|
|
258
|
+
result = await interceptor(result);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
this.log("error", "Response interceptor failed", { error });
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
231
265
|
return result;
|
|
232
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolves a redirect location relative to the base URL.
|
|
269
|
+
* Handles both absolute and relative redirect URLs.
|
|
270
|
+
* @param location - The redirect location from the response
|
|
271
|
+
* @param baseUrl - The original request URL
|
|
272
|
+
* @returns The resolved absolute URL
|
|
273
|
+
*/
|
|
233
274
|
resolveRedirect(location, baseUrl) {
|
|
234
275
|
try {
|
|
235
276
|
return new URL(location, baseUrl).toString();
|
|
@@ -238,6 +279,12 @@ class HttpClientImproved {
|
|
|
238
279
|
return location;
|
|
239
280
|
}
|
|
240
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Parses the Retry-After header to determine when to retry a request.
|
|
284
|
+
* Supports both seconds and HTTP date formats.
|
|
285
|
+
* @param retryAfterHeader - The Retry-After header value
|
|
286
|
+
* @returns Delay in milliseconds, or undefined if not parseable
|
|
287
|
+
*/
|
|
241
288
|
parseRetryAfterMs(retryAfterHeader) {
|
|
242
289
|
if (!retryAfterHeader)
|
|
243
290
|
return undefined;
|
|
@@ -252,14 +299,51 @@ class HttpClientImproved {
|
|
|
252
299
|
return Math.max(0, asDate - Date.now());
|
|
253
300
|
return undefined;
|
|
254
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Reads response body with size limit enforcement.
|
|
304
|
+
* Collects chunks until the response is complete or the limit is exceeded.
|
|
305
|
+
* @param body - Async iterable of response chunks
|
|
306
|
+
* @returns Complete response body as a Buffer
|
|
307
|
+
*/
|
|
255
308
|
async readBodyWithLimit(body) {
|
|
256
|
-
const
|
|
309
|
+
const chunks = [];
|
|
257
310
|
const limit = this.options.maxResponseBytes;
|
|
258
|
-
|
|
259
|
-
|
|
311
|
+
let total = 0;
|
|
312
|
+
for await (const chunk of body) {
|
|
313
|
+
const buf = Buffer.from(chunk);
|
|
314
|
+
total += buf.length;
|
|
315
|
+
if (limit && total > limit) {
|
|
316
|
+
throw new HttpClientError(`Response too large (${total} bytes), limit is ${limit}`, 0);
|
|
317
|
+
}
|
|
318
|
+
chunks.push(buf);
|
|
260
319
|
}
|
|
261
|
-
return
|
|
320
|
+
return Buffer.concat(chunks, total);
|
|
262
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Removes old metrics entries to prevent memory leaks.
|
|
324
|
+
* Keeps only metrics from the last 24 hours.
|
|
325
|
+
*/
|
|
326
|
+
trimMetrics() {
|
|
327
|
+
if (this.requestMetrics.size > this.options.maxMetricsSize) {
|
|
328
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
329
|
+
for (const [key, metrics] of this.requestMetrics) {
|
|
330
|
+
if (metrics.endTime < cutoff) {
|
|
331
|
+
this.requestMetrics.delete(key);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Sends an HTTP request with retry logic and rate limiting.
|
|
338
|
+
* Handles timeouts, redirects, and various retry scenarios.
|
|
339
|
+
* @param method - HTTP method (GET, POST, etc.)
|
|
340
|
+
* @param url - Target URL
|
|
341
|
+
* @param headers - HTTP headers
|
|
342
|
+
* @param body - Request body (optional)
|
|
343
|
+
* @param metrics - Optional metrics object to track request details
|
|
344
|
+
* @param redirects - Number of redirects followed so far
|
|
345
|
+
* @returns Promise resolving to the response data
|
|
346
|
+
*/
|
|
263
347
|
async sendWithRetry(method, url, headers, body, metrics, redirects = 0) {
|
|
264
348
|
let lastError;
|
|
265
349
|
for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
|
|
@@ -272,7 +356,7 @@ class HttpClientImproved {
|
|
|
272
356
|
body,
|
|
273
357
|
});
|
|
274
358
|
const controller = new AbortController();
|
|
275
|
-
const timeout = this.options.timeout
|
|
359
|
+
const timeout = this.options.timeout;
|
|
276
360
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
277
361
|
try {
|
|
278
362
|
const res = await (0, undici_1.request)(finalConfig.url, {
|
|
@@ -290,7 +374,9 @@ class HttpClientImproved {
|
|
|
290
374
|
body: buf,
|
|
291
375
|
url: finalConfig.url,
|
|
292
376
|
});
|
|
293
|
-
|
|
377
|
+
if (!this.options.validateStatus(response.status)) {
|
|
378
|
+
throw new HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
|
|
379
|
+
}
|
|
294
380
|
if (this.options.followRedirects &&
|
|
295
381
|
[301, 302, 303, 307, 308].includes(response.status) &&
|
|
296
382
|
redirects < (this.options.maxRedirects ?? 5)) {
|
|
@@ -300,7 +386,6 @@ class HttpClientImproved {
|
|
|
300
386
|
const redirectMethod = response.status === 303 ? "GET" : method;
|
|
301
387
|
const nextHeaders = { ...headers };
|
|
302
388
|
let nextBody = body;
|
|
303
|
-
// If switching to GET, drop body-related headers.
|
|
304
389
|
if (redirectMethod === "GET") {
|
|
305
390
|
nextBody = undefined;
|
|
306
391
|
delete nextHeaders["content-type"];
|
|
@@ -315,7 +400,6 @@ class HttpClientImproved {
|
|
|
315
400
|
return this.sendWithRetry(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, redirects + 1);
|
|
316
401
|
}
|
|
317
402
|
}
|
|
318
|
-
// Retry by status
|
|
319
403
|
if (this.retryOptions.retryStatusCodes.includes(response.status)) {
|
|
320
404
|
metrics && (metrics.retries += 1);
|
|
321
405
|
if (response.status === 429) {
|
|
@@ -346,6 +430,9 @@ class HttpClientImproved {
|
|
|
346
430
|
throw new TimeoutError(url, timeout);
|
|
347
431
|
throw timeoutErr;
|
|
348
432
|
}
|
|
433
|
+
finally {
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
}
|
|
349
436
|
}
|
|
350
437
|
catch (err) {
|
|
351
438
|
lastError = err;
|
|
@@ -364,6 +451,11 @@ class HttpClientImproved {
|
|
|
364
451
|
throw lastError;
|
|
365
452
|
throw new HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
|
|
366
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Parses the Content-Type header to extract MIME type and character encoding.
|
|
456
|
+
* @param contentType - Content-Type header value
|
|
457
|
+
* @returns Object containing type and charset information
|
|
458
|
+
*/
|
|
367
459
|
parseContentType(contentType) {
|
|
368
460
|
if (!contentType)
|
|
369
461
|
return { type: "text/plain", charset: "utf-8" };
|
|
@@ -387,28 +479,48 @@ class HttpClientImproved {
|
|
|
387
479
|
"base64",
|
|
388
480
|
"hex",
|
|
389
481
|
];
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
:
|
|
393
|
-
|
|
482
|
+
return {
|
|
483
|
+
type,
|
|
484
|
+
charset: allowed.includes(normalized)
|
|
485
|
+
? normalized
|
|
486
|
+
: "utf-8",
|
|
487
|
+
};
|
|
394
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Parses the HTTP response body based on content type and requested response type.
|
|
491
|
+
* Handles JSON, XML, text, and buffer responses with fallback parsing.
|
|
492
|
+
* @param res - HTTP response object
|
|
493
|
+
* @param responseType - Desired response type
|
|
494
|
+
* @returns Parsed response data
|
|
495
|
+
*/
|
|
395
496
|
async parseResponse(res, responseType) {
|
|
497
|
+
const { type, charset } = this.parseContentType(res.headers["content-type"]);
|
|
498
|
+
const text = res.body.toString(charset);
|
|
499
|
+
const finalType = responseType ?? (type.includes("application/json") ? "json" : "text");
|
|
396
500
|
try {
|
|
397
|
-
const { type, charset } = this.parseContentType(res.headers["content-type"]);
|
|
398
|
-
const text = await this.decompress(res.body, res.headers["content-encoding"], charset);
|
|
399
|
-
const finalType = responseType ?? "json";
|
|
400
501
|
switch (finalType) {
|
|
401
|
-
case "json":
|
|
402
|
-
if (type.includes("json"))
|
|
403
|
-
return JSON.parse(text);
|
|
502
|
+
case "json": {
|
|
404
503
|
try {
|
|
405
504
|
return JSON.parse(text);
|
|
406
505
|
}
|
|
506
|
+
catch {
|
|
507
|
+
try {
|
|
508
|
+
return new fast_xml_parser_1.XMLParser({ ignoreAttributes: false }).parse(text);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return { data: text };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
case "xml": {
|
|
516
|
+
try {
|
|
517
|
+
const jsonData = JSON.parse(text);
|
|
518
|
+
return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root: jsonData });
|
|
519
|
+
}
|
|
407
520
|
catch {
|
|
408
521
|
return text;
|
|
409
522
|
}
|
|
410
|
-
|
|
411
|
-
return new fast_xml_parser_1.XMLParser({ ignoreAttributes: false }).parse(text);
|
|
523
|
+
}
|
|
412
524
|
case "text":
|
|
413
525
|
return text;
|
|
414
526
|
case "buffer":
|
|
@@ -417,15 +529,87 @@ class HttpClientImproved {
|
|
|
417
529
|
return text;
|
|
418
530
|
}
|
|
419
531
|
}
|
|
420
|
-
catch (
|
|
421
|
-
|
|
422
|
-
error,
|
|
423
|
-
status: res.status,
|
|
424
|
-
});
|
|
425
|
-
throw new HttpClientError(`Response parsing failed: ${error instanceof Error ? error.message : String(error)}`, res.status);
|
|
532
|
+
catch (err) {
|
|
533
|
+
throw new HttpClientError(`Parsing failed: ${err?.message ?? String(err)}`, res.status);
|
|
426
534
|
}
|
|
427
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Makes an HTTP request without using the cache.
|
|
538
|
+
* Used for methods that shouldn't be cached or when caching is disabled.
|
|
539
|
+
* @param method - HTTP method
|
|
540
|
+
* @param req - Request configuration
|
|
541
|
+
* @param responseType - Expected response type
|
|
542
|
+
* @returns Promise resolving to the response data
|
|
543
|
+
*/
|
|
544
|
+
async requestInternalWithoutCache(method, req, responseType) {
|
|
545
|
+
const url = req.getURL();
|
|
546
|
+
const rawBody = req.getBodyData();
|
|
547
|
+
const headers = {
|
|
548
|
+
...this.defaultHeaders,
|
|
549
|
+
...req.getHeaders(),
|
|
550
|
+
};
|
|
551
|
+
const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
552
|
+
let body;
|
|
553
|
+
const contentType = headers["content-type"] || headers["Content-Type"] || "";
|
|
554
|
+
if (isBodyAllowed && rawBody !== undefined && rawBody !== null) {
|
|
555
|
+
if (Buffer.isBuffer(rawBody)) {
|
|
556
|
+
body = rawBody;
|
|
557
|
+
}
|
|
558
|
+
else if (typeof rawBody === "string") {
|
|
559
|
+
body = rawBody;
|
|
560
|
+
}
|
|
561
|
+
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
562
|
+
body = new url_1.URLSearchParams(rawBody).toString();
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
body = JSON.stringify(rawBody);
|
|
566
|
+
if (!contentType)
|
|
567
|
+
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const metrics = {
|
|
571
|
+
startTime: Date.now(),
|
|
572
|
+
endTime: 0,
|
|
573
|
+
duration: 0,
|
|
574
|
+
bytesReceived: 0,
|
|
575
|
+
bytesSent: 0,
|
|
576
|
+
retries: 0,
|
|
577
|
+
cached: false,
|
|
578
|
+
url,
|
|
579
|
+
method,
|
|
580
|
+
};
|
|
581
|
+
const result = await this.queue.enqueue(async () => {
|
|
582
|
+
const res = await this.sendWithRetry(method, url, headers, body, metrics);
|
|
583
|
+
metrics.statusCode = res.status;
|
|
584
|
+
metrics.bytesReceived = res.body.length;
|
|
585
|
+
metrics.bytesSent = body instanceof Buffer
|
|
586
|
+
? body.length
|
|
587
|
+
: Buffer.byteLength(body || "");
|
|
588
|
+
if (method === "HEAD") {
|
|
589
|
+
return { status: res.status, headers: res.headers };
|
|
590
|
+
}
|
|
591
|
+
const parsed = await this.parseResponse(res, responseType);
|
|
592
|
+
return parsed;
|
|
593
|
+
});
|
|
594
|
+
metrics.endTime = Date.now();
|
|
595
|
+
metrics.duration = metrics.endTime - metrics.startTime;
|
|
596
|
+
this.requestMetrics.set(url, metrics);
|
|
597
|
+
this.trimMetrics();
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Makes an HTTP request with caching support.
|
|
602
|
+
* Handles cache lookups, request deduplication, and automatic cache storage.
|
|
603
|
+
* @param method - HTTP method
|
|
604
|
+
* @param req - Request configuration
|
|
605
|
+
* @param useCache - Whether to use caching (default: true)
|
|
606
|
+
* @param responseType - Expected response type
|
|
607
|
+
* @returns Promise resolving to the response data
|
|
608
|
+
*/
|
|
428
609
|
async requestInternal(method, req, useCache = true, responseType) {
|
|
610
|
+
if (this.options.cacheTTL === 0) {
|
|
611
|
+
return this.requestInternalWithoutCache(method, req, responseType);
|
|
612
|
+
}
|
|
429
613
|
const url = req.getURL();
|
|
430
614
|
const rawBody = req.getBodyData();
|
|
431
615
|
const headers = {
|
|
@@ -433,7 +617,6 @@ class HttpClientImproved {
|
|
|
433
617
|
...req.getHeaders(),
|
|
434
618
|
};
|
|
435
619
|
const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
436
|
-
// Prepare request body + auto content-type for JSON
|
|
437
620
|
let body;
|
|
438
621
|
const contentType = headers["content-type"] || headers["Content-Type"] || "";
|
|
439
622
|
if (isBodyAllowed && rawBody !== undefined && rawBody !== null) {
|
|
@@ -444,26 +627,28 @@ class HttpClientImproved {
|
|
|
444
627
|
body = rawBody;
|
|
445
628
|
}
|
|
446
629
|
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
447
|
-
body =
|
|
630
|
+
body = new url_1.URLSearchParams(rawBody).toString();
|
|
448
631
|
}
|
|
449
632
|
else {
|
|
450
|
-
// default JSON
|
|
451
633
|
body = JSON.stringify(rawBody);
|
|
452
634
|
if (!contentType)
|
|
453
635
|
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
454
636
|
}
|
|
455
637
|
}
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
638
|
+
const bodyHash = this.hashBody(body);
|
|
639
|
+
const cacheKey = (0, crypto_1.createHash)("sha1")
|
|
640
|
+
.update(method + url + bodyHash + responseType)
|
|
641
|
+
.digest("hex");
|
|
642
|
+
if (this.options.cacheMethods.includes(method) && useCache) {
|
|
643
|
+
const cached = this.cache.get(cacheKey);
|
|
459
644
|
if (cached) {
|
|
460
645
|
this.log("debug", `Cache hit for ${url}`);
|
|
461
646
|
return cached;
|
|
462
647
|
}
|
|
463
648
|
}
|
|
464
|
-
if (this.inflight.has(
|
|
649
|
+
if (this.inflight.has(cacheKey)) {
|
|
465
650
|
this.log("debug", `Deduplicating request for ${url}`);
|
|
466
|
-
return this.inflight.get(
|
|
651
|
+
return this.inflight.get(cacheKey);
|
|
467
652
|
}
|
|
468
653
|
const promise = (async () => {
|
|
469
654
|
const metrics = {
|
|
@@ -476,6 +661,7 @@ class HttpClientImproved {
|
|
|
476
661
|
cached: false,
|
|
477
662
|
url,
|
|
478
663
|
method,
|
|
664
|
+
bodyHash,
|
|
479
665
|
};
|
|
480
666
|
try {
|
|
481
667
|
this.log("debug", `Starting request: ${method} ${url}`);
|
|
@@ -487,115 +673,194 @@ class HttpClientImproved {
|
|
|
487
673
|
body instanceof Buffer
|
|
488
674
|
? body.length
|
|
489
675
|
: Buffer.byteLength(body || "");
|
|
676
|
+
if (method === "HEAD") {
|
|
677
|
+
return { status: res.status, headers: res.headers };
|
|
678
|
+
}
|
|
490
679
|
const parsed = await this.parseResponse(res, responseType);
|
|
491
|
-
if (method
|
|
492
|
-
this.cache.set(
|
|
680
|
+
if (this.options.cacheMethods.includes(method) && useCache) {
|
|
681
|
+
this.cache.set(cacheKey, parsed);
|
|
493
682
|
metrics.cached = true;
|
|
494
683
|
}
|
|
495
684
|
return parsed;
|
|
496
685
|
});
|
|
497
686
|
metrics.endTime = Date.now();
|
|
498
687
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
499
|
-
this.requestMetrics.set(
|
|
688
|
+
this.requestMetrics.set(cacheKey, metrics);
|
|
689
|
+
this.trimMetrics();
|
|
500
690
|
this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
|
|
501
691
|
return result;
|
|
502
692
|
}
|
|
503
693
|
catch (error) {
|
|
504
694
|
metrics.endTime = Date.now();
|
|
505
695
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
506
|
-
this.requestMetrics.set(
|
|
696
|
+
this.requestMetrics.set(cacheKey, metrics);
|
|
697
|
+
this.trimMetrics();
|
|
507
698
|
throw error;
|
|
508
699
|
}
|
|
509
700
|
finally {
|
|
510
|
-
|
|
511
|
-
this.inflight.delete(key);
|
|
701
|
+
this.inflight.delete(cacheKey);
|
|
512
702
|
}
|
|
513
703
|
})();
|
|
514
|
-
this.inflight.set(
|
|
704
|
+
this.inflight.set(cacheKey, promise);
|
|
515
705
|
return promise;
|
|
516
706
|
}
|
|
517
707
|
/**
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* @
|
|
522
|
-
* @
|
|
708
|
+
* Makes an HTTP GET request.
|
|
709
|
+
* Supports both RequestInterface objects and direct URL strings.
|
|
710
|
+
* GET requests are cached by default unless caching is disabled.
|
|
711
|
+
* @param req - Request configuration or URL string
|
|
712
|
+
* @param responseType - Expected response type (default: "json")
|
|
713
|
+
* @returns Promise resolving to the response data
|
|
523
714
|
*/
|
|
524
|
-
get(req, responseType) {
|
|
525
|
-
|
|
715
|
+
get(req, responseType = "json") {
|
|
716
|
+
if (typeof req === "string") {
|
|
717
|
+
const simpleReq = {
|
|
718
|
+
getURL: () => req,
|
|
719
|
+
getBodyData: () => undefined,
|
|
720
|
+
getHeaders: () => ({}),
|
|
721
|
+
};
|
|
722
|
+
return this.requestInternal("GET", simpleReq, true, responseType);
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
return this.requestInternal("GET", req, true, responseType);
|
|
726
|
+
}
|
|
526
727
|
}
|
|
527
728
|
/**
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
* @
|
|
532
|
-
* @
|
|
729
|
+
* Makes an HTTP POST request.
|
|
730
|
+
* Supports both RequestInterface objects and direct URL strings with body data.
|
|
731
|
+
* POST requests are not cached by default due to their side effects.
|
|
732
|
+
* @param req - Request configuration or URL string
|
|
733
|
+
* @param body - Request body data (optional)
|
|
734
|
+
* @param responseType - Expected response type (default: "json")
|
|
735
|
+
* @returns Promise resolving to the response data
|
|
533
736
|
*/
|
|
534
|
-
post(req, responseType) {
|
|
535
|
-
|
|
737
|
+
post(req, body, responseType = "json") {
|
|
738
|
+
if (typeof req === "string") {
|
|
739
|
+
const simpleReq = {
|
|
740
|
+
getURL: () => req,
|
|
741
|
+
getBodyData: () => body,
|
|
742
|
+
getHeaders: () => ({ "Content-Type": "application/json" }),
|
|
743
|
+
};
|
|
744
|
+
return this.requestInternal("POST", simpleReq, false, responseType);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
return this.requestInternal("POST", req, false, responseType);
|
|
748
|
+
}
|
|
536
749
|
}
|
|
537
750
|
/**
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
* @
|
|
542
|
-
* @
|
|
751
|
+
* Makes an HTTP PUT request.
|
|
752
|
+
* Supports both RequestInterface objects and direct URL strings with body data.
|
|
753
|
+
* PUT requests are not cached by default due to their side effects.
|
|
754
|
+
* @param req - Request configuration or URL string
|
|
755
|
+
* @param body - Request body data (optional)
|
|
756
|
+
* @param responseType - Expected response type (default: "json")
|
|
757
|
+
* @returns Promise resolving to the response data
|
|
543
758
|
*/
|
|
544
|
-
put(req, responseType) {
|
|
759
|
+
put(req, body, responseType = "json") {
|
|
760
|
+
if (typeof req === "string") {
|
|
761
|
+
const client = defaultClient ?? (defaultClient = new HttpClientImproved());
|
|
762
|
+
const simpleReq = {
|
|
763
|
+
getURL: () => req,
|
|
764
|
+
getBodyData: () => body,
|
|
765
|
+
getHeaders: () => ({ "Content-Type": "application/json" }),
|
|
766
|
+
};
|
|
767
|
+
return client.put(simpleReq, responseType);
|
|
768
|
+
}
|
|
545
769
|
return this.requestInternal("PUT", req, false, responseType);
|
|
546
770
|
}
|
|
547
771
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
* @
|
|
552
|
-
* @
|
|
772
|
+
* Makes an HTTP DELETE request.
|
|
773
|
+
* Supports both RequestInterface objects and direct URL strings.
|
|
774
|
+
* DELETE requests are not cached by default due to their side effects.
|
|
775
|
+
* @param req - Request configuration or URL string
|
|
776
|
+
* @param responseType - Expected response type (default: "json")
|
|
777
|
+
* @returns Promise resolving to the response data
|
|
553
778
|
*/
|
|
554
|
-
delete(req, responseType) {
|
|
779
|
+
delete(req, responseType = "json") {
|
|
780
|
+
if (typeof req === "string") {
|
|
781
|
+
const client = defaultClient ?? (defaultClient = new HttpClientImproved());
|
|
782
|
+
const simpleReq = {
|
|
783
|
+
getURL: () => req,
|
|
784
|
+
getBodyData: () => undefined,
|
|
785
|
+
getHeaders: () => ({}),
|
|
786
|
+
};
|
|
787
|
+
return client.delete(simpleReq, responseType);
|
|
788
|
+
}
|
|
555
789
|
return this.requestInternal("DELETE", req, false, responseType);
|
|
556
790
|
}
|
|
557
791
|
/**
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
* @param
|
|
561
|
-
* @
|
|
562
|
-
* @
|
|
792
|
+
* Makes an HTTP PATCH request.
|
|
793
|
+
* PATCH requests are not cached by default due to their side effects.
|
|
794
|
+
* @param req - Request configuration
|
|
795
|
+
* @param responseType - Expected response type (default: "json")
|
|
796
|
+
* @returns Promise resolving to the response data
|
|
563
797
|
*/
|
|
564
|
-
patch(req, responseType) {
|
|
798
|
+
patch(req, responseType = "json") {
|
|
565
799
|
return this.requestInternal("PATCH", req, false, responseType);
|
|
566
800
|
}
|
|
567
801
|
/**
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
802
|
+
* Makes an HTTP HEAD request.
|
|
803
|
+
* Returns only the status code and headers without the response body.
|
|
804
|
+
* HEAD requests are not cached by default.
|
|
805
|
+
* @param req - Request configuration or URL string
|
|
806
|
+
* @returns Promise resolving to status and headers
|
|
571
807
|
*/
|
|
572
|
-
head(req) {
|
|
573
|
-
|
|
808
|
+
async head(req) {
|
|
809
|
+
if (typeof req === "string") {
|
|
810
|
+
const client = defaultClient ?? (defaultClient = new HttpClientImproved());
|
|
811
|
+
const simpleReq = {
|
|
812
|
+
getURL: () => req,
|
|
813
|
+
getBodyData: () => undefined,
|
|
814
|
+
getHeaders: () => ({}),
|
|
815
|
+
};
|
|
816
|
+
return client.head(simpleReq);
|
|
817
|
+
}
|
|
818
|
+
return this.requestInternal("HEAD", req, false);
|
|
574
819
|
}
|
|
575
820
|
/**
|
|
576
|
-
* Clears the
|
|
821
|
+
* Clears the internal cache of the HTTP client.
|
|
822
|
+
* Removes all cached responses and resets the cache state.
|
|
577
823
|
*/
|
|
578
824
|
clearCache() {
|
|
579
825
|
this.cache.clear();
|
|
580
826
|
this.log("info", "Cache cleared");
|
|
581
827
|
}
|
|
582
828
|
/**
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
* @param method The HTTP method
|
|
586
|
-
* @returns The request metrics if available, undefined otherwise
|
|
829
|
+
* Clears all collected request metrics.
|
|
830
|
+
* Removes performance and timing data from memory.
|
|
587
831
|
*/
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
832
|
+
clearMetrics() {
|
|
833
|
+
this.requestMetrics.clear();
|
|
834
|
+
this.log("info", "Metrics cleared");
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Retrieves metrics for a specific request by its URL.
|
|
838
|
+
* @param key - The URL or cache key to retrieve metrics for
|
|
839
|
+
* @returns Metrics object if found, undefined otherwise
|
|
840
|
+
*/
|
|
841
|
+
getMetrics(key) {
|
|
842
|
+
return this.requestMetrics.get(key);
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Retrieves all collected request metrics.
|
|
846
|
+
* @returns Array of all metrics objects
|
|
847
|
+
*/
|
|
848
|
+
getAllMetrics() {
|
|
849
|
+
return Array.from(this.requestMetrics.values());
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Creates a fluent request builder for making HTTP requests.
|
|
853
|
+
* Provides a chainable API for building and sending requests.
|
|
854
|
+
* @param url - The target URL for the request
|
|
855
|
+
* @returns RequestBuilder instance for chaining
|
|
856
|
+
*/
|
|
857
|
+
request(url) {
|
|
858
|
+
return new RequestBuilder(url);
|
|
595
859
|
}
|
|
596
860
|
/**
|
|
597
861
|
* Returns current statistics about the HTTP client's state.
|
|
598
|
-
*
|
|
862
|
+
* Useful for monitoring and debugging performance.
|
|
863
|
+
* @returns Object containing various client statistics
|
|
599
864
|
*/
|
|
600
865
|
getStats() {
|
|
601
866
|
return {
|
|
@@ -604,8 +869,159 @@ class HttpClientImproved {
|
|
|
604
869
|
queuedRequests: this.queue.queuedCount,
|
|
605
870
|
activeRequests: this.queue.activeCount,
|
|
606
871
|
currentRateLimit: this.limiter.currentCount,
|
|
872
|
+
metricsSize: this.requestMetrics.size,
|
|
607
873
|
};
|
|
608
874
|
}
|
|
609
875
|
}
|
|
610
876
|
exports.default = HttpClientImproved;
|
|
877
|
+
/**
|
|
878
|
+
* Fluent request builder for making HTTP requests with a chainable API.
|
|
879
|
+
* Provides a convenient way to build and send HTTP requests with various options.
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* ```ts
|
|
883
|
+
* const client = new HttpClientImproved();
|
|
884
|
+
* const response = await client.request('https://api.example.com/data')
|
|
885
|
+
* .headers({ 'Authorization': 'Bearer token' })
|
|
886
|
+
* .query({ limit: 10, offset: 0 })
|
|
887
|
+
* .json()
|
|
888
|
+
* .send();
|
|
889
|
+
* ```
|
|
890
|
+
*/
|
|
891
|
+
class RequestBuilder {
|
|
892
|
+
_url;
|
|
893
|
+
_method = "GET";
|
|
894
|
+
_headers = {};
|
|
895
|
+
_body;
|
|
896
|
+
_responseType = "json";
|
|
897
|
+
/**
|
|
898
|
+
* Creates a new request builder for the specified URL.
|
|
899
|
+
* @param url - The target URL for the request
|
|
900
|
+
*/
|
|
901
|
+
constructor(url) {
|
|
902
|
+
this._url = url;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Sets HTTP headers for the request.
|
|
906
|
+
* @param headers - Object containing header key-value pairs
|
|
907
|
+
* @returns The builder instance for chaining
|
|
908
|
+
*/
|
|
909
|
+
headers(headers) {
|
|
910
|
+
this._headers = headers;
|
|
911
|
+
return this;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Sets the request body data.
|
|
915
|
+
* @param bodyData - The body data to send with the request
|
|
916
|
+
* @returns The builder instance for chaining
|
|
917
|
+
*/
|
|
918
|
+
body(bodyData) {
|
|
919
|
+
this._body = bodyData;
|
|
920
|
+
return this;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Sets the response type to JSON.
|
|
924
|
+
* @returns The builder instance for chaining
|
|
925
|
+
*/
|
|
926
|
+
json() {
|
|
927
|
+
this._responseType = "json";
|
|
928
|
+
return this;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Sets the response type to plain text.
|
|
932
|
+
* @returns The builder instance for chaining
|
|
933
|
+
*/
|
|
934
|
+
text() {
|
|
935
|
+
this._responseType = "text";
|
|
936
|
+
return this;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Sets the response type to XML.
|
|
940
|
+
* @returns The builder instance for chaining
|
|
941
|
+
*/
|
|
942
|
+
xml() {
|
|
943
|
+
this._responseType = "xml";
|
|
944
|
+
return this;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Sets the HTTP method to POST.
|
|
948
|
+
* @returns The builder instance for chaining
|
|
949
|
+
*/
|
|
950
|
+
post() {
|
|
951
|
+
this._method = "POST";
|
|
952
|
+
return this;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Sets the HTTP method to PUT.
|
|
956
|
+
* @returns The builder instance for chaining
|
|
957
|
+
*/
|
|
958
|
+
put() {
|
|
959
|
+
this._method = "PUT";
|
|
960
|
+
return this;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Sets the HTTP method to PATCH.
|
|
964
|
+
* @returns The builder instance for chaining
|
|
965
|
+
*/
|
|
966
|
+
patch() {
|
|
967
|
+
this._method = "PATCH";
|
|
968
|
+
return this;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Sets the HTTP method to DELETE.
|
|
972
|
+
* @returns The builder instance for chaining
|
|
973
|
+
*/
|
|
974
|
+
delete() {
|
|
975
|
+
this._method = "DELETE";
|
|
976
|
+
return this;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Adds query parameters to the URL.
|
|
980
|
+
* @param params - Object containing query parameter key-value pairs
|
|
981
|
+
* @returns The builder instance for chaining
|
|
982
|
+
*/
|
|
983
|
+
query(params) {
|
|
984
|
+
const urlObj = new URL(this._url);
|
|
985
|
+
Object.entries(params).forEach(([k, v]) => urlObj.searchParams.set(k, String(v)));
|
|
986
|
+
this._url = urlObj.toString();
|
|
987
|
+
return this;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Sets a JSON body for the request.
|
|
991
|
+
* Automatically sets the Content-Type header to application/json.
|
|
992
|
+
* @param body - The JSON body data
|
|
993
|
+
* @returns The builder instance for chaining
|
|
994
|
+
*/
|
|
995
|
+
jsonBody(body) {
|
|
996
|
+
this._body = body;
|
|
997
|
+
this._headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
998
|
+
return this;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Sends the HTTP request and returns the response.
|
|
1002
|
+
* @returns Promise resolving to the response data
|
|
1003
|
+
*/
|
|
1004
|
+
async send() {
|
|
1005
|
+
const client = defaultClient ?? (defaultClient = new HttpClientImproved());
|
|
1006
|
+
const req = {
|
|
1007
|
+
getURL: () => this._url,
|
|
1008
|
+
getBodyData: () => this._body,
|
|
1009
|
+
getHeaders: () => this._headers,
|
|
1010
|
+
};
|
|
1011
|
+
switch (this._method) {
|
|
1012
|
+
case "GET":
|
|
1013
|
+
return client.get(req, this._responseType);
|
|
1014
|
+
case "POST":
|
|
1015
|
+
return client.post(req, this._responseType);
|
|
1016
|
+
case "PUT":
|
|
1017
|
+
return client.put(req, this._responseType);
|
|
1018
|
+
case "DELETE":
|
|
1019
|
+
return client.delete(req, this._responseType);
|
|
1020
|
+
case "PATCH":
|
|
1021
|
+
return client.patch(req, this._responseType);
|
|
1022
|
+
default:
|
|
1023
|
+
return client.get(req, this._responseType);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
611
1027
|
//# sourceMappingURL=HttpClientImproved.js.map
|