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