hyperttp 0.2.1 → 0.2.2
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 +16 -3
- package/dist/Hyperttp/Core/CacheManager.d.ts.map +1 -1
- package/dist/Hyperttp/Core/CacheManager.js +45 -13
- package/dist/Hyperttp/Core/CacheManager.js.map +1 -1
- package/dist/Hyperttp/Core/HttpClientImproved.d.ts +5 -3
- package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
- package/dist/Hyperttp/Core/HttpClientImproved.js +164 -157
- package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
- package/dist/Hyperttp/Core/MetricsManager.d.ts +32 -0
- package/dist/Hyperttp/Core/MetricsManager.d.ts.map +1 -0
- package/dist/Hyperttp/Core/MetricsManager.js +96 -0
- package/dist/Hyperttp/Core/MetricsManager.js.map +1 -0
- package/dist/Hyperttp/Core/QueueManager.d.ts.map +1 -1
- package/dist/Hyperttp/Core/QueueManager.js +12 -5
- package/dist/Hyperttp/Core/QueueManager.js.map +1 -1
- package/dist/Hyperttp/Core/RateLimiter.d.ts +6 -36
- package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
- package/dist/Hyperttp/Core/RateLimiter.js +41 -58
- package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
- package/dist/Hyperttp/Core/RequestBuilder.d.ts +17 -4
- package/dist/Hyperttp/Core/RequestBuilder.d.ts.map +1 -1
- package/dist/Hyperttp/Core/RequestBuilder.js +41 -18
- package/dist/Hyperttp/Core/RequestBuilder.js.map +1 -1
- package/dist/Hyperttp/Core/index.d.ts +4 -1
- package/dist/Hyperttp/Core/index.d.ts.map +1 -1
- package/dist/Hyperttp/Core/index.js +11 -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 +1 -1
|
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
const tough_cookie_1 = require("tough-cookie");
|
|
37
40
|
const undici_1 = require("undici");
|
|
@@ -39,11 +42,13 @@ const undici_2 = require("http-cookie-agent/undici");
|
|
|
39
42
|
const zlib = __importStar(require("zlib"));
|
|
40
43
|
const util_1 = require("util");
|
|
41
44
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
45
|
+
const fast_xml_builder_1 = __importDefault(require("fast-xml-builder"));
|
|
42
46
|
const CacheManager_1 = require("./CacheManager");
|
|
43
47
|
const QueueManager_1 = require("./QueueManager");
|
|
44
48
|
const RateLimiter_1 = require("./RateLimiter");
|
|
45
49
|
const Types_1 = require("../../Types");
|
|
46
50
|
const RequestBuilder_1 = require("./RequestBuilder");
|
|
51
|
+
const MetricsManager_1 = require("./MetricsManager");
|
|
47
52
|
const gunzip = (0, util_1.promisify)(zlib.gunzip);
|
|
48
53
|
const inflate = (0, util_1.promisify)(zlib.inflate);
|
|
49
54
|
const brotliDecompress = (0, util_1.promisify)(zlib.brotliDecompress);
|
|
@@ -63,7 +68,7 @@ class HttpClientImproved {
|
|
|
63
68
|
options;
|
|
64
69
|
requestInterceptors = [];
|
|
65
70
|
responseInterceptors = [];
|
|
66
|
-
|
|
71
|
+
metricsManager;
|
|
67
72
|
/**
|
|
68
73
|
* Creates a new instance of HttpClientImproved.
|
|
69
74
|
* @param options Optional configuration options for the HTTP client
|
|
@@ -100,6 +105,9 @@ class HttpClientImproved {
|
|
|
100
105
|
enableRateLimit: options?.enableRateLimit ?? false,
|
|
101
106
|
enableCache: options?.enableCache ?? true,
|
|
102
107
|
};
|
|
108
|
+
this.metricsManager = new MetricsManager_1.MetricsManager({
|
|
109
|
+
maxHistory: this.options.maxMetricsSize,
|
|
110
|
+
});
|
|
103
111
|
if (this.options.enableCache) {
|
|
104
112
|
this.cache = new CacheManager_1.CacheManager({
|
|
105
113
|
cacheTTL: this.options.cacheTTL,
|
|
@@ -126,14 +134,11 @@ class HttpClientImproved {
|
|
|
126
134
|
"Accept-Encoding": "gzip, deflate, br",
|
|
127
135
|
"User-Agent": this.options.userAgent ?? "Hyperttp/0.1.0 Node.js",
|
|
128
136
|
};
|
|
129
|
-
this.agent = new
|
|
137
|
+
this.agent = new undici_2.CookieAgent({
|
|
130
138
|
connections: 1000,
|
|
131
139
|
pipelining: 10,
|
|
132
140
|
keepAliveTimeout: 60000,
|
|
133
141
|
keepAliveMaxTimeout: 600000,
|
|
134
|
-
interceptors: {
|
|
135
|
-
Client: [(0, undici_2.cookie)({ jar: this.cookieJar })],
|
|
136
|
-
},
|
|
137
142
|
});
|
|
138
143
|
}
|
|
139
144
|
/**
|
|
@@ -164,9 +169,14 @@ class HttpClientImproved {
|
|
|
164
169
|
addResponseInterceptor(interceptor) {
|
|
165
170
|
this.responseInterceptors.push(interceptor);
|
|
166
171
|
}
|
|
167
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* @ru Закрывает агент и освобождает ресурсы (keep-alive соединения).
|
|
174
|
+
* @en Closes the HTTP agent and terminates keep-alive connections.
|
|
175
|
+
*/
|
|
168
176
|
close() {
|
|
169
|
-
this.agent.
|
|
177
|
+
if (this.agent && typeof this.agent.destroy === "function") {
|
|
178
|
+
this.agent.destroy();
|
|
179
|
+
}
|
|
170
180
|
}
|
|
171
181
|
log(level, msg, meta) {
|
|
172
182
|
if (this.options.verbose) {
|
|
@@ -238,16 +248,34 @@ class HttpClientImproved {
|
|
|
238
248
|
return undefined;
|
|
239
249
|
}
|
|
240
250
|
async readBodyWithLimit(body) {
|
|
241
|
-
const buf = Buffer.from(await body.arrayBuffer());
|
|
242
251
|
const limit = this.options.maxResponseBytes;
|
|
243
|
-
|
|
244
|
-
|
|
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));
|
|
245
262
|
}
|
|
246
|
-
return
|
|
263
|
+
return Buffer.concat(chunks);
|
|
247
264
|
}
|
|
248
|
-
async sendWithRetry(method, url, headers, body, metrics, redirects = 0) {
|
|
265
|
+
async sendWithRetry(method, url, headers, body, metrics, signal, redirects = 0) {
|
|
249
266
|
let lastError;
|
|
250
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
|
+
}
|
|
251
279
|
try {
|
|
252
280
|
if (this.limiter && this.options.enableRateLimit) {
|
|
253
281
|
await this.limiter.wait();
|
|
@@ -258,16 +286,13 @@ class HttpClientImproved {
|
|
|
258
286
|
headers,
|
|
259
287
|
body,
|
|
260
288
|
});
|
|
261
|
-
const controller = new AbortController();
|
|
262
|
-
const timeout = this.options.timeout ?? 15000;
|
|
263
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
264
289
|
try {
|
|
265
290
|
const res = await (0, undici_1.request)(finalConfig.url, {
|
|
266
291
|
method: finalConfig.method,
|
|
267
292
|
headers: finalConfig.headers,
|
|
268
293
|
body: finalConfig.body,
|
|
269
294
|
dispatcher: this.agent,
|
|
270
|
-
signal:
|
|
295
|
+
signal: timeoutController.signal,
|
|
271
296
|
});
|
|
272
297
|
clearTimeout(timer);
|
|
273
298
|
const buf = await this.readBodyWithLimit(res.body);
|
|
@@ -277,7 +302,6 @@ class HttpClientImproved {
|
|
|
277
302
|
body: buf,
|
|
278
303
|
url: finalConfig.url,
|
|
279
304
|
});
|
|
280
|
-
// Redirects
|
|
281
305
|
if (this.options.followRedirects &&
|
|
282
306
|
[301, 302, 303, 307, 308].includes(response.status) &&
|
|
283
307
|
redirects < (this.options.maxRedirects ?? 5)) {
|
|
@@ -287,7 +311,6 @@ class HttpClientImproved {
|
|
|
287
311
|
const redirectMethod = response.status === 303 ? "GET" : method;
|
|
288
312
|
const nextHeaders = { ...headers };
|
|
289
313
|
let nextBody = body;
|
|
290
|
-
// If switching to GET, drop body-related headers.
|
|
291
314
|
if (redirectMethod === "GET") {
|
|
292
315
|
nextBody = undefined;
|
|
293
316
|
delete nextHeaders["content-type"];
|
|
@@ -295,20 +318,15 @@ class HttpClientImproved {
|
|
|
295
318
|
delete nextHeaders["content-length"];
|
|
296
319
|
delete nextHeaders["Content-Length"];
|
|
297
320
|
}
|
|
298
|
-
this.log("debug", `Redirecting to ${nextUrl}
|
|
299
|
-
|
|
300
|
-
status: response.status,
|
|
301
|
-
});
|
|
302
|
-
return this.sendWithRetry(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, redirects + 1);
|
|
321
|
+
this.log("debug", `Redirecting to ${nextUrl}`);
|
|
322
|
+
return this.sendWithRetry(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, signal, redirects + 1);
|
|
303
323
|
}
|
|
304
324
|
}
|
|
305
|
-
// Retry by status
|
|
306
325
|
if (this.retryOptions.retryStatusCodes.includes(response.status)) {
|
|
307
326
|
metrics && (metrics.retries += 1);
|
|
308
327
|
if (response.status === 429) {
|
|
309
328
|
const ra = this.parseRetryAfterMs(response.headers["retry-after"]);
|
|
310
329
|
if (ra !== undefined) {
|
|
311
|
-
this.log("warn", `429 Rate limited, waiting Retry-After ${ra}ms`, { url: finalConfig.url });
|
|
312
330
|
if (attempt < this.retryOptions.maxRetries) {
|
|
313
331
|
await this.sleep(ra);
|
|
314
332
|
continue;
|
|
@@ -316,10 +334,6 @@ class HttpClientImproved {
|
|
|
316
334
|
throw new Types_1.RateLimitError(finalConfig.url, ra);
|
|
317
335
|
}
|
|
318
336
|
}
|
|
319
|
-
this.log("warn", `Retrying ${method} ${finalConfig.url} due to status ${response.status}`, {
|
|
320
|
-
attempt: attempt + 1,
|
|
321
|
-
maxRetries: this.retryOptions.maxRetries,
|
|
322
|
-
});
|
|
323
337
|
if (attempt < this.retryOptions.maxRetries) {
|
|
324
338
|
await this.sleep(this.calcDelay(attempt));
|
|
325
339
|
continue;
|
|
@@ -327,114 +341,110 @@ class HttpClientImproved {
|
|
|
327
341
|
}
|
|
328
342
|
return response;
|
|
329
343
|
}
|
|
330
|
-
catch (
|
|
331
|
-
clearTimeout(timer);
|
|
332
|
-
if (timeoutErr?.name === "AbortError")
|
|
333
|
-
throw new Types_1.TimeoutError(url, timeout);
|
|
334
|
-
throw timeoutErr;
|
|
335
|
-
}
|
|
336
|
-
finally {
|
|
344
|
+
catch (innerErr) {
|
|
337
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;
|
|
338
355
|
}
|
|
339
356
|
}
|
|
340
357
|
catch (err) {
|
|
341
358
|
lastError = err;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
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}`);
|
|
346
367
|
metrics && (metrics.retries += 1);
|
|
347
368
|
if (attempt < this.retryOptions.maxRetries) {
|
|
348
369
|
await this.sleep(this.calcDelay(attempt));
|
|
349
370
|
continue;
|
|
350
371
|
}
|
|
351
372
|
}
|
|
373
|
+
finally {
|
|
374
|
+
clearTimeout(timer);
|
|
375
|
+
if (signal) {
|
|
376
|
+
signal.removeEventListener("abort", abortHandler);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
352
379
|
}
|
|
353
380
|
if (lastError instanceof Types_1.HttpClientError)
|
|
354
381
|
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 };
|
|
382
|
+
throw new Types_1.HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, "REQUEST_FAILED", undefined, lastError instanceof Error ? lastError : undefined, url, method);
|
|
384
383
|
}
|
|
385
384
|
xmlParser = new fast_xml_parser_1.XMLParser({
|
|
386
385
|
ignoreAttributes: false,
|
|
387
386
|
allowBooleanAttributes: true,
|
|
388
387
|
});
|
|
389
|
-
async parseResponse(res, responseType) {
|
|
388
|
+
async parseResponse(res, responseType = "auto") {
|
|
390
389
|
try {
|
|
391
|
-
const contentType = res.headers["content-type"] || "";
|
|
392
390
|
const text = await this.decompress(res.body, res.headers["content-encoding"]);
|
|
393
|
-
const
|
|
394
|
-
switch (
|
|
391
|
+
const trimmed = text.trim();
|
|
392
|
+
switch (responseType) {
|
|
395
393
|
case "json": {
|
|
396
|
-
if (
|
|
397
|
-
return JSON.parse(
|
|
394
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
395
|
+
return JSON.parse(trimmed);
|
|
398
396
|
}
|
|
399
|
-
if (
|
|
400
|
-
return this.xmlParser.parse(
|
|
397
|
+
if (trimmed.startsWith("<")) {
|
|
398
|
+
return this.xmlParser.parse(trimmed);
|
|
401
399
|
}
|
|
402
|
-
return {
|
|
400
|
+
return { data: trimmed };
|
|
403
401
|
}
|
|
404
402
|
case "xml": {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return text;
|
|
408
|
-
}
|
|
403
|
+
if (trimmed.startsWith("<"))
|
|
404
|
+
return trimmed;
|
|
409
405
|
try {
|
|
410
|
-
const
|
|
411
|
-
|
|
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);
|
|
412
413
|
}
|
|
413
414
|
catch {
|
|
414
|
-
return
|
|
415
|
+
return text;
|
|
415
416
|
}
|
|
416
417
|
}
|
|
417
418
|
case "text":
|
|
418
419
|
return text;
|
|
419
420
|
case "buffer":
|
|
420
421
|
return res.body;
|
|
421
|
-
case "
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
}
|
|
424
435
|
return text;
|
|
436
|
+
}
|
|
425
437
|
}
|
|
426
438
|
}
|
|
427
|
-
catch (
|
|
428
|
-
|
|
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);
|
|
439
|
+
catch (err) {
|
|
440
|
+
throw new Types_1.HttpClientError(`Parsing failed: ${err?.message ?? String(err)}`, "PARSING_ERROR", res.status);
|
|
434
441
|
}
|
|
435
442
|
}
|
|
436
443
|
async requestInternal(method, req, useCache = true, responseType) {
|
|
437
444
|
const url = req.getURL();
|
|
445
|
+
if (this.metricsManager.isCircuitOpen(url)) {
|
|
446
|
+
throw new Types_1.HttpClientError(`Circuit Breaker is OPEN for host: ${new URL(url).host}`, "CIRCUIT_OPEN", 503, undefined, url, method);
|
|
447
|
+
}
|
|
438
448
|
const rawBody = req.getBodyData();
|
|
439
449
|
const headers = {
|
|
440
450
|
...this.defaultHeaders,
|
|
@@ -455,16 +465,15 @@ class HttpClientImproved {
|
|
|
455
465
|
}
|
|
456
466
|
else {
|
|
457
467
|
body = JSON.stringify(rawBody);
|
|
458
|
-
if (!contentType)
|
|
468
|
+
if (!contentType)
|
|
459
469
|
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
460
|
-
}
|
|
461
470
|
}
|
|
462
471
|
}
|
|
463
472
|
const key = `${method}:${url}:${body ?? ""}`;
|
|
464
473
|
if (method === "GET" && useCache && this.cache) {
|
|
465
474
|
const cached = await this.cache.get(key);
|
|
466
475
|
if (cached) {
|
|
467
|
-
this.log("debug", `
|
|
476
|
+
this.log("debug", `Memory cache hit for ${url}`);
|
|
468
477
|
return cached;
|
|
469
478
|
}
|
|
470
479
|
}
|
|
@@ -472,7 +481,11 @@ class HttpClientImproved {
|
|
|
472
481
|
this.log("debug", `Deduplicating request for ${url}`);
|
|
473
482
|
return this.inflight.get(key);
|
|
474
483
|
}
|
|
475
|
-
const
|
|
484
|
+
const signal = req.getSignal?.();
|
|
485
|
+
if (signal?.aborted) {
|
|
486
|
+
throw new Types_1.HttpClientError("Aborted before execution", "ABORTED", 0, undefined, url, method);
|
|
487
|
+
}
|
|
488
|
+
const executeRequest = async () => {
|
|
476
489
|
const metrics = {
|
|
477
490
|
startTime: Date.now(),
|
|
478
491
|
endTime: 0,
|
|
@@ -485,44 +498,36 @@ class HttpClientImproved {
|
|
|
485
498
|
method,
|
|
486
499
|
};
|
|
487
500
|
try {
|
|
488
|
-
|
|
489
|
-
const res = await this.sendWithRetry(method, url, headers, body, metrics);
|
|
490
|
-
metrics.statusCode = res.status;
|
|
491
|
-
metrics.bytesReceived = res.body.length;
|
|
492
|
-
metrics.bytesSent =
|
|
493
|
-
body instanceof Buffer ? body.length : Buffer.byteLength(body || "");
|
|
501
|
+
const res = await this.sendWithRetry(method, url, headers, body, metrics, signal);
|
|
494
502
|
if (method === "HEAD") {
|
|
495
503
|
metrics.endTime = Date.now();
|
|
496
504
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
497
|
-
this.
|
|
498
|
-
|
|
499
|
-
return {
|
|
500
|
-
status: res.status,
|
|
501
|
-
headers: res.headers,
|
|
502
|
-
};
|
|
505
|
+
this.metricsManager.record(metrics);
|
|
506
|
+
return { status: res.status, headers: res.headers };
|
|
503
507
|
}
|
|
504
508
|
const parsed = await this.parseResponse(res, responseType);
|
|
505
509
|
if (method === "GET" && useCache && this.cache) {
|
|
506
510
|
this.cache.set(key, parsed);
|
|
507
|
-
metrics.cached = true;
|
|
508
511
|
}
|
|
509
|
-
result = parsed;
|
|
510
512
|
metrics.endTime = Date.now();
|
|
511
513
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
512
|
-
|
|
513
|
-
this.
|
|
514
|
-
return
|
|
514
|
+
metrics.statusCode = res.status;
|
|
515
|
+
this.metricsManager.record(metrics);
|
|
516
|
+
return parsed;
|
|
515
517
|
}
|
|
516
518
|
catch (error) {
|
|
517
519
|
metrics.endTime = Date.now();
|
|
518
520
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
519
|
-
this.
|
|
521
|
+
this.metricsManager.record(metrics);
|
|
520
522
|
throw error;
|
|
521
523
|
}
|
|
522
524
|
finally {
|
|
523
525
|
this.inflight.delete(key);
|
|
524
526
|
}
|
|
525
|
-
}
|
|
527
|
+
};
|
|
528
|
+
const promise = this.options.enableQueue
|
|
529
|
+
? this.queue.enqueue(() => executeRequest())
|
|
530
|
+
: executeRequest();
|
|
526
531
|
this.inflight.set(key, promise);
|
|
527
532
|
return promise;
|
|
528
533
|
}
|
|
@@ -533,7 +538,7 @@ class HttpClientImproved {
|
|
|
533
538
|
* @returns A promise that resolves to the parsed response
|
|
534
539
|
* @template T The expected response type
|
|
535
540
|
*/
|
|
536
|
-
get(req, responseType = "
|
|
541
|
+
get(req, responseType = "auto") {
|
|
537
542
|
if (typeof req === "string") {
|
|
538
543
|
const simpleReq = {
|
|
539
544
|
getURL: () => req,
|
|
@@ -553,7 +558,7 @@ class HttpClientImproved {
|
|
|
553
558
|
* @returns A promise that resolves to the parsed response
|
|
554
559
|
* @template T The expected response type
|
|
555
560
|
*/
|
|
556
|
-
post(req, body, responseType = "
|
|
561
|
+
post(req, body, responseType = "auto") {
|
|
557
562
|
if (typeof req === "string") {
|
|
558
563
|
const simpleReq = {
|
|
559
564
|
getURL: () => req,
|
|
@@ -573,7 +578,7 @@ class HttpClientImproved {
|
|
|
573
578
|
* @returns A promise that resolves to the parsed response
|
|
574
579
|
* @template T The expected response type
|
|
575
580
|
*/
|
|
576
|
-
put(req, body, responseType = "
|
|
581
|
+
put(req, body, responseType = "auto") {
|
|
577
582
|
if (typeof req === "string") {
|
|
578
583
|
const simpleReq = {
|
|
579
584
|
getURL: () => req,
|
|
@@ -591,15 +596,14 @@ class HttpClientImproved {
|
|
|
591
596
|
* @returns A promise that resolves to the parsed response
|
|
592
597
|
* @template T The expected response type
|
|
593
598
|
*/
|
|
594
|
-
delete(req, responseType = "
|
|
599
|
+
delete(req, responseType = "auto") {
|
|
595
600
|
if (typeof req === "string") {
|
|
596
|
-
const client = new HttpClientImproved();
|
|
597
601
|
const simpleReq = {
|
|
598
602
|
getURL: () => req,
|
|
599
603
|
getBodyData: () => undefined,
|
|
600
604
|
getHeaders: () => ({}),
|
|
601
605
|
};
|
|
602
|
-
return
|
|
606
|
+
return this.requestInternal("DELETE", simpleReq, false, responseType);
|
|
603
607
|
}
|
|
604
608
|
return this.requestInternal("DELETE", req, false, responseType);
|
|
605
609
|
}
|
|
@@ -610,7 +614,7 @@ class HttpClientImproved {
|
|
|
610
614
|
* @returns A promise that resolves to the parsed response
|
|
611
615
|
* @template T The expected response type
|
|
612
616
|
*/
|
|
613
|
-
patch(req, body, responseType = "
|
|
617
|
+
patch(req, body, responseType = "auto") {
|
|
614
618
|
if (typeof req === "string") {
|
|
615
619
|
const simpleReq = {
|
|
616
620
|
getURL: () => req,
|
|
@@ -625,41 +629,33 @@ class HttpClientImproved {
|
|
|
625
629
|
* @ru Получает потоковый ответ (для SSE, больших файлов).
|
|
626
630
|
* @en Gets streaming response (for SSE, large files).
|
|
627
631
|
*/
|
|
628
|
-
stream(req) {
|
|
629
|
-
|
|
630
|
-
|
|
632
|
+
async stream(req) {
|
|
633
|
+
const requestObj = typeof req === "string"
|
|
634
|
+
? {
|
|
631
635
|
getURL: () => req,
|
|
632
636
|
getBodyData: () => undefined,
|
|
633
637
|
getHeaders: () => ({}),
|
|
634
|
-
|
|
635
|
-
|
|
638
|
+
getSignal: () => undefined,
|
|
639
|
+
}
|
|
640
|
+
: req;
|
|
641
|
+
const url = requestObj.getURL();
|
|
642
|
+
const signal = requestObj.getSignal?.();
|
|
643
|
+
if (signal?.aborted) {
|
|
644
|
+
throw new Types_1.HttpClientError("Request aborted before execution", "ABORTED", 0, undefined, url, "GET");
|
|
636
645
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
};
|
|
644
|
-
const response = await (0, undici_1.request)(url, {
|
|
645
|
-
method: "GET",
|
|
646
|
-
headers,
|
|
647
|
-
dispatcher: this.agent,
|
|
648
|
-
});
|
|
649
|
-
return {
|
|
650
|
-
status: response.statusCode,
|
|
651
|
-
headers: response.headers,
|
|
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());
|
|
646
|
+
const executeStream = async () => {
|
|
647
|
+
const headers = {
|
|
648
|
+
...this.defaultHeaders,
|
|
649
|
+
...requestObj.getHeaders(),
|
|
650
|
+
};
|
|
651
|
+
try {
|
|
659
652
|
const response = await (0, undici_1.request)(url, {
|
|
660
653
|
method: "GET",
|
|
661
654
|
headers,
|
|
662
655
|
dispatcher: this.agent,
|
|
656
|
+
signal,
|
|
657
|
+
bodyTimeout: this.options.timeout,
|
|
658
|
+
headersTimeout: this.options.timeout,
|
|
663
659
|
});
|
|
664
660
|
return {
|
|
665
661
|
status: response.statusCode,
|
|
@@ -667,7 +663,18 @@ class HttpClientImproved {
|
|
|
667
663
|
body: response.body,
|
|
668
664
|
url,
|
|
669
665
|
};
|
|
670
|
-
}
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
if (err.name === "AbortError") {
|
|
669
|
+
throw new Types_1.HttpClientError("Stream aborted by user", "ABORTED", 0, err, url, "GET");
|
|
670
|
+
}
|
|
671
|
+
throw err;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
if (this.queue && this.options.enableQueue) {
|
|
675
|
+
return this.queue.enqueue(() => executeStream());
|
|
676
|
+
}
|
|
677
|
+
return executeStream();
|
|
671
678
|
}
|
|
672
679
|
/**
|
|
673
680
|
* Performs an HTTP HEAD request.
|
|
@@ -699,7 +706,7 @@ class HttpClientImproved {
|
|
|
699
706
|
* Removes performance and timing data from memory.
|
|
700
707
|
*/
|
|
701
708
|
clearMetrics() {
|
|
702
|
-
this.
|
|
709
|
+
this.metricsManager.clear();
|
|
703
710
|
this.log("info", "Metrics cleared");
|
|
704
711
|
}
|
|
705
712
|
/**
|
|
@@ -708,14 +715,14 @@ class HttpClientImproved {
|
|
|
708
715
|
* @returns Metrics object if found, undefined otherwise
|
|
709
716
|
*/
|
|
710
717
|
getMetrics(key) {
|
|
711
|
-
return this.
|
|
718
|
+
return this.metricsManager.get(key);
|
|
712
719
|
}
|
|
713
720
|
/**
|
|
714
721
|
* Retrieves all collected request metrics.
|
|
715
722
|
* @returns Array of all metrics objects
|
|
716
723
|
*/
|
|
717
724
|
getAllMetrics() {
|
|
718
|
-
return Array.from(this.
|
|
725
|
+
return Array.from(this.metricsManager.getAll());
|
|
719
726
|
}
|
|
720
727
|
/**
|
|
721
728
|
* Creates a fluent request builder for making HTTP requests.
|
|
@@ -724,7 +731,7 @@ class HttpClientImproved {
|
|
|
724
731
|
* @returns RequestBuilder instance for chaining
|
|
725
732
|
*/
|
|
726
733
|
request(url) {
|
|
727
|
-
return new RequestBuilder_1.RequestBuilder(url);
|
|
734
|
+
return new RequestBuilder_1.RequestBuilder(url, this);
|
|
728
735
|
}
|
|
729
736
|
/**
|
|
730
737
|
* Returns current statistics about the HTTP client's state.
|