hyperttp 0.2.1 → 0.2.3
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/CacheManager.d.ts +40 -3
- package/dist/Hyperttp/Core/CacheManager.d.ts.map +1 -1
- package/dist/Hyperttp/Core/CacheManager.js +61 -12
- package/dist/Hyperttp/Core/CacheManager.js.map +1 -1
- package/dist/Hyperttp/Core/HttpClientImproved.d.ts +69 -97
- package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
- package/dist/Hyperttp/Core/HttpClientImproved.js +331 -642
- package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
- package/dist/Hyperttp/Core/InterceptorManager.d.ts +62 -0
- package/dist/Hyperttp/Core/InterceptorManager.d.ts.map +1 -0
- package/dist/Hyperttp/Core/InterceptorManager.js +64 -0
- package/dist/Hyperttp/Core/InterceptorManager.js.map +1 -0
- package/dist/Hyperttp/Core/MetricsManager.d.ts +80 -0
- package/dist/Hyperttp/Core/MetricsManager.d.ts.map +1 -0
- package/dist/Hyperttp/Core/MetricsManager.js +140 -0
- package/dist/Hyperttp/Core/MetricsManager.js.map +1 -0
- package/dist/Hyperttp/Core/QueueManager.d.ts +5 -40
- package/dist/Hyperttp/Core/QueueManager.d.ts.map +1 -1
- package/dist/Hyperttp/Core/QueueManager.js +41 -47
- package/dist/Hyperttp/Core/QueueManager.js.map +1 -1
- package/dist/Hyperttp/Core/RateLimiter.d.ts +40 -20
- package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
- package/dist/Hyperttp/Core/RateLimiter.js +65 -44
- package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
- package/dist/Hyperttp/Core/RequestBuilder.d.ts +48 -64
- package/dist/Hyperttp/Core/RequestBuilder.d.ts.map +1 -1
- package/dist/Hyperttp/Core/RequestBuilder.js +87 -99
- package/dist/Hyperttp/Core/RequestBuilder.js.map +1 -1
- package/dist/Hyperttp/Core/RequestExecutor.d.ts +58 -0
- package/dist/Hyperttp/Core/RequestExecutor.d.ts.map +1 -0
- package/dist/Hyperttp/Core/RequestExecutor.js +160 -0
- package/dist/Hyperttp/Core/RequestExecutor.js.map +1 -0
- package/dist/Hyperttp/Core/ResponseTransformer.d.ts +35 -0
- package/dist/Hyperttp/Core/ResponseTransformer.d.ts.map +1 -0
- package/dist/Hyperttp/Core/ResponseTransformer.js +171 -0
- package/dist/Hyperttp/Core/ResponseTransformer.js.map +1 -0
- package/dist/Hyperttp/Core/index.d.ts +7 -1
- package/dist/Hyperttp/Core/index.d.ts.map +1 -1
- package/dist/Hyperttp/Core/index.js +17 -5
- package/dist/Hyperttp/Core/index.js.map +1 -1
- package/dist/Hyperttp/Request.d.ts +5 -0
- package/dist/Hyperttp/Request.d.ts.map +1 -1
- package/dist/Hyperttp/Request.js +17 -35
- package/dist/Hyperttp/Request.js.map +1 -1
- package/dist/Hyperttp/UrlExtractor.d.ts +1 -1
- package/dist/Hyperttp/UrlExtractor.d.ts.map +1 -1
- package/dist/Hyperttp/UrlExtractor.js.map +1 -1
- package/dist/Hyperttp/index.d.ts +3 -1
- package/dist/Hyperttp/index.d.ts.map +1 -1
- package/dist/Hyperttp/index.js +6 -1
- package/dist/Hyperttp/index.js.map +1 -1
- package/dist/Types/index.d.ts +45 -206
- package/dist/Types/index.d.ts.map +1 -1
- package/dist/Types/index.js +7 -16
- package/dist/Types/index.js.map +1 -1
- package/dist/Types/request.d.ts +15 -100
- package/dist/Types/request.d.ts.map +1 -1
- package/dist/Types/request.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,750 +1,439 @@
|
|
|
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
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const RequestBuilder_1 = require("./RequestBuilder");
|
|
47
|
-
const gunzip = (0, util_1.promisify)(zlib.gunzip);
|
|
48
|
-
const inflate = (0, util_1.promisify)(zlib.inflate);
|
|
49
|
-
const brotliDecompress = (0, util_1.promisify)(zlib.brotliDecompress);
|
|
3
|
+
const undici_1 = require("http-cookie-agent/undici");
|
|
4
|
+
const CacheManager_js_1 = require("./CacheManager.js");
|
|
5
|
+
const QueueManager_js_1 = require("./QueueManager.js");
|
|
6
|
+
const RateLimiter_js_1 = require("./RateLimiter.js");
|
|
7
|
+
const MetricsManager_js_1 = require("./MetricsManager.js");
|
|
8
|
+
const RequestBuilder_js_1 = require("./RequestBuilder.js");
|
|
9
|
+
const InterceptorManager_js_1 = require("./InterceptorManager.js");
|
|
10
|
+
const ResponseTransformer_js_1 = require("./ResponseTransformer.js");
|
|
11
|
+
const RequestExecutor_js_1 = require("./RequestExecutor.js");
|
|
12
|
+
const index_js_1 = require("../../Types/index.js");
|
|
50
13
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
14
|
+
* @class HttpClientImproved
|
|
15
|
+
* @en High-performance HTTP client with built-in caching, queuing, rate limiting, and metrics.
|
|
16
|
+
* @ru Высокопроизводительный HTTP-клиент со встроенным кэшированием, очередями, лимитами и метриками.
|
|
53
17
|
*/
|
|
54
18
|
class HttpClientImproved {
|
|
55
|
-
cookieJar = new tough_cookie_1.CookieJar();
|
|
56
19
|
agent;
|
|
20
|
+
options;
|
|
57
21
|
cache;
|
|
58
22
|
queue;
|
|
59
23
|
limiter;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
requestInterceptors = [];
|
|
65
|
-
responseInterceptors = [];
|
|
66
|
-
requestMetrics = new Map();
|
|
24
|
+
metricsManager;
|
|
25
|
+
interceptors;
|
|
26
|
+
transformer;
|
|
27
|
+
executor;
|
|
67
28
|
/**
|
|
68
|
-
*
|
|
69
|
-
* @
|
|
29
|
+
* @en Internal map to track active requests for deduplication and cancellation.
|
|
30
|
+
* @ru Внутренняя карта для отслеживания активных запросов (дедупликация и отмена).
|
|
70
31
|
*/
|
|
32
|
+
inflight = new Map();
|
|
33
|
+
defaultHeaders = {};
|
|
71
34
|
constructor(options) {
|
|
72
|
-
this.options =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
followRedirects: options?.followRedirects ?? true,
|
|
79
|
-
maxRedirects: options?.maxRedirects ?? 5,
|
|
80
|
-
validateStatus: options?.validateStatus ??
|
|
81
|
-
((status) => status >= 200 && status < 300),
|
|
82
|
-
cacheMethods: options?.cacheMethods ?? ["GET", "HEAD"],
|
|
83
|
-
maxMetricsSize: options?.maxMetricsSize ?? 1000,
|
|
84
|
-
rateLimit: options?.rateLimit ?? { maxRequests: 100, windowMs: 60000 },
|
|
85
|
-
userAgent: options?.userAgent ?? "Hyperttp/0.1.0 Node.js",
|
|
86
|
-
logger: options?.logger ??
|
|
87
|
-
((level, message, meta) => {
|
|
88
|
-
const methods = {
|
|
89
|
-
debug: console.debug,
|
|
90
|
-
info: console.info,
|
|
91
|
-
warn: console.warn,
|
|
92
|
-
error: console.error,
|
|
93
|
-
};
|
|
94
|
-
(methods[level] || console.log)(`[${level.toUpperCase()}] ${message}`, meta || "");
|
|
95
|
-
}),
|
|
96
|
-
retryOptions: options?.retryOptions ?? {},
|
|
97
|
-
maxResponseBytes: options?.maxResponseBytes ?? 1024 * 1024,
|
|
98
|
-
verbose: false,
|
|
99
|
-
enableQueue: options?.enableQueue ?? false,
|
|
100
|
-
enableRateLimit: options?.enableRateLimit ?? false,
|
|
101
|
-
enableCache: options?.enableCache ?? true,
|
|
102
|
-
};
|
|
35
|
+
this.options = this.applyDefaultOptions(options);
|
|
36
|
+
this.metricsManager = new MetricsManager_js_1.MetricsManager({
|
|
37
|
+
maxHistory: this.options.maxMetricsSize,
|
|
38
|
+
});
|
|
39
|
+
this.interceptors = new InterceptorManager_js_1.InterceptorManager();
|
|
40
|
+
this.transformer = new ResponseTransformer_js_1.ResponseTransformer(this.options.maxResponseBytes, this.options.logger);
|
|
103
41
|
if (this.options.enableCache) {
|
|
104
|
-
this.cache = new
|
|
42
|
+
this.cache = new CacheManager_js_1.CacheManager({
|
|
105
43
|
cacheTTL: this.options.cacheTTL,
|
|
106
44
|
cacheMaxSize: this.options.cacheMaxSize,
|
|
107
45
|
});
|
|
108
46
|
}
|
|
109
47
|
if (this.options.enableQueue) {
|
|
110
|
-
this.queue = new
|
|
48
|
+
this.queue = new QueueManager_js_1.QueueManager(this.options.maxConcurrent ?? 500);
|
|
111
49
|
}
|
|
112
50
|
if (this.options.enableRateLimit) {
|
|
113
|
-
this.limiter = new
|
|
51
|
+
this.limiter = new RateLimiter_js_1.RateLimiter(this.options.rateLimit);
|
|
114
52
|
}
|
|
115
|
-
this.
|
|
116
|
-
maxRetries: this.options.maxRetries ?? 5,
|
|
117
|
-
baseDelay: this.options.retryOptions?.baseDelay ?? 1000,
|
|
118
|
-
maxDelay: this.options.retryOptions?.maxDelay ?? 30000,
|
|
119
|
-
retryStatusCodes: this.options.retryOptions?.retryStatusCodes ?? [
|
|
120
|
-
408, 429, 500, 502, 503, 504,
|
|
121
|
-
],
|
|
122
|
-
jitter: this.options.retryOptions?.jitter ?? true,
|
|
123
|
-
};
|
|
124
|
-
this.defaultHeaders = {
|
|
125
|
-
Accept: "application/json, text/plain, */*",
|
|
126
|
-
"Accept-Encoding": "gzip, deflate, br",
|
|
127
|
-
"User-Agent": this.options.userAgent ?? "Hyperttp/0.1.0 Node.js",
|
|
128
|
-
};
|
|
129
|
-
this.agent = new undici_1.Agent({
|
|
53
|
+
this.agent = new undici_1.CookieAgent({
|
|
130
54
|
connections: 1000,
|
|
131
55
|
pipelining: 10,
|
|
132
56
|
keepAliveTimeout: 60000,
|
|
133
|
-
keepAliveMaxTimeout: 600000,
|
|
134
|
-
interceptors: {
|
|
135
|
-
Client: [(0, undici_2.cookie)({ jar: this.cookieJar })],
|
|
136
|
-
},
|
|
137
57
|
});
|
|
58
|
+
this.executor = new RequestExecutor_js_1.RequestExecutor(this.agent, this.interceptors, {
|
|
59
|
+
timeout: this.options.timeout,
|
|
60
|
+
maxRetries: this.options.maxRetries,
|
|
61
|
+
followRedirects: this.options.followRedirects,
|
|
62
|
+
maxRedirects: this.options.maxRedirects,
|
|
63
|
+
retryOptions: this.options.retryOptions,
|
|
64
|
+
verbose: this.options.verbose,
|
|
65
|
+
logger: this.options.logger,
|
|
66
|
+
});
|
|
67
|
+
this.defaultHeaders = {
|
|
68
|
+
Accept: "application/json, text/plain, */*",
|
|
69
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
70
|
+
"User-Agent": this.options.userAgent,
|
|
71
|
+
};
|
|
138
72
|
}
|
|
139
73
|
/**
|
|
140
|
-
*
|
|
141
|
-
* @
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Returns the cookie jar used for managing HTTP cookies.
|
|
148
|
-
* @returns The CookieJar instance
|
|
149
|
-
*/
|
|
150
|
-
getCookieJar() {
|
|
151
|
-
return this.cookieJar;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Adds a request interceptor to modify requests before they are sent.
|
|
155
|
-
* @param interceptor The interceptor function to add
|
|
156
|
-
*/
|
|
157
|
-
addRequestInterceptor(interceptor) {
|
|
158
|
-
this.requestInterceptors.push(interceptor);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Adds a response interceptor to modify responses after they are received.
|
|
162
|
-
* @param interceptor The interceptor function to add
|
|
74
|
+
* @en Core internal method for handling all HTTP requests.
|
|
75
|
+
* @ru Основной внутренний метод для обработки всех HTTP-запросов.
|
|
76
|
+
* @param method HTTP method (GET, POST, etc.)
|
|
77
|
+
* @param req Request object
|
|
78
|
+
* @param useCache Whether to use caching for this request
|
|
79
|
+
* @param responseType Expected response format
|
|
163
80
|
*/
|
|
164
|
-
|
|
165
|
-
this.responseInterceptors.push(interceptor);
|
|
166
|
-
}
|
|
167
|
-
/** Closes the HTTP agent to properly terminate keep-alive connections. */
|
|
168
|
-
close() {
|
|
169
|
-
this.agent.close();
|
|
170
|
-
}
|
|
171
|
-
log(level, msg, meta) {
|
|
172
|
-
if (this.options.verbose) {
|
|
173
|
-
if (this.options.logger)
|
|
174
|
-
this.options.logger(level, msg, meta);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
async decompress(buf, enc, charset = "utf-8") {
|
|
178
|
-
if (!enc)
|
|
179
|
-
return buf.toString(charset);
|
|
180
|
-
try {
|
|
181
|
-
switch (enc.toLowerCase()) {
|
|
182
|
-
case "gzip":
|
|
183
|
-
return (await gunzip(buf)).toString(charset);
|
|
184
|
-
case "deflate":
|
|
185
|
-
return (await inflate(buf)).toString(charset);
|
|
186
|
-
case "br":
|
|
187
|
-
return (await brotliDecompress(buf)).toString(charset);
|
|
188
|
-
default:
|
|
189
|
-
return buf.toString(charset);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
this.log("error", `Decompression failed for encoding ${enc}`, error);
|
|
194
|
-
return buf.toString(charset);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
calcDelay(attempt) {
|
|
198
|
-
const base = Math.min(this.retryOptions.baseDelay * 2 ** attempt, this.retryOptions.maxDelay);
|
|
199
|
-
return this.retryOptions.jitter
|
|
200
|
-
? base * (0.75 + Math.random() * 0.5)
|
|
201
|
-
: base;
|
|
202
|
-
}
|
|
203
|
-
sleep(ms) {
|
|
204
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
205
|
-
}
|
|
206
|
-
async applyRequestInterceptors(config) {
|
|
207
|
-
let result = config;
|
|
208
|
-
for (const interceptor of this.requestInterceptors)
|
|
209
|
-
result = await interceptor(result);
|
|
210
|
-
return result;
|
|
211
|
-
}
|
|
212
|
-
async applyResponseInterceptors(response) {
|
|
213
|
-
let result = response;
|
|
214
|
-
for (const interceptor of this.responseInterceptors)
|
|
215
|
-
result = await interceptor(result);
|
|
216
|
-
return result;
|
|
217
|
-
}
|
|
218
|
-
resolveRedirect(location, baseUrl) {
|
|
219
|
-
try {
|
|
220
|
-
return new URL(location, baseUrl).toString();
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
return location;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
parseRetryAfterMs(retryAfterHeader) {
|
|
227
|
-
if (!retryAfterHeader)
|
|
228
|
-
return undefined;
|
|
229
|
-
const raw = Array.isArray(retryAfterHeader)
|
|
230
|
-
? retryAfterHeader[0]
|
|
231
|
-
: String(retryAfterHeader);
|
|
232
|
-
const asSeconds = Number(raw);
|
|
233
|
-
if (Number.isFinite(asSeconds))
|
|
234
|
-
return Math.max(0, Math.floor(asSeconds * 1000));
|
|
235
|
-
const asDate = Date.parse(raw);
|
|
236
|
-
if (!Number.isNaN(asDate))
|
|
237
|
-
return Math.max(0, asDate - Date.now());
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
async readBodyWithLimit(body) {
|
|
241
|
-
const buf = Buffer.from(await body.arrayBuffer());
|
|
242
|
-
const limit = this.options.maxResponseBytes;
|
|
243
|
-
if (typeof limit === "number" && limit > 0 && buf.length > limit) {
|
|
244
|
-
throw new Types_1.HttpClientError(`Response too large (${buf.length} bytes), limit is ${limit}`, 0);
|
|
245
|
-
}
|
|
246
|
-
return buf;
|
|
247
|
-
}
|
|
248
|
-
async sendWithRetry(method, url, headers, body, metrics, redirects = 0) {
|
|
249
|
-
let lastError;
|
|
250
|
-
for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
|
|
251
|
-
try {
|
|
252
|
-
if (this.limiter && this.options.enableRateLimit) {
|
|
253
|
-
await this.limiter.wait();
|
|
254
|
-
}
|
|
255
|
-
const finalConfig = await this.applyRequestInterceptors({
|
|
256
|
-
url,
|
|
257
|
-
method,
|
|
258
|
-
headers,
|
|
259
|
-
body,
|
|
260
|
-
});
|
|
261
|
-
const controller = new AbortController();
|
|
262
|
-
const timeout = this.options.timeout ?? 15000;
|
|
263
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
264
|
-
try {
|
|
265
|
-
const res = await (0, undici_1.request)(finalConfig.url, {
|
|
266
|
-
method: finalConfig.method,
|
|
267
|
-
headers: finalConfig.headers,
|
|
268
|
-
body: finalConfig.body,
|
|
269
|
-
dispatcher: this.agent,
|
|
270
|
-
signal: controller.signal,
|
|
271
|
-
});
|
|
272
|
-
clearTimeout(timer);
|
|
273
|
-
const buf = await this.readBodyWithLimit(res.body);
|
|
274
|
-
let response = await this.applyResponseInterceptors({
|
|
275
|
-
status: res.statusCode,
|
|
276
|
-
headers: res.headers,
|
|
277
|
-
body: buf,
|
|
278
|
-
url: finalConfig.url,
|
|
279
|
-
});
|
|
280
|
-
// Redirects
|
|
281
|
-
if (this.options.followRedirects &&
|
|
282
|
-
[301, 302, 303, 307, 308].includes(response.status) &&
|
|
283
|
-
redirects < (this.options.maxRedirects ?? 5)) {
|
|
284
|
-
const location = response.headers.location;
|
|
285
|
-
if (location) {
|
|
286
|
-
const nextUrl = this.resolveRedirect(location, finalConfig.url);
|
|
287
|
-
const redirectMethod = response.status === 303 ? "GET" : method;
|
|
288
|
-
const nextHeaders = { ...headers };
|
|
289
|
-
let nextBody = body;
|
|
290
|
-
// If switching to GET, drop body-related headers.
|
|
291
|
-
if (redirectMethod === "GET") {
|
|
292
|
-
nextBody = undefined;
|
|
293
|
-
delete nextHeaders["content-type"];
|
|
294
|
-
delete nextHeaders["Content-Type"];
|
|
295
|
-
delete nextHeaders["content-length"];
|
|
296
|
-
delete nextHeaders["Content-Length"];
|
|
297
|
-
}
|
|
298
|
-
this.log("debug", `Redirecting to ${nextUrl}`, {
|
|
299
|
-
originalUrl: finalConfig.url,
|
|
300
|
-
status: response.status,
|
|
301
|
-
});
|
|
302
|
-
return this.sendWithRetry(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, redirects + 1);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
// Retry by status
|
|
306
|
-
if (this.retryOptions.retryStatusCodes.includes(response.status)) {
|
|
307
|
-
metrics && (metrics.retries += 1);
|
|
308
|
-
if (response.status === 429) {
|
|
309
|
-
const ra = this.parseRetryAfterMs(response.headers["retry-after"]);
|
|
310
|
-
if (ra !== undefined) {
|
|
311
|
-
this.log("warn", `429 Rate limited, waiting Retry-After ${ra}ms`, { url: finalConfig.url });
|
|
312
|
-
if (attempt < this.retryOptions.maxRetries) {
|
|
313
|
-
await this.sleep(ra);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
throw new Types_1.RateLimitError(finalConfig.url, ra);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
this.log("warn", `Retrying ${method} ${finalConfig.url} due to status ${response.status}`, {
|
|
320
|
-
attempt: attempt + 1,
|
|
321
|
-
maxRetries: this.retryOptions.maxRetries,
|
|
322
|
-
});
|
|
323
|
-
if (attempt < this.retryOptions.maxRetries) {
|
|
324
|
-
await this.sleep(this.calcDelay(attempt));
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return response;
|
|
329
|
-
}
|
|
330
|
-
catch (timeoutErr) {
|
|
331
|
-
clearTimeout(timer);
|
|
332
|
-
if (timeoutErr?.name === "AbortError")
|
|
333
|
-
throw new Types_1.TimeoutError(url, timeout);
|
|
334
|
-
throw timeoutErr;
|
|
335
|
-
}
|
|
336
|
-
finally {
|
|
337
|
-
clearTimeout(timer);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
catch (err) {
|
|
341
|
-
lastError = err;
|
|
342
|
-
this.log("error", `Request error ${method} ${url}: ${err?.message ?? String(err)}`, {
|
|
343
|
-
attempt: attempt + 1,
|
|
344
|
-
error: err,
|
|
345
|
-
});
|
|
346
|
-
metrics && (metrics.retries += 1);
|
|
347
|
-
if (attempt < this.retryOptions.maxRetries) {
|
|
348
|
-
await this.sleep(this.calcDelay(attempt));
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (lastError instanceof Types_1.HttpClientError)
|
|
354
|
-
throw lastError;
|
|
355
|
-
throw new Types_1.HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
|
|
356
|
-
}
|
|
357
|
-
parseContentType(contentType) {
|
|
358
|
-
if (!contentType)
|
|
359
|
-
return { type: "text/plain", charset: "utf-8" };
|
|
360
|
-
const parts = contentType.split(";");
|
|
361
|
-
const type = parts[0].trim();
|
|
362
|
-
const rawCharset = parts
|
|
363
|
-
.map((p) => p.trim())
|
|
364
|
-
.find((p) => p.toLowerCase().startsWith("charset="))
|
|
365
|
-
?.split("=")[1]
|
|
366
|
-
?.trim() || "utf-8";
|
|
367
|
-
const normalized = rawCharset.toLowerCase();
|
|
368
|
-
const allowed = [
|
|
369
|
-
"utf8",
|
|
370
|
-
"utf-8",
|
|
371
|
-
"latin1",
|
|
372
|
-
"ucs2",
|
|
373
|
-
"ucs-2",
|
|
374
|
-
"utf16le",
|
|
375
|
-
"utf-16le",
|
|
376
|
-
"ascii",
|
|
377
|
-
"base64",
|
|
378
|
-
"hex",
|
|
379
|
-
];
|
|
380
|
-
const charset = (allowed.includes(normalized)
|
|
381
|
-
? normalized
|
|
382
|
-
: "utf-8");
|
|
383
|
-
return { type, charset };
|
|
384
|
-
}
|
|
385
|
-
xmlParser = new fast_xml_parser_1.XMLParser({
|
|
386
|
-
ignoreAttributes: false,
|
|
387
|
-
allowBooleanAttributes: true,
|
|
388
|
-
});
|
|
389
|
-
async parseResponse(res, responseType) {
|
|
390
|
-
try {
|
|
391
|
-
const contentType = res.headers["content-type"] || "";
|
|
392
|
-
const text = await this.decompress(res.body, res.headers["content-encoding"]);
|
|
393
|
-
const finalType = responseType ?? "json";
|
|
394
|
-
switch (finalType) {
|
|
395
|
-
case "json": {
|
|
396
|
-
if (contentType.includes("json")) {
|
|
397
|
-
return JSON.parse(text);
|
|
398
|
-
}
|
|
399
|
-
if (text.trim().startsWith("<")) {
|
|
400
|
-
return this.xmlParser.parse(text);
|
|
401
|
-
}
|
|
402
|
-
return { text };
|
|
403
|
-
}
|
|
404
|
-
case "xml": {
|
|
405
|
-
const text = await this.decompress(res.body, res.headers["content-encoding"]);
|
|
406
|
-
if (text.trim().startsWith("<")) {
|
|
407
|
-
return text;
|
|
408
|
-
}
|
|
409
|
-
try {
|
|
410
|
-
const json = JSON.parse(text);
|
|
411
|
-
return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root: json });
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
return String(text);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
case "text":
|
|
418
|
-
return text;
|
|
419
|
-
case "buffer":
|
|
420
|
-
return res.body;
|
|
421
|
-
case "stream":
|
|
422
|
-
return res.body;
|
|
423
|
-
default:
|
|
424
|
-
return text;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
catch (error) {
|
|
428
|
-
this.log("error", "Failed to parse response", {
|
|
429
|
-
error,
|
|
430
|
-
status: res.status,
|
|
431
|
-
contentType: res.headers["content-type"],
|
|
432
|
-
});
|
|
433
|
-
throw new Types_1.HttpClientError(`Response parsing failed: ${error instanceof Error ? error.message : String(error)}`, res.status);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
async requestInternal(method, req, useCache = true, responseType) {
|
|
81
|
+
async requestInternal(method, req, useCache = true, responseType = "auto") {
|
|
437
82
|
const url = req.getURL();
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
444
|
-
let body;
|
|
445
|
-
const contentType = headers["content-type"] || headers["Content-Type"] || "";
|
|
446
|
-
if (isBodyAllowed && rawBody !== undefined && rawBody !== null) {
|
|
447
|
-
if (Buffer.isBuffer(rawBody)) {
|
|
448
|
-
body = rawBody;
|
|
449
|
-
}
|
|
450
|
-
else if (typeof rawBody === "string") {
|
|
451
|
-
body = rawBody;
|
|
452
|
-
}
|
|
453
|
-
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
454
|
-
body = new URLSearchParams(rawBody).toString();
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
body = JSON.stringify(rawBody);
|
|
458
|
-
if (!contentType) {
|
|
459
|
-
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
460
|
-
}
|
|
461
|
-
}
|
|
83
|
+
if (this.metricsManager.isCircuitOpen(url)) {
|
|
84
|
+
throw new index_js_1.HttpClientError(`Circuit Breaker is OPEN`, "CIRCUIT_OPEN", 503, undefined, url, method);
|
|
85
|
+
}
|
|
86
|
+
if (this.limiter && this.options.enableRateLimit) {
|
|
87
|
+
await this.limiter.wait();
|
|
462
88
|
}
|
|
463
|
-
const
|
|
89
|
+
const { body, headers } = this.prepareRequestData(method, req);
|
|
90
|
+
const key = method === "GET"
|
|
91
|
+
? `GET:${url}`
|
|
92
|
+
: `${method}:${url}:${body ? JSON.stringify(body) : ""}`;
|
|
464
93
|
if (method === "GET" && useCache && this.cache) {
|
|
465
94
|
const cached = await this.cache.get(key);
|
|
466
|
-
if (cached)
|
|
467
|
-
this.log("debug", `Cache hit for ${url}`);
|
|
95
|
+
if (cached)
|
|
468
96
|
return cached;
|
|
469
|
-
}
|
|
470
97
|
}
|
|
471
|
-
if (this.inflight.has(key))
|
|
472
|
-
this.
|
|
473
|
-
|
|
98
|
+
if (this.inflight.has(key))
|
|
99
|
+
return this.inflight.get(key).promise;
|
|
100
|
+
const internalController = new AbortController();
|
|
101
|
+
const userSignal = req.getSignal?.();
|
|
102
|
+
const abortHandler = () => internalController.abort();
|
|
103
|
+
if (userSignal) {
|
|
104
|
+
userSignal.addEventListener("abort", abortHandler, { once: true });
|
|
474
105
|
}
|
|
475
|
-
const
|
|
476
|
-
const metrics =
|
|
477
|
-
startTime: Date.now(),
|
|
478
|
-
endTime: 0,
|
|
479
|
-
duration: 0,
|
|
480
|
-
bytesReceived: 0,
|
|
481
|
-
bytesSent: 0,
|
|
482
|
-
retries: 0,
|
|
483
|
-
cached: false,
|
|
484
|
-
url,
|
|
485
|
-
method,
|
|
486
|
-
};
|
|
106
|
+
const executeAction = async () => {
|
|
107
|
+
const metrics = this.createInitialMetrics(url, method);
|
|
487
108
|
try {
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
body instanceof Buffer ? body.length : Buffer.byteLength(body || "");
|
|
494
|
-
if (method === "HEAD") {
|
|
495
|
-
metrics.endTime = Date.now();
|
|
496
|
-
metrics.duration = metrics.endTime - metrics.startTime;
|
|
497
|
-
this.requestMetrics.set(key, metrics);
|
|
498
|
-
this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
|
|
109
|
+
const rawResponse = await this.executor.execute(method, url, headers, body, metrics, internalController.signal);
|
|
110
|
+
const bufferBody = await this.transformer.readBodyWithLimit(rawResponse.body);
|
|
111
|
+
this.metricsManager.recordBytes(bufferBody.length);
|
|
112
|
+
const parsed = await this.transformer.parseResponse({ ...rawResponse, body: bufferBody }, responseType);
|
|
113
|
+
if (method === "HEAD")
|
|
499
114
|
return {
|
|
500
|
-
status:
|
|
501
|
-
headers:
|
|
115
|
+
status: rawResponse.status,
|
|
116
|
+
headers: rawResponse.headers,
|
|
502
117
|
};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
118
|
+
if (method === "GET" &&
|
|
119
|
+
useCache &&
|
|
120
|
+
this.cache &&
|
|
121
|
+
parsed !== undefined) {
|
|
506
122
|
this.cache.set(key, parsed);
|
|
507
|
-
metrics.cached = true;
|
|
508
123
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
metrics.duration = metrics.endTime - metrics.startTime;
|
|
512
|
-
this.requestMetrics.set(key, metrics);
|
|
513
|
-
this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
|
|
514
|
-
return result;
|
|
124
|
+
this.recordSuccess(metrics, rawResponse.status);
|
|
125
|
+
return parsed;
|
|
515
126
|
}
|
|
516
127
|
catch (error) {
|
|
517
|
-
metrics
|
|
518
|
-
metrics.duration = metrics.endTime - metrics.startTime;
|
|
519
|
-
this.requestMetrics.set(key, metrics);
|
|
128
|
+
this.recordError(metrics, error);
|
|
520
129
|
throw error;
|
|
521
130
|
}
|
|
522
131
|
finally {
|
|
132
|
+
if (userSignal)
|
|
133
|
+
userSignal.removeEventListener("abort", abortHandler);
|
|
523
134
|
this.inflight.delete(key);
|
|
524
135
|
}
|
|
525
|
-
}
|
|
526
|
-
this.
|
|
136
|
+
};
|
|
137
|
+
const promise = this.options.enableQueue && this.queue
|
|
138
|
+
? this.queue.enqueue(() => executeAction())
|
|
139
|
+
: executeAction();
|
|
140
|
+
this.inflight.set(key, { promise, controller: internalController });
|
|
527
141
|
return promise;
|
|
528
142
|
}
|
|
529
143
|
/**
|
|
530
|
-
* Performs an HTTP GET request.
|
|
531
|
-
* @
|
|
532
|
-
* @param
|
|
533
|
-
* @
|
|
534
|
-
* @template T The expected response type
|
|
144
|
+
* @en Performs an HTTP GET request.
|
|
145
|
+
* @ru Выполняет HTTP GET запрос.
|
|
146
|
+
* @param req Request URL or Request object
|
|
147
|
+
* @param responseType Expected response format
|
|
535
148
|
*/
|
|
536
|
-
get(req, responseType = "
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
getURL: () => req,
|
|
540
|
-
getBodyData: () => undefined,
|
|
541
|
-
getHeaders: () => ({}),
|
|
542
|
-
};
|
|
543
|
-
return this.requestInternal("GET", simpleReq, true, responseType);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
return this.requestInternal("GET", req, true, responseType);
|
|
547
|
-
}
|
|
149
|
+
get(req, responseType = "auto") {
|
|
150
|
+
const requestObj = this.normalizeRequest(req);
|
|
151
|
+
return this.requestInternal("GET", requestObj, true, responseType);
|
|
548
152
|
}
|
|
549
153
|
/**
|
|
550
|
-
* Performs an HTTP POST request.
|
|
551
|
-
* @
|
|
552
|
-
* @param
|
|
553
|
-
* @
|
|
554
|
-
* @
|
|
154
|
+
* @en Performs an HTTP POST request.
|
|
155
|
+
* @ru Выполняет HTTP POST запрос.
|
|
156
|
+
* @param req Request URL or Request object
|
|
157
|
+
* @param body Request body data
|
|
158
|
+
* @param responseType Expected response format
|
|
555
159
|
*/
|
|
556
|
-
post(req, body, responseType = "
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
getURL: () => req,
|
|
560
|
-
getBodyData: () => body,
|
|
561
|
-
getHeaders: () => ({ "Content-Type": "application/json" }),
|
|
562
|
-
};
|
|
563
|
-
return this.requestInternal("POST", simpleReq, false, responseType);
|
|
564
|
-
}
|
|
565
|
-
else {
|
|
566
|
-
return this.requestInternal("POST", req, false, responseType);
|
|
567
|
-
}
|
|
160
|
+
post(req, body, responseType = "auto") {
|
|
161
|
+
const requestObj = this.normalizeRequest(req, body);
|
|
162
|
+
return this.requestInternal("POST", requestObj, false, responseType);
|
|
568
163
|
}
|
|
569
164
|
/**
|
|
570
|
-
* Performs an HTTP PUT request.
|
|
571
|
-
* @
|
|
572
|
-
* @param responseType Optional response parsing type
|
|
573
|
-
* @returns A promise that resolves to the parsed response
|
|
574
|
-
* @template T The expected response type
|
|
165
|
+
* @en Performs an HTTP PUT request.
|
|
166
|
+
* @ru Выполняет HTTP PUT запрос.
|
|
575
167
|
*/
|
|
576
|
-
put(req, body, responseType = "
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
getURL: () => req,
|
|
580
|
-
getBodyData: () => body,
|
|
581
|
-
getHeaders: () => ({ "Content-Type": "application/json" }),
|
|
582
|
-
};
|
|
583
|
-
return this.requestInternal("PUT", simpleReq, false, responseType);
|
|
584
|
-
}
|
|
585
|
-
return this.requestInternal("PUT", req, false, responseType);
|
|
168
|
+
put(req, body, responseType = "auto") {
|
|
169
|
+
const requestObj = this.normalizeRequest(req, body);
|
|
170
|
+
return this.requestInternal("PUT", requestObj, false, responseType);
|
|
586
171
|
}
|
|
587
172
|
/**
|
|
588
|
-
* Performs an HTTP DELETE request.
|
|
589
|
-
* @
|
|
590
|
-
* @param responseType Optional response parsing type
|
|
591
|
-
* @returns A promise that resolves to the parsed response
|
|
592
|
-
* @template T The expected response type
|
|
173
|
+
* @en Performs an HTTP DELETE request.
|
|
174
|
+
* @ru Выполняет HTTP DELETE запрос.
|
|
593
175
|
*/
|
|
594
|
-
delete(req, responseType = "
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const simpleReq = {
|
|
598
|
-
getURL: () => req,
|
|
599
|
-
getBodyData: () => undefined,
|
|
600
|
-
getHeaders: () => ({}),
|
|
601
|
-
};
|
|
602
|
-
return client.delete(simpleReq, responseType);
|
|
603
|
-
}
|
|
604
|
-
return this.requestInternal("DELETE", req, false, responseType);
|
|
176
|
+
delete(req, responseType = "auto") {
|
|
177
|
+
const requestObj = this.normalizeRequest(req);
|
|
178
|
+
return this.requestInternal("DELETE", requestObj, false, responseType);
|
|
605
179
|
}
|
|
606
180
|
/**
|
|
607
|
-
* Performs an HTTP PATCH request.
|
|
608
|
-
* @
|
|
609
|
-
* @param responseType Optional response parsing type
|
|
610
|
-
* @returns A promise that resolves to the parsed response
|
|
611
|
-
* @template T The expected response type
|
|
181
|
+
* @en Performs an HTTP PATCH request.
|
|
182
|
+
* @ru Выполняет HTTP PATCH запрос.
|
|
612
183
|
*/
|
|
613
|
-
patch(req, body, responseType = "
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
getURL: () => req,
|
|
617
|
-
getBodyData: () => body,
|
|
618
|
-
getHeaders: () => ({ "Content-Type": "application/json" }),
|
|
619
|
-
};
|
|
620
|
-
return this.requestInternal("PATCH", simpleReq, false, responseType);
|
|
621
|
-
}
|
|
622
|
-
return this.requestInternal("PATCH", req, false, responseType);
|
|
184
|
+
patch(req, body, responseType = "auto") {
|
|
185
|
+
const requestObj = this.normalizeRequest(req, body);
|
|
186
|
+
return this.requestInternal("PATCH", requestObj, false, responseType);
|
|
623
187
|
}
|
|
624
188
|
/**
|
|
625
|
-
* @
|
|
626
|
-
* @
|
|
189
|
+
* @en Creates a RequestBuilder for a fluent API approach.
|
|
190
|
+
* @ru Создает RequestBuilder для использования Fluent API.
|
|
191
|
+
* @example client.request('url').get().send();
|
|
627
192
|
*/
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
193
|
+
request(url) {
|
|
194
|
+
return new RequestBuilder_js_1.RequestBuilder(url, this);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* @en Releases all resources, aborts active requests, and closes connections.
|
|
198
|
+
* @ru Освобождает ресурсы клиента, отменяет активные запросы и закрывает соединения.
|
|
199
|
+
*/
|
|
200
|
+
async destroy() {
|
|
201
|
+
if (this.inflight.size > 0) {
|
|
202
|
+
if (this.options.verbose) {
|
|
203
|
+
this.options.logger?.("info", `Aborting ${this.inflight.size} active requests...`);
|
|
204
|
+
}
|
|
205
|
+
for (const { controller } of this.inflight.values()) {
|
|
206
|
+
controller.abort();
|
|
207
|
+
}
|
|
208
|
+
this.inflight.clear();
|
|
636
209
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
body: response.body,
|
|
653
|
-
url,
|
|
654
|
-
};
|
|
655
|
-
}.bind(this))
|
|
656
|
-
: async function () {
|
|
657
|
-
const url = req.getURL();
|
|
658
|
-
const headers = Object.assign({}, this.defaultHeaders, req.getHeaders());
|
|
659
|
-
const response = await (0, undici_1.request)(url, {
|
|
660
|
-
method: "GET",
|
|
661
|
-
headers,
|
|
662
|
-
dispatcher: this.agent,
|
|
663
|
-
});
|
|
664
|
-
return {
|
|
665
|
-
status: response.statusCode,
|
|
666
|
-
headers: response.headers,
|
|
667
|
-
body: response.body,
|
|
668
|
-
url,
|
|
669
|
-
};
|
|
670
|
-
}.bind(this)());
|
|
210
|
+
if (this.agent) {
|
|
211
|
+
try {
|
|
212
|
+
if (typeof this.agent.destroy === "function") {
|
|
213
|
+
await this.agent.destroy();
|
|
214
|
+
}
|
|
215
|
+
else if (typeof this.agent.close === "function") {
|
|
216
|
+
await this.agent.close();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
/* ignore */
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (this.cache)
|
|
224
|
+
this.cache.clear();
|
|
671
225
|
}
|
|
672
226
|
/**
|
|
673
|
-
* Performs an HTTP HEAD request.
|
|
674
|
-
* @
|
|
675
|
-
* @returns A promise that resolves when the request completes
|
|
227
|
+
* @en Performs an HTTP HEAD request.
|
|
228
|
+
* @ru Выполняет HTTP HEAD запрос.
|
|
676
229
|
*/
|
|
677
230
|
async head(req) {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
231
|
+
const requestObj = this.normalizeRequest(req);
|
|
232
|
+
return this.requestInternal("HEAD", requestObj, false);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* @en Executes a request and returns an AsyncIterable stream.
|
|
236
|
+
* @ru Выполняет запрос и возвращает итерируемый поток данных.
|
|
237
|
+
*/
|
|
238
|
+
async stream(req) {
|
|
239
|
+
const requestObj = this.normalizeRequest(req);
|
|
240
|
+
const url = requestObj.getURL();
|
|
241
|
+
const { body, headers } = this.prepareRequestData("GET", requestObj);
|
|
242
|
+
const key = `STREAM:GET:${url}`;
|
|
243
|
+
const internalController = new AbortController();
|
|
244
|
+
const userSignal = requestObj.getSignal?.();
|
|
245
|
+
const abortHandler = () => internalController.abort();
|
|
246
|
+
if (userSignal) {
|
|
247
|
+
userSignal.addEventListener("abort", abortHandler, { once: true });
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const rawResponse = await this.executor.execute("GET", url, headers, body, undefined, internalController.signal);
|
|
251
|
+
this.inflight.set(key, {
|
|
252
|
+
promise: Promise.resolve(),
|
|
253
|
+
controller: internalController,
|
|
254
|
+
});
|
|
255
|
+
const cleanup = () => {
|
|
256
|
+
if (userSignal)
|
|
257
|
+
userSignal.removeEventListener("abort", abortHandler);
|
|
258
|
+
this.inflight.delete(key);
|
|
259
|
+
};
|
|
260
|
+
rawResponse.body.on("close", cleanup);
|
|
261
|
+
rawResponse.body.on("error", cleanup);
|
|
262
|
+
return {
|
|
263
|
+
status: rawResponse.status,
|
|
264
|
+
headers: rawResponse.headers,
|
|
265
|
+
body: rawResponse.body,
|
|
266
|
+
url: rawResponse.url,
|
|
683
267
|
};
|
|
684
|
-
return this.requestInternal("HEAD", simpleReq, false);
|
|
685
268
|
}
|
|
686
|
-
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (userSignal)
|
|
271
|
+
userSignal.removeEventListener("abort", abortHandler);
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
687
274
|
}
|
|
688
275
|
/**
|
|
689
|
-
* Clears the
|
|
276
|
+
* @en Clears the internal cache.
|
|
277
|
+
* @ru Полностью очищает внутренний кэш клиента.
|
|
690
278
|
*/
|
|
691
|
-
|
|
279
|
+
clearCache() {
|
|
692
280
|
if (this.cache) {
|
|
693
|
-
|
|
694
|
-
this.log("info", "Cache cleared");
|
|
281
|
+
this.cache.clear();
|
|
695
282
|
}
|
|
696
283
|
}
|
|
697
284
|
/**
|
|
698
|
-
* Clears all collected
|
|
699
|
-
*
|
|
285
|
+
* @en Clears all collected performance metrics.
|
|
286
|
+
* @ru Очищает все собранные метрики производительности.
|
|
700
287
|
*/
|
|
701
288
|
clearMetrics() {
|
|
702
|
-
this.
|
|
703
|
-
this.
|
|
289
|
+
this.metricsManager.clear();
|
|
290
|
+
this.options.logger?.("info", "Metrics cleared");
|
|
704
291
|
}
|
|
705
292
|
/**
|
|
706
|
-
* Retrieves metrics for a specific
|
|
707
|
-
* @
|
|
708
|
-
* @returns Metrics object if found, undefined otherwise
|
|
293
|
+
* @en Retrieves metrics for a specific URL.
|
|
294
|
+
* @ru Получает метрики для конкретного URL.
|
|
709
295
|
*/
|
|
710
296
|
getMetrics(key) {
|
|
711
|
-
return this.
|
|
297
|
+
return this.metricsManager.get(key);
|
|
712
298
|
}
|
|
713
299
|
/**
|
|
714
|
-
* Retrieves all
|
|
715
|
-
* @
|
|
300
|
+
* @en Retrieves all stored request metrics.
|
|
301
|
+
* @ru Получает список всех сохраненных метрик.
|
|
716
302
|
*/
|
|
717
303
|
getAllMetrics() {
|
|
718
|
-
return Array.from(this.
|
|
304
|
+
return Array.from(this.metricsManager.getAll());
|
|
719
305
|
}
|
|
720
306
|
/**
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
* @
|
|
724
|
-
* @returns RequestBuilder instance for chaining
|
|
725
|
-
*/
|
|
726
|
-
request(url) {
|
|
727
|
-
return new RequestBuilder_1.RequestBuilder(url);
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Returns current statistics about the HTTP client's state.
|
|
731
|
-
* @returns An object containing cache size, request counts, and rate limit information
|
|
307
|
+
* @en Returns real-time statistics about the client's internal state.
|
|
308
|
+
* @ru Возвращает статистику состояния клиента в реальном времени.
|
|
309
|
+
* @returns Cache size, active requests, queue state, etc.
|
|
732
310
|
*/
|
|
733
311
|
getStats() {
|
|
734
312
|
return {
|
|
735
313
|
cacheSize: this.cache?.size ?? 0,
|
|
736
314
|
inflightRequests: this.inflight.size,
|
|
737
|
-
queuedRequests: this.
|
|
315
|
+
queuedRequests: this.options.enableQueue && this.queue
|
|
738
316
|
? (this.queue.queuedCount ?? 0)
|
|
739
317
|
: 0,
|
|
740
|
-
activeRequests: this.
|
|
318
|
+
activeRequests: this.options.enableQueue && this.queue
|
|
741
319
|
? (this.queue.activeCount ?? 0)
|
|
742
320
|
: 0,
|
|
743
|
-
currentRateLimit: this.
|
|
321
|
+
currentRateLimit: this.options.enableRateLimit && this.limiter
|
|
744
322
|
? (this.limiter.currentCount ?? 0)
|
|
745
323
|
: 0,
|
|
746
324
|
};
|
|
747
325
|
}
|
|
326
|
+
normalizeRequest(req, body) {
|
|
327
|
+
if (typeof req === "string") {
|
|
328
|
+
return {
|
|
329
|
+
getURL: () => req,
|
|
330
|
+
getBodyData: () => body,
|
|
331
|
+
getHeaders: () => ({}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return req;
|
|
335
|
+
}
|
|
336
|
+
applyDefaultOptions(opt) {
|
|
337
|
+
const defaults = {
|
|
338
|
+
timeout: 30000,
|
|
339
|
+
maxRetries: 3,
|
|
340
|
+
followRedirects: true,
|
|
341
|
+
maxRedirects: 5,
|
|
342
|
+
userAgent: "Hyperttp/2.0",
|
|
343
|
+
maxResponseBytes: 10 * 1024 * 1024, // 10MB
|
|
344
|
+
cacheTTL: 1000 * 60 * 5,
|
|
345
|
+
cacheMaxSize: 500,
|
|
346
|
+
enableCache: true,
|
|
347
|
+
enableQueue: true,
|
|
348
|
+
enableRateLimit: true,
|
|
349
|
+
retryOptions: {
|
|
350
|
+
maxRetries: 3,
|
|
351
|
+
baseDelay: 1000,
|
|
352
|
+
maxDelay: 10000,
|
|
353
|
+
retryStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
354
|
+
jitter: true,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
return { ...defaults, ...opt };
|
|
358
|
+
}
|
|
359
|
+
prepareRequestData(method, req) {
|
|
360
|
+
const headers = { ...this.defaultHeaders, ...req.getHeaders() };
|
|
361
|
+
let rawBody = req.getBodyData();
|
|
362
|
+
const methodsWithBody = ["POST", "PUT", "PATCH", "DELETE"];
|
|
363
|
+
if (!methodsWithBody.includes(method)) {
|
|
364
|
+
return { body: undefined, headers };
|
|
365
|
+
}
|
|
366
|
+
if (rawBody &&
|
|
367
|
+
typeof rawBody === "object" &&
|
|
368
|
+
!(rawBody instanceof Buffer)) {
|
|
369
|
+
const contentType = (headers["content-type"] ||
|
|
370
|
+
headers["Content-Type"] ||
|
|
371
|
+
"").toLowerCase();
|
|
372
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
373
|
+
const params = new URLSearchParams();
|
|
374
|
+
for (const [key, value] of Object.entries(rawBody)) {
|
|
375
|
+
const finalValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
376
|
+
params.append(key, finalValue);
|
|
377
|
+
}
|
|
378
|
+
rawBody = params.toString();
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
try {
|
|
382
|
+
rawBody = JSON.stringify(rawBody);
|
|
383
|
+
if (!headers["content-type"]) {
|
|
384
|
+
headers["content-type"] = "application/json; charset=utf-8";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
this.options.logger?.("error", `Serialization failed for ${method}`, {
|
|
389
|
+
url: req.getURL(),
|
|
390
|
+
});
|
|
391
|
+
rawBody = String(rawBody);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
body: rawBody === null || rawBody === undefined ? undefined : rawBody,
|
|
397
|
+
headers,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
createInitialMetrics(url, method) {
|
|
401
|
+
return {
|
|
402
|
+
startTime: Date.now(),
|
|
403
|
+
endTime: 0,
|
|
404
|
+
duration: 0,
|
|
405
|
+
bytesReceived: 0,
|
|
406
|
+
bytesSent: 0,
|
|
407
|
+
retries: 0,
|
|
408
|
+
cached: false,
|
|
409
|
+
url,
|
|
410
|
+
method,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
recordSuccess(metrics, status) {
|
|
414
|
+
metrics.endTime = Date.now();
|
|
415
|
+
metrics.duration = metrics.endTime - metrics.startTime;
|
|
416
|
+
metrics.statusCode = status;
|
|
417
|
+
this.metricsManager.record(metrics);
|
|
418
|
+
if (this.options.verbose && this.options.logger) {
|
|
419
|
+
this.options.logger("info", `Request successful: ${metrics.method} ${metrics.url}`, {
|
|
420
|
+
duration: metrics.duration,
|
|
421
|
+
status: metrics.statusCode,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
recordError(metrics, error) {
|
|
426
|
+
metrics.endTime = Date.now();
|
|
427
|
+
metrics.duration = metrics.endTime - metrics.startTime;
|
|
428
|
+
metrics.statusCode = error.statusCode || 0;
|
|
429
|
+
this.metricsManager.record(metrics);
|
|
430
|
+
if (this.options.verbose && this.options.logger) {
|
|
431
|
+
this.options.logger("error", `Request failed: ${metrics.method} ${metrics.url}`, {
|
|
432
|
+
error: error.message,
|
|
433
|
+
code: error.code,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
748
437
|
}
|
|
749
438
|
exports.default = HttpClientImproved;
|
|
750
439
|
//# sourceMappingURL=HttpClientImproved.js.map
|