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.
Files changed (48) hide show
  1. package/dist/Hyperttp/Core/CacheManager.d.ts +16 -3
  2. package/dist/Hyperttp/Core/CacheManager.d.ts.map +1 -1
  3. package/dist/Hyperttp/Core/CacheManager.js +45 -13
  4. package/dist/Hyperttp/Core/CacheManager.js.map +1 -1
  5. package/dist/Hyperttp/Core/HttpClientImproved.d.ts +5 -3
  6. package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
  7. package/dist/Hyperttp/Core/HttpClientImproved.js +164 -157
  8. package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
  9. package/dist/Hyperttp/Core/MetricsManager.d.ts +32 -0
  10. package/dist/Hyperttp/Core/MetricsManager.d.ts.map +1 -0
  11. package/dist/Hyperttp/Core/MetricsManager.js +96 -0
  12. package/dist/Hyperttp/Core/MetricsManager.js.map +1 -0
  13. package/dist/Hyperttp/Core/QueueManager.d.ts.map +1 -1
  14. package/dist/Hyperttp/Core/QueueManager.js +12 -5
  15. package/dist/Hyperttp/Core/QueueManager.js.map +1 -1
  16. package/dist/Hyperttp/Core/RateLimiter.d.ts +6 -36
  17. package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
  18. package/dist/Hyperttp/Core/RateLimiter.js +41 -58
  19. package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
  20. package/dist/Hyperttp/Core/RequestBuilder.d.ts +17 -4
  21. package/dist/Hyperttp/Core/RequestBuilder.d.ts.map +1 -1
  22. package/dist/Hyperttp/Core/RequestBuilder.js +41 -18
  23. package/dist/Hyperttp/Core/RequestBuilder.js.map +1 -1
  24. package/dist/Hyperttp/Core/index.d.ts +4 -1
  25. package/dist/Hyperttp/Core/index.d.ts.map +1 -1
  26. package/dist/Hyperttp/Core/index.js +11 -5
  27. package/dist/Hyperttp/Core/index.js.map +1 -1
  28. package/dist/Hyperttp/Request.d.ts +5 -0
  29. package/dist/Hyperttp/Request.d.ts.map +1 -1
  30. package/dist/Hyperttp/Request.js +17 -35
  31. package/dist/Hyperttp/Request.js.map +1 -1
  32. package/dist/Hyperttp/UrlExtractor.d.ts +1 -1
  33. package/dist/Hyperttp/UrlExtractor.d.ts.map +1 -1
  34. package/dist/Hyperttp/UrlExtractor.js.map +1 -1
  35. package/dist/Hyperttp/index.d.ts +3 -1
  36. package/dist/Hyperttp/index.d.ts.map +1 -1
  37. package/dist/Hyperttp/index.js +6 -1
  38. package/dist/Hyperttp/index.js.map +1 -1
  39. package/dist/Types/index.d.ts +45 -206
  40. package/dist/Types/index.d.ts.map +1 -1
  41. package/dist/Types/index.js +7 -16
  42. package/dist/Types/index.js.map +1 -1
  43. package/dist/Types/request.d.ts +15 -100
  44. package/dist/Types/request.d.ts.map +1 -1
  45. package/dist/Types/request.js.map +1 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js.map +1 -1
  48. 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
- requestMetrics = new Map();
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 undici_1.Agent({
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
- /** Closes the HTTP agent to properly terminate keep-alive connections. */
172
+ /**
173
+ * @ru Закрывает агент и освобождает ресурсы (keep-alive соединения).
174
+ * @en Closes the HTTP agent and terminates keep-alive connections.
175
+ */
168
176
  close() {
169
- this.agent.close();
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
- if (typeof limit === "number" && limit > 0 && buf.length > limit) {
244
- throw new Types_1.HttpClientError(`Response too large (${buf.length} bytes), limit is ${limit}`, 0);
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 buf;
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: controller.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
- originalUrl: finalConfig.url,
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 (timeoutErr) {
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
- this.log("error", `Request error ${method} ${url}: ${err?.message ?? String(err)}`, {
343
- attempt: attempt + 1,
344
- error: err,
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 finalType = responseType ?? "json";
394
- switch (finalType) {
391
+ const trimmed = text.trim();
392
+ switch (responseType) {
395
393
  case "json": {
396
- if (contentType.includes("json")) {
397
- return JSON.parse(text);
394
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
395
+ return JSON.parse(trimmed);
398
396
  }
399
- if (text.trim().startsWith("<")) {
400
- return this.xmlParser.parse(text);
397
+ if (trimmed.startsWith("<")) {
398
+ return this.xmlParser.parse(trimmed);
401
399
  }
402
- return { text };
400
+ return { data: trimmed };
403
401
  }
404
402
  case "xml": {
405
- const text = await this.decompress(res.body, res.headers["content-encoding"]);
406
- if (text.trim().startsWith("<")) {
407
- return text;
408
- }
403
+ if (trimmed.startsWith("<"))
404
+ return trimmed;
409
405
  try {
410
- const json = JSON.parse(text);
411
- return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root: json });
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 String(text);
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 "stream":
422
- return res.body;
423
- default:
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 (error) {
428
- this.log("error", "Failed to parse response", {
429
- error,
430
- status: res.status,
431
- contentType: res.headers["content-type"],
432
- });
433
- throw new Types_1.HttpClientError(`Response parsing failed: ${error instanceof Error ? error.message : String(error)}`, res.status);
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", `Cache hit for ${url}`);
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 promise = (async () => {
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
- let result;
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.requestMetrics.set(key, metrics);
498
- this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
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
- this.requestMetrics.set(key, metrics);
513
- this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
514
- return result;
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.requestMetrics.set(key, metrics);
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 = "json") {
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 = "json") {
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 = "json") {
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 = "json") {
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 client.delete(simpleReq, responseType);
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 = "json") {
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
- if (typeof req === "string") {
630
- const simpleReq = {
632
+ async stream(req) {
633
+ const requestObj = typeof req === "string"
634
+ ? {
631
635
  getURL: () => req,
632
636
  getBodyData: () => undefined,
633
637
  getHeaders: () => ({}),
634
- };
635
- return this.stream(simpleReq);
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
- return (this.queue && this.options.enableQueue
638
- ? this.queue.enqueue(async function () {
639
- const url = req.getURL();
640
- const headers = {
641
- ...this.defaultHeaders,
642
- ...req.getHeaders(),
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
- }.bind(this)());
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.requestMetrics.clear();
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.requestMetrics.get(key);
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.requestMetrics.values());
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.