hyperttp 0.1.7 → 0.1.9
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 +13 -54
- package/dist/Hyperttp/Core/CacheManager.d.ts.map +1 -1
- package/dist/Hyperttp/Core/CacheManager.js +28 -50
- package/dist/Hyperttp/Core/CacheManager.js.map +1 -1
- package/dist/Hyperttp/Core/HttpClientImproved.d.ts +19 -249
- package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
- package/dist/Hyperttp/Core/HttpClientImproved.js +393 -173
- package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
- package/dist/Hyperttp/Core/QueueManager.d.ts.map +1 -1
- package/dist/Hyperttp/Core/QueueManager.js +0 -2
- package/dist/Hyperttp/Core/QueueManager.js.map +1 -1
- package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
- package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
- package/dist/Hyperttp/Core/index.d.ts +7 -7
- package/dist/Hyperttp/Core/index.d.ts.map +1 -1
- package/dist/Hyperttp/Core/index.js +8 -8
- package/dist/Hyperttp/Core/index.js.map +1 -1
- package/dist/Hyperttp/Request.d.ts.map +1 -1
- package/dist/Hyperttp/Request.js +0 -1
- package/dist/Hyperttp/Request.js.map +1 -1
- package/dist/Hyperttp/UrlExtractor.d.ts.map +1 -1
- package/dist/Hyperttp/UrlExtractor.js +0 -1
- package/dist/Hyperttp/UrlExtractor.js.map +1 -1
- package/dist/Hyperttp/index.d.ts +4 -4
- package/dist/Hyperttp/index.d.ts.map +1 -1
- package/dist/Hyperttp/index.js +11 -11
- package/dist/Hyperttp/index.js.map +1 -1
- package/dist/Types/index.d.ts +256 -0
- package/dist/Types/index.d.ts.map +1 -1
- package/dist/Types/index.js +47 -0
- package/dist/Types/index.js.map +1 -1
- package/dist/Types/request.d.ts +1 -1
- package/dist/Types/request.d.ts.map +1 -1
- package/dist/Types/request.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RateLimitError = exports.TimeoutError = exports.HttpClientError = void 0;
|
|
4
3
|
const tough_cookie_1 = require("tough-cookie");
|
|
5
4
|
const undici_1 = require("undici");
|
|
6
5
|
const undici_2 = require("http-cookie-agent/undici");
|
|
@@ -10,53 +9,8 @@ const crypto_1 = require("crypto");
|
|
|
10
9
|
const CacheManager_1 = require("./CacheManager");
|
|
11
10
|
const QueueManager_1 = require("./QueueManager");
|
|
12
11
|
const RateLimiter_1 = require("./RateLimiter");
|
|
12
|
+
const Types_1 = require("../../Types");
|
|
13
13
|
let defaultClient = null;
|
|
14
|
-
/**
|
|
15
|
-
* Base error class for HTTP client operations.
|
|
16
|
-
* Contains additional context about the failed request including status code, URL, and method.
|
|
17
|
-
*/
|
|
18
|
-
class HttpClientError extends Error {
|
|
19
|
-
statusCode;
|
|
20
|
-
originalError;
|
|
21
|
-
url;
|
|
22
|
-
method;
|
|
23
|
-
constructor(message, statusCode, originalError, url, method) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.statusCode = statusCode;
|
|
26
|
-
this.originalError = originalError;
|
|
27
|
-
this.url = url;
|
|
28
|
-
this.method = method;
|
|
29
|
-
this.name = "HttpClientError";
|
|
30
|
-
Object.setPrototypeOf(this, HttpClientError.prototype);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
exports.HttpClientError = HttpClientError;
|
|
34
|
-
/**
|
|
35
|
-
* Error thrown when an HTTP request exceeds the configured timeout duration.
|
|
36
|
-
* Contains information about the URL and timeout value that caused the failure.
|
|
37
|
-
*/
|
|
38
|
-
class TimeoutError extends HttpClientError {
|
|
39
|
-
constructor(url, timeout) {
|
|
40
|
-
super(`Request timeout after ${timeout}ms for ${url}`);
|
|
41
|
-
this.name = "TimeoutError";
|
|
42
|
-
Object.setPrototypeOf(this, TimeoutError.prototype);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
exports.TimeoutError = TimeoutError;
|
|
46
|
-
/**
|
|
47
|
-
* Error thrown when an HTTP request is rate limited by the server.
|
|
48
|
-
* Contains information about the URL and optional retry-after duration.
|
|
49
|
-
*/
|
|
50
|
-
class RateLimitError extends HttpClientError {
|
|
51
|
-
retryAfter;
|
|
52
|
-
constructor(url, retryAfter) {
|
|
53
|
-
super(`Rate limited for ${url}${retryAfter ? `, retry after ${retryAfter}ms` : ""}`);
|
|
54
|
-
this.retryAfter = retryAfter;
|
|
55
|
-
this.name = "RateLimitError";
|
|
56
|
-
Object.setPrototypeOf(this, RateLimitError.prototype);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
exports.RateLimitError = RateLimitError;
|
|
60
14
|
/**
|
|
61
15
|
* @ru
|
|
62
16
|
* Улучшенный HTTP-клиент с кэшированием, ограничением скорости, логикой повторных попыток и расширенными функциями.
|
|
@@ -119,7 +73,10 @@ class HttpClientImproved {
|
|
|
119
73
|
limiter;
|
|
120
74
|
inflight = new Map();
|
|
121
75
|
retryOptions;
|
|
122
|
-
|
|
76
|
+
baseHeaders = Object.freeze({
|
|
77
|
+
Accept: "application/json, text/plain, */*",
|
|
78
|
+
"User-Agent": "Hyperttp/0.1.0 Node.js",
|
|
79
|
+
});
|
|
123
80
|
options;
|
|
124
81
|
requestInterceptors = [];
|
|
125
82
|
responseInterceptors = [];
|
|
@@ -136,7 +93,7 @@ class HttpClientImproved {
|
|
|
136
93
|
validateStatus: options?.validateStatus ??
|
|
137
94
|
((status) => status >= 200 && status < 300),
|
|
138
95
|
cacheMethods: options?.cacheMethods ?? ["GET", "HEAD"],
|
|
139
|
-
maxMetricsSize: options?.maxMetricsSize ??
|
|
96
|
+
maxMetricsSize: options?.maxMetricsSize ?? 1000,
|
|
140
97
|
rateLimit: options?.rateLimit ?? { maxRequests: 100, windowMs: 60000 },
|
|
141
98
|
userAgent: options?.userAgent ?? "Hyperttp/0.1.0 Node.js",
|
|
142
99
|
logger: options?.logger ??
|
|
@@ -152,13 +109,22 @@ class HttpClientImproved {
|
|
|
152
109
|
retryOptions: options?.retryOptions ?? {},
|
|
153
110
|
maxResponseBytes: options?.maxResponseBytes ?? 1024 * 1024,
|
|
154
111
|
verbose: false,
|
|
112
|
+
enableQueue: options?.enableQueue ?? false,
|
|
113
|
+
enableRateLimit: options?.enableRateLimit ?? false,
|
|
114
|
+
enableCache: options?.enableCache ?? true,
|
|
155
115
|
};
|
|
156
|
-
this.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
116
|
+
if (this.options.enableCache) {
|
|
117
|
+
this.cache = new CacheManager_1.CacheManager({
|
|
118
|
+
cacheTTL: this.options.cacheTTL,
|
|
119
|
+
cacheMaxSize: this.options.cacheMaxSize,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (this.options.enableQueue) {
|
|
123
|
+
this.queue = new QueueManager_1.QueueManager(this.options.maxConcurrent ?? 500);
|
|
124
|
+
}
|
|
125
|
+
if (this.options.enableRateLimit) {
|
|
126
|
+
this.limiter = new RateLimiter_1.RateLimiter(this.options.rateLimit);
|
|
127
|
+
}
|
|
162
128
|
this.retryOptions = {
|
|
163
129
|
maxRetries: this.options.maxRetries,
|
|
164
130
|
baseDelay: this.options.retryOptions?.baseDelay ?? 1000,
|
|
@@ -168,13 +134,11 @@ class HttpClientImproved {
|
|
|
168
134
|
],
|
|
169
135
|
jitter: this.options.retryOptions?.jitter ?? true,
|
|
170
136
|
};
|
|
171
|
-
this.defaultHeaders = {
|
|
172
|
-
Accept: "application/json, text/plain, */*",
|
|
173
|
-
"User-Agent": this.options.userAgent ?? "Hyperttp/0.1.0 Node.js",
|
|
174
|
-
};
|
|
175
137
|
this.agent = new undici_1.Agent({
|
|
176
|
-
connections:
|
|
177
|
-
pipelining:
|
|
138
|
+
connections: 1000,
|
|
139
|
+
pipelining: 10,
|
|
140
|
+
keepAliveTimeout: 60000,
|
|
141
|
+
keepAliveMaxTimeout: 600000,
|
|
178
142
|
interceptors: {
|
|
179
143
|
Client: [(0, undici_2.cookie)({ jar: this.cookieJar })],
|
|
180
144
|
},
|
|
@@ -305,31 +269,45 @@ class HttpClientImproved {
|
|
|
305
269
|
* @param body - Async iterable of response chunks
|
|
306
270
|
* @returns Complete response body as a Buffer
|
|
307
271
|
*/
|
|
308
|
-
async readBodyWithLimit(body) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
272
|
+
async readBodyWithLimit(body, maxBytes = this.options.maxResponseBytes) {
|
|
273
|
+
if (!body)
|
|
274
|
+
return Buffer.alloc(0);
|
|
275
|
+
// ⚡ максимально быстрый парсинг через arrayBuffer
|
|
276
|
+
if (body?.arrayBuffer) {
|
|
277
|
+
const arrayBuffer = await body.arrayBuffer();
|
|
278
|
+
return Buffer.from(arrayBuffer);
|
|
279
|
+
}
|
|
280
|
+
if (body[Symbol.asyncIterator]) {
|
|
281
|
+
const chunks = [];
|
|
282
|
+
let total = 0;
|
|
283
|
+
for await (const chunk of body) {
|
|
284
|
+
chunks.push(Buffer.from(chunk));
|
|
285
|
+
total += chunk.length;
|
|
286
|
+
if (total > maxBytes)
|
|
287
|
+
break;
|
|
317
288
|
}
|
|
318
|
-
|
|
289
|
+
return Buffer.concat(chunks);
|
|
319
290
|
}
|
|
320
|
-
|
|
291
|
+
if (Buffer.isBuffer(body))
|
|
292
|
+
return body.subarray(0, maxBytes);
|
|
293
|
+
if (typeof body === "string")
|
|
294
|
+
return Buffer.from(body);
|
|
295
|
+
throw new TypeError(`Unsupported body type: ${typeof body}`);
|
|
321
296
|
}
|
|
322
297
|
/**
|
|
323
298
|
* Removes old metrics entries to prevent memory leaks.
|
|
324
299
|
* Keeps only metrics from the last 24 hours.
|
|
325
300
|
*/
|
|
326
301
|
trimMetrics() {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
302
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
303
|
+
for (const [key, metrics] of this.requestMetrics) {
|
|
304
|
+
if (metrics.endTime && metrics.endTime < cutoff) {
|
|
305
|
+
this.requestMetrics.delete(key);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const key of this.inflight.keys()) {
|
|
309
|
+
if (this.inflight.size > 1000) {
|
|
310
|
+
this.inflight.delete(key);
|
|
333
311
|
}
|
|
334
312
|
}
|
|
335
313
|
}
|
|
@@ -344,11 +322,105 @@ class HttpClientImproved {
|
|
|
344
322
|
* @param redirects - Number of redirects followed so far
|
|
345
323
|
* @returns Promise resolving to the response data
|
|
346
324
|
*/
|
|
325
|
+
async sendOnce(method, url, headers, body, metrics, redirects = 0) {
|
|
326
|
+
let lastError;
|
|
327
|
+
try {
|
|
328
|
+
if (this.limiter && this.options.enableRateLimit) {
|
|
329
|
+
await this.limiter.wait();
|
|
330
|
+
}
|
|
331
|
+
const finalConfig = await this.applyRequestInterceptors({
|
|
332
|
+
url,
|
|
333
|
+
method,
|
|
334
|
+
headers,
|
|
335
|
+
body,
|
|
336
|
+
});
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const timeout = this.options.timeout;
|
|
339
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
340
|
+
try {
|
|
341
|
+
const res = await (0, undici_1.request)(finalConfig.url, {
|
|
342
|
+
method: finalConfig.method,
|
|
343
|
+
headers: finalConfig.headers,
|
|
344
|
+
body: finalConfig.body,
|
|
345
|
+
dispatcher: this.agent,
|
|
346
|
+
signal: controller.signal,
|
|
347
|
+
});
|
|
348
|
+
if (method === "HEAD") {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
return {
|
|
351
|
+
status: res.statusCode,
|
|
352
|
+
headers: res.headers,
|
|
353
|
+
body: Buffer.alloc(0),
|
|
354
|
+
url: finalConfig.url,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
clearTimeout(timer);
|
|
358
|
+
const buf = await this.readBodyWithLimit(res.body);
|
|
359
|
+
let response = await this.applyResponseInterceptors({
|
|
360
|
+
status: res.statusCode,
|
|
361
|
+
headers: res.headers,
|
|
362
|
+
body: buf,
|
|
363
|
+
url: finalConfig.url,
|
|
364
|
+
});
|
|
365
|
+
if (!this.options.validateStatus(response.status)) {
|
|
366
|
+
throw new Types_1.HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
|
|
367
|
+
}
|
|
368
|
+
if (this.options.followRedirects &&
|
|
369
|
+
[301, 302, 303, 307, 308].includes(response.status) &&
|
|
370
|
+
redirects < (this.options.maxRedirects ?? 5)) {
|
|
371
|
+
const location = response.headers.location;
|
|
372
|
+
if (location) {
|
|
373
|
+
const nextUrl = this.resolveRedirect(location, finalConfig.url);
|
|
374
|
+
const redirectMethod = response.status === 303 ? "GET" : method;
|
|
375
|
+
const nextHeaders = { ...headers };
|
|
376
|
+
let nextBody = body;
|
|
377
|
+
if (redirectMethod === "GET") {
|
|
378
|
+
nextBody = undefined;
|
|
379
|
+
delete nextHeaders["content-type"];
|
|
380
|
+
delete nextHeaders["Content-Type"];
|
|
381
|
+
delete nextHeaders["content-length"];
|
|
382
|
+
delete nextHeaders["Content-Length"];
|
|
383
|
+
}
|
|
384
|
+
this.log("debug", `Redirecting to ${nextUrl}`, {
|
|
385
|
+
originalUrl: finalConfig.url,
|
|
386
|
+
status: response.status,
|
|
387
|
+
});
|
|
388
|
+
return this.sendOnce(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, redirects + 1);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return response;
|
|
392
|
+
}
|
|
393
|
+
catch (timeoutErr) {
|
|
394
|
+
clearTimeout(timer);
|
|
395
|
+
if (timeoutErr?.name === "AbortError")
|
|
396
|
+
throw new Types_1.TimeoutError(url, timeout);
|
|
397
|
+
throw timeoutErr;
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
lastError = err;
|
|
405
|
+
this.log("error", `Request error ${method} ${url}: ${err?.message ?? String(err)}`, {
|
|
406
|
+
error: err,
|
|
407
|
+
});
|
|
408
|
+
metrics && (metrics.retries += 1);
|
|
409
|
+
}
|
|
410
|
+
if (lastError instanceof Types_1.HttpClientError)
|
|
411
|
+
throw lastError;
|
|
412
|
+
throw new Types_1.HttpClientError(`Request failed after 1 attempt`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
|
|
413
|
+
}
|
|
347
414
|
async sendWithRetry(method, url, headers, body, metrics, redirects = 0) {
|
|
348
415
|
let lastError;
|
|
416
|
+
if (this.retryOptions.maxRetries === 0) {
|
|
417
|
+
return this.sendOnce(method, url, headers, body, metrics, redirects);
|
|
418
|
+
}
|
|
349
419
|
for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
|
|
350
420
|
try {
|
|
351
|
-
|
|
421
|
+
if (this.limiter && this.options.enableRateLimit) {
|
|
422
|
+
await this.limiter.wait();
|
|
423
|
+
}
|
|
352
424
|
const finalConfig = await this.applyRequestInterceptors({
|
|
353
425
|
url,
|
|
354
426
|
method,
|
|
@@ -366,6 +438,15 @@ class HttpClientImproved {
|
|
|
366
438
|
dispatcher: this.agent,
|
|
367
439
|
signal: controller.signal,
|
|
368
440
|
});
|
|
441
|
+
if (method === "HEAD") {
|
|
442
|
+
clearTimeout(timer);
|
|
443
|
+
return {
|
|
444
|
+
status: res.statusCode,
|
|
445
|
+
headers: res.headers,
|
|
446
|
+
body: Buffer.alloc(0),
|
|
447
|
+
url: finalConfig.url,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
369
450
|
clearTimeout(timer);
|
|
370
451
|
const buf = await this.readBodyWithLimit(res.body);
|
|
371
452
|
let response = await this.applyResponseInterceptors({
|
|
@@ -375,7 +456,7 @@ class HttpClientImproved {
|
|
|
375
456
|
url: finalConfig.url,
|
|
376
457
|
});
|
|
377
458
|
if (!this.options.validateStatus(response.status)) {
|
|
378
|
-
throw new HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
|
|
459
|
+
throw new Types_1.HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
|
|
379
460
|
}
|
|
380
461
|
if (this.options.followRedirects &&
|
|
381
462
|
[301, 302, 303, 307, 308].includes(response.status) &&
|
|
@@ -410,7 +491,7 @@ class HttpClientImproved {
|
|
|
410
491
|
await this.sleep(ra);
|
|
411
492
|
continue;
|
|
412
493
|
}
|
|
413
|
-
throw new RateLimitError(finalConfig.url, ra);
|
|
494
|
+
throw new Types_1.RateLimitError(finalConfig.url, ra);
|
|
414
495
|
}
|
|
415
496
|
}
|
|
416
497
|
this.log("warn", `Retrying ${method} ${finalConfig.url} due to status ${response.status}`, {
|
|
@@ -427,7 +508,7 @@ class HttpClientImproved {
|
|
|
427
508
|
catch (timeoutErr) {
|
|
428
509
|
clearTimeout(timer);
|
|
429
510
|
if (timeoutErr?.name === "AbortError")
|
|
430
|
-
throw new TimeoutError(url, timeout);
|
|
511
|
+
throw new Types_1.TimeoutError(url, timeout);
|
|
431
512
|
throw timeoutErr;
|
|
432
513
|
}
|
|
433
514
|
finally {
|
|
@@ -447,9 +528,9 @@ class HttpClientImproved {
|
|
|
447
528
|
}
|
|
448
529
|
}
|
|
449
530
|
}
|
|
450
|
-
if (lastError instanceof HttpClientError)
|
|
531
|
+
if (lastError instanceof Types_1.HttpClientError)
|
|
451
532
|
throw lastError;
|
|
452
|
-
throw new HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
|
|
533
|
+
throw new Types_1.HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
|
|
453
534
|
}
|
|
454
535
|
/**
|
|
455
536
|
* Parses the Content-Type header to extract MIME type and character encoding.
|
|
@@ -494,43 +575,33 @@ class HttpClientImproved {
|
|
|
494
575
|
* @returns Parsed response data
|
|
495
576
|
*/
|
|
496
577
|
async parseResponse(res, responseType) {
|
|
497
|
-
const { type
|
|
498
|
-
const text = res.body.toString(charset);
|
|
578
|
+
const { type } = this.parseContentType(res.headers["content-type"]);
|
|
499
579
|
const finalType = responseType ?? (type.includes("application/json") ? "json" : "text");
|
|
500
580
|
try {
|
|
501
581
|
switch (finalType) {
|
|
502
|
-
case "json":
|
|
503
|
-
|
|
504
|
-
return JSON.parse(text);
|
|
505
|
-
}
|
|
506
|
-
catch {
|
|
507
|
-
try {
|
|
508
|
-
return new fast_xml_parser_1.XMLParser({ ignoreAttributes: false }).parse(text);
|
|
509
|
-
}
|
|
510
|
-
catch {
|
|
511
|
-
return { data: text };
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
582
|
+
case "json":
|
|
583
|
+
return JSON.parse(res.body.toString("utf8"));
|
|
515
584
|
case "xml": {
|
|
516
585
|
try {
|
|
517
|
-
const jsonData = JSON.parse(
|
|
586
|
+
const jsonData = JSON.parse(res.body.toString("utf8"));
|
|
518
587
|
return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root: jsonData });
|
|
519
588
|
}
|
|
520
589
|
catch {
|
|
521
|
-
return
|
|
590
|
+
return res.body.toString("utf8");
|
|
522
591
|
}
|
|
523
592
|
}
|
|
524
593
|
case "text":
|
|
525
|
-
return
|
|
594
|
+
return res.body.toString("utf8");
|
|
526
595
|
case "buffer":
|
|
527
596
|
return res.body;
|
|
597
|
+
case "stream":
|
|
598
|
+
throw new Error("Stream mode requires raw response. Use stream() method.");
|
|
528
599
|
default:
|
|
529
|
-
return
|
|
600
|
+
return res.body.toString("utf8");
|
|
530
601
|
}
|
|
531
602
|
}
|
|
532
603
|
catch (err) {
|
|
533
|
-
throw new HttpClientError(`Parsing failed: ${err?.message ?? String(err)}`, res.status);
|
|
604
|
+
throw new Types_1.HttpClientError(`Parsing failed: ${err?.message ?? String(err)}`, res.status);
|
|
534
605
|
}
|
|
535
606
|
}
|
|
536
607
|
/**
|
|
@@ -544,10 +615,7 @@ class HttpClientImproved {
|
|
|
544
615
|
async requestInternalWithoutCache(method, req, responseType) {
|
|
545
616
|
const url = req.getURL();
|
|
546
617
|
const rawBody = req.getBodyData();
|
|
547
|
-
const headers = {
|
|
548
|
-
...this.defaultHeaders,
|
|
549
|
-
...req.getHeaders(),
|
|
550
|
-
};
|
|
618
|
+
const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
|
|
551
619
|
const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
552
620
|
let body;
|
|
553
621
|
const contentType = headers["content-type"] || headers["Content-Type"] || "";
|
|
@@ -578,19 +646,35 @@ class HttpClientImproved {
|
|
|
578
646
|
url,
|
|
579
647
|
method,
|
|
580
648
|
};
|
|
581
|
-
const result = await this.queue
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
649
|
+
const result = await (this.queue && this.options.enableQueue
|
|
650
|
+
? this.queue.enqueue(async () => {
|
|
651
|
+
const res = await this.sendWithRetry(method, url, headers, body, metrics);
|
|
652
|
+
metrics.statusCode = res.status;
|
|
653
|
+
metrics.bytesReceived = res.body.length;
|
|
654
|
+
metrics.bytesSent =
|
|
655
|
+
body instanceof Buffer
|
|
656
|
+
? body.length
|
|
657
|
+
: Buffer.byteLength(body || "");
|
|
658
|
+
if (method === "HEAD") {
|
|
659
|
+
return { status: res.status, headers: res.headers };
|
|
660
|
+
}
|
|
661
|
+
const parsed = await this.parseResponse(res, responseType);
|
|
662
|
+
return parsed;
|
|
663
|
+
})
|
|
664
|
+
: (async () => {
|
|
665
|
+
const res = await this.sendWithRetry(method, url, headers, body, metrics);
|
|
666
|
+
metrics.statusCode = res.status;
|
|
667
|
+
metrics.bytesReceived = res.body.length;
|
|
668
|
+
metrics.bytesSent =
|
|
669
|
+
body instanceof Buffer
|
|
670
|
+
? body.length
|
|
671
|
+
: Buffer.byteLength(body || "");
|
|
672
|
+
if (method === "HEAD") {
|
|
673
|
+
return { status: res.status, headers: res.headers };
|
|
674
|
+
}
|
|
675
|
+
const parsed = await this.parseResponse(res, responseType);
|
|
676
|
+
return parsed;
|
|
677
|
+
})());
|
|
594
678
|
metrics.endTime = Date.now();
|
|
595
679
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
596
680
|
this.requestMetrics.set(url, metrics);
|
|
@@ -606,16 +690,50 @@ class HttpClientImproved {
|
|
|
606
690
|
* @param responseType - Expected response type
|
|
607
691
|
* @returns Promise resolving to the response data
|
|
608
692
|
*/
|
|
693
|
+
async fastRequest(method, req, responseType) {
|
|
694
|
+
const url = req.getURL();
|
|
695
|
+
const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
|
|
696
|
+
const body = req.getBodyData();
|
|
697
|
+
let finalBody;
|
|
698
|
+
if (body == null) {
|
|
699
|
+
finalBody = undefined;
|
|
700
|
+
}
|
|
701
|
+
else if (typeof body === "string" || Buffer.isBuffer(body)) {
|
|
702
|
+
finalBody = body;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
finalBody = JSON.stringify(body);
|
|
706
|
+
}
|
|
707
|
+
const res = await (0, undici_1.request)(url, {
|
|
708
|
+
method,
|
|
709
|
+
headers,
|
|
710
|
+
body: finalBody,
|
|
711
|
+
dispatcher: this.agent,
|
|
712
|
+
});
|
|
713
|
+
if (responseType === "json") {
|
|
714
|
+
return res.body.json();
|
|
715
|
+
}
|
|
716
|
+
if (responseType === "text") {
|
|
717
|
+
const text = await res.body.text();
|
|
718
|
+
return text;
|
|
719
|
+
}
|
|
720
|
+
return Buffer.from(await res.body.arrayBuffer());
|
|
721
|
+
}
|
|
609
722
|
async requestInternal(method, req, useCache = true, responseType) {
|
|
610
|
-
if (this.
|
|
723
|
+
if (!this.cache &&
|
|
724
|
+
!this.options.enableQueue &&
|
|
725
|
+
!this.options.enableRateLimit &&
|
|
726
|
+
this.retryOptions.maxRetries === 0 &&
|
|
727
|
+
this.requestInterceptors.length === 0 &&
|
|
728
|
+
this.responseInterceptors.length === 0) {
|
|
729
|
+
return this.fastRequest(method, req, responseType);
|
|
730
|
+
}
|
|
731
|
+
if (this.options.cacheTTL === 0 || this.options.enableCache === false) {
|
|
611
732
|
return this.requestInternalWithoutCache(method, req, responseType);
|
|
612
733
|
}
|
|
613
734
|
const url = req.getURL();
|
|
614
735
|
const rawBody = req.getBodyData();
|
|
615
|
-
const headers = {
|
|
616
|
-
...this.defaultHeaders,
|
|
617
|
-
...req.getHeaders(),
|
|
618
|
-
};
|
|
736
|
+
const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
|
|
619
737
|
const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
620
738
|
let body;
|
|
621
739
|
const contentType = headers["content-type"] || headers["Content-Type"] || "";
|
|
@@ -635,18 +753,21 @@ class HttpClientImproved {
|
|
|
635
753
|
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
636
754
|
}
|
|
637
755
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
756
|
+
let cacheKey;
|
|
757
|
+
if (this.cache && useCache) {
|
|
758
|
+
const bodyHash = this.hashBody(body);
|
|
759
|
+
cacheKey = (0, crypto_1.createHash)("sha1")
|
|
760
|
+
.update(method + url + bodyHash + responseType)
|
|
761
|
+
.digest("hex");
|
|
762
|
+
}
|
|
763
|
+
if (cacheKey && this.options.cacheMethods.includes(method) && useCache && this.cache) {
|
|
764
|
+
const cached = await this.cache.get(cacheKey);
|
|
765
|
+
if (cached != null) {
|
|
645
766
|
this.log("debug", `Cache hit for ${url}`);
|
|
646
767
|
return cached;
|
|
647
768
|
}
|
|
648
769
|
}
|
|
649
|
-
if (this.inflight.has(cacheKey)) {
|
|
770
|
+
if (cacheKey && this.inflight.has(cacheKey)) {
|
|
650
771
|
this.log("debug", `Deduplicating request for ${url}`);
|
|
651
772
|
return this.inflight.get(cacheKey);
|
|
652
773
|
}
|
|
@@ -661,31 +782,55 @@ class HttpClientImproved {
|
|
|
661
782
|
cached: false,
|
|
662
783
|
url,
|
|
663
784
|
method,
|
|
664
|
-
bodyHash,
|
|
665
785
|
};
|
|
666
786
|
try {
|
|
667
787
|
this.log("debug", `Starting request: ${method} ${url}`);
|
|
668
|
-
const result = await this.queue
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
788
|
+
const result = await (this.queue && this.options.enableQueue
|
|
789
|
+
? this.queue.enqueue(async () => {
|
|
790
|
+
const res = await this.sendWithRetry(method, url, headers, body, metrics);
|
|
791
|
+
metrics.statusCode = res.status;
|
|
792
|
+
metrics.bytesReceived = res.body.length;
|
|
793
|
+
metrics.bytesSent =
|
|
794
|
+
body instanceof Buffer
|
|
795
|
+
? body.length
|
|
796
|
+
: Buffer.byteLength(body || "");
|
|
797
|
+
if (method === "HEAD") {
|
|
798
|
+
return { status: res.status, headers: res.headers };
|
|
799
|
+
}
|
|
800
|
+
const parsed = await this.parseResponse(res, responseType);
|
|
801
|
+
if (cacheKey &&
|
|
802
|
+
this.options.cacheMethods.includes(method) &&
|
|
803
|
+
useCache &&
|
|
804
|
+
this.cache) {
|
|
805
|
+
this.cache.set(cacheKey, parsed);
|
|
806
|
+
metrics.cached = true;
|
|
807
|
+
}
|
|
808
|
+
return parsed;
|
|
809
|
+
})
|
|
810
|
+
: (async () => {
|
|
811
|
+
const res = await this.sendWithRetry(method, url, headers, body, metrics);
|
|
812
|
+
metrics.statusCode = res.status;
|
|
813
|
+
metrics.bytesReceived = res.body.length;
|
|
814
|
+
metrics.bytesSent =
|
|
815
|
+
body instanceof Buffer
|
|
816
|
+
? body.length
|
|
817
|
+
: Buffer.byteLength(body || "");
|
|
818
|
+
if (method === "HEAD") {
|
|
819
|
+
return { status: res.status, headers: res.headers };
|
|
820
|
+
}
|
|
821
|
+
const parsed = await this.parseResponse(res, responseType);
|
|
822
|
+
if (cacheKey &&
|
|
823
|
+
this.options.cacheMethods.includes(method) &&
|
|
824
|
+
useCache &&
|
|
825
|
+
this.cache) {
|
|
826
|
+
await this.cache.set(cacheKey, parsed);
|
|
827
|
+
metrics.cached = true;
|
|
828
|
+
}
|
|
829
|
+
return parsed;
|
|
830
|
+
})());
|
|
686
831
|
metrics.endTime = Date.now();
|
|
687
832
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
688
|
-
this.requestMetrics.set(cacheKey, metrics);
|
|
833
|
+
this.requestMetrics.set(cacheKey || url, metrics);
|
|
689
834
|
this.trimMetrics();
|
|
690
835
|
this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
|
|
691
836
|
return result;
|
|
@@ -693,15 +838,17 @@ class HttpClientImproved {
|
|
|
693
838
|
catch (error) {
|
|
694
839
|
metrics.endTime = Date.now();
|
|
695
840
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
696
|
-
this.requestMetrics.set(cacheKey, metrics);
|
|
841
|
+
this.requestMetrics.set(cacheKey || url, metrics);
|
|
697
842
|
this.trimMetrics();
|
|
698
843
|
throw error;
|
|
699
844
|
}
|
|
700
845
|
finally {
|
|
701
|
-
|
|
846
|
+
if (cacheKey)
|
|
847
|
+
this.inflight.delete(cacheKey);
|
|
702
848
|
}
|
|
703
849
|
})();
|
|
704
|
-
|
|
850
|
+
if (cacheKey)
|
|
851
|
+
this.inflight.set(cacheKey, promise);
|
|
705
852
|
return promise;
|
|
706
853
|
}
|
|
707
854
|
/**
|
|
@@ -758,16 +905,60 @@ class HttpClientImproved {
|
|
|
758
905
|
*/
|
|
759
906
|
put(req, body, responseType = "json") {
|
|
760
907
|
if (typeof req === "string") {
|
|
761
|
-
const client = defaultClient ?? (defaultClient = new HttpClientImproved());
|
|
762
908
|
const simpleReq = {
|
|
763
909
|
getURL: () => req,
|
|
764
910
|
getBodyData: () => body,
|
|
765
911
|
getHeaders: () => ({ "Content-Type": "application/json" }),
|
|
766
912
|
};
|
|
767
|
-
return
|
|
913
|
+
return this.requestInternal("PUT", simpleReq, false, responseType);
|
|
768
914
|
}
|
|
769
915
|
return this.requestInternal("PUT", req, false, responseType);
|
|
770
916
|
}
|
|
917
|
+
/**
|
|
918
|
+
* @ru Получает потоковый ответ (для SSE, больших файлов).
|
|
919
|
+
* @en Gets streaming response (for SSE, large files).
|
|
920
|
+
*/
|
|
921
|
+
stream(req) {
|
|
922
|
+
if (typeof req === "string") {
|
|
923
|
+
const simpleReq = {
|
|
924
|
+
getURL: () => req,
|
|
925
|
+
getBodyData: () => undefined,
|
|
926
|
+
getHeaders: () => ({}),
|
|
927
|
+
};
|
|
928
|
+
return this.stream(simpleReq);
|
|
929
|
+
}
|
|
930
|
+
return (this.queue && this.options.enableQueue
|
|
931
|
+
? this.queue.enqueue(async function () {
|
|
932
|
+
const url = req.getURL();
|
|
933
|
+
const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
|
|
934
|
+
const response = await (0, undici_1.request)(url, {
|
|
935
|
+
method: "GET",
|
|
936
|
+
headers,
|
|
937
|
+
dispatcher: this.agent,
|
|
938
|
+
});
|
|
939
|
+
return {
|
|
940
|
+
status: response.statusCode,
|
|
941
|
+
headers: response.headers,
|
|
942
|
+
body: response.body,
|
|
943
|
+
url,
|
|
944
|
+
};
|
|
945
|
+
}.bind(this))
|
|
946
|
+
: async function () {
|
|
947
|
+
const url = req.getURL();
|
|
948
|
+
const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
|
|
949
|
+
const response = await (0, undici_1.request)(url, {
|
|
950
|
+
method: "GET",
|
|
951
|
+
headers,
|
|
952
|
+
dispatcher: this.agent,
|
|
953
|
+
});
|
|
954
|
+
return {
|
|
955
|
+
status: response.statusCode,
|
|
956
|
+
headers: response.headers,
|
|
957
|
+
body: response.body,
|
|
958
|
+
url,
|
|
959
|
+
};
|
|
960
|
+
}.bind(this)());
|
|
961
|
+
}
|
|
771
962
|
/**
|
|
772
963
|
* Makes an HTTP DELETE request.
|
|
773
964
|
* Supports both RequestInterface objects and direct URL strings.
|
|
@@ -795,7 +986,15 @@ class HttpClientImproved {
|
|
|
795
986
|
* @param responseType - Expected response type (default: "json")
|
|
796
987
|
* @returns Promise resolving to the response data
|
|
797
988
|
*/
|
|
798
|
-
patch(req, responseType = "json") {
|
|
989
|
+
patch(req, body, responseType = "json") {
|
|
990
|
+
if (typeof req === "string") {
|
|
991
|
+
const simpleReq = {
|
|
992
|
+
getURL: () => req,
|
|
993
|
+
getBodyData: () => body,
|
|
994
|
+
getHeaders: () => ({ "Content-Type": "application/json" }),
|
|
995
|
+
};
|
|
996
|
+
return this.requestInternal("PATCH", simpleReq, false, responseType);
|
|
997
|
+
}
|
|
799
998
|
return this.requestInternal("PATCH", req, false, responseType);
|
|
800
999
|
}
|
|
801
1000
|
/**
|
|
@@ -807,13 +1006,12 @@ class HttpClientImproved {
|
|
|
807
1006
|
*/
|
|
808
1007
|
async head(req) {
|
|
809
1008
|
if (typeof req === "string") {
|
|
810
|
-
const client = defaultClient ?? (defaultClient = new HttpClientImproved());
|
|
811
1009
|
const simpleReq = {
|
|
812
1010
|
getURL: () => req,
|
|
813
1011
|
getBodyData: () => undefined,
|
|
814
1012
|
getHeaders: () => ({}),
|
|
815
1013
|
};
|
|
816
|
-
return
|
|
1014
|
+
return this.requestInternal("HEAD", simpleReq, false);
|
|
817
1015
|
}
|
|
818
1016
|
return this.requestInternal("HEAD", req, false);
|
|
819
1017
|
}
|
|
@@ -821,9 +1019,11 @@ class HttpClientImproved {
|
|
|
821
1019
|
* Clears the internal cache of the HTTP client.
|
|
822
1020
|
* Removes all cached responses and resets the cache state.
|
|
823
1021
|
*/
|
|
824
|
-
clearCache() {
|
|
825
|
-
this.cache
|
|
826
|
-
|
|
1022
|
+
async clearCache() {
|
|
1023
|
+
if (this.cache) {
|
|
1024
|
+
await this.cache.clear();
|
|
1025
|
+
this.log("info", "Cache cleared");
|
|
1026
|
+
}
|
|
827
1027
|
}
|
|
828
1028
|
/**
|
|
829
1029
|
* Clears all collected request metrics.
|
|
@@ -864,11 +1064,17 @@ class HttpClientImproved {
|
|
|
864
1064
|
*/
|
|
865
1065
|
getStats() {
|
|
866
1066
|
return {
|
|
867
|
-
cacheSize: this.cache
|
|
1067
|
+
cacheSize: this.cache?.size ?? 0,
|
|
868
1068
|
inflightRequests: this.inflight.size,
|
|
869
|
-
queuedRequests: this.queue.
|
|
870
|
-
|
|
871
|
-
|
|
1069
|
+
queuedRequests: this.queue && this.options.enableQueue
|
|
1070
|
+
? (this.queue.queuedCount ?? 0)
|
|
1071
|
+
: 0,
|
|
1072
|
+
activeRequests: this.queue && this.options.enableQueue
|
|
1073
|
+
? (this.queue.activeCount ?? 0)
|
|
1074
|
+
: 0,
|
|
1075
|
+
currentRateLimit: this.limiter && this.options.enableRateLimit
|
|
1076
|
+
? (this.limiter.currentCount ?? 0)
|
|
1077
|
+
: 0,
|
|
872
1078
|
metricsSize: this.requestMetrics.size,
|
|
873
1079
|
};
|
|
874
1080
|
}
|
|
@@ -951,6 +1157,14 @@ class RequestBuilder {
|
|
|
951
1157
|
this._method = "POST";
|
|
952
1158
|
return this;
|
|
953
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* @ru Устанавливает потоковый режим ответа.
|
|
1162
|
+
* @en Sets streaming response mode.
|
|
1163
|
+
*/
|
|
1164
|
+
stream() {
|
|
1165
|
+
this._responseType = "stream";
|
|
1166
|
+
return this;
|
|
1167
|
+
}
|
|
954
1168
|
/**
|
|
955
1169
|
* Sets the HTTP method to PUT.
|
|
956
1170
|
* @returns The builder instance for chaining
|
|
@@ -994,7 +1208,7 @@ class RequestBuilder {
|
|
|
994
1208
|
*/
|
|
995
1209
|
jsonBody(body) {
|
|
996
1210
|
this._body = body;
|
|
997
|
-
this._headers[
|
|
1211
|
+
this._headers["Content-Type"] = "application/json; charset=utf-8";
|
|
998
1212
|
return this;
|
|
999
1213
|
}
|
|
1000
1214
|
/**
|
|
@@ -1010,16 +1224,22 @@ class RequestBuilder {
|
|
|
1010
1224
|
};
|
|
1011
1225
|
switch (this._method) {
|
|
1012
1226
|
case "GET":
|
|
1227
|
+
if (this._responseType === "stream") {
|
|
1228
|
+
return client.stream(req);
|
|
1229
|
+
}
|
|
1013
1230
|
return client.get(req, this._responseType);
|
|
1014
1231
|
case "POST":
|
|
1015
|
-
return client.post(req, this._responseType);
|
|
1232
|
+
return client.post(req, this._body, this._responseType);
|
|
1016
1233
|
case "PUT":
|
|
1017
|
-
return client.put(req, this._responseType);
|
|
1234
|
+
return client.put(req, this._body, this._responseType);
|
|
1018
1235
|
case "DELETE":
|
|
1019
1236
|
return client.delete(req, this._responseType);
|
|
1020
1237
|
case "PATCH":
|
|
1021
|
-
return client.patch(req, this._responseType);
|
|
1238
|
+
return client.patch(req, this._body, this._responseType);
|
|
1022
1239
|
default:
|
|
1240
|
+
if (this._responseType === "stream") {
|
|
1241
|
+
return client.stream(req);
|
|
1242
|
+
}
|
|
1023
1243
|
return client.get(req, this._responseType);
|
|
1024
1244
|
}
|
|
1025
1245
|
}
|