hyperttp 0.1.9 → 0.2.0
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/HttpClientImproved.d.ts +64 -287
- package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
- package/dist/Hyperttp/Core/HttpClientImproved.js +258 -755
- package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
- package/dist/Hyperttp/Core/RequestBuilder.d.ts +97 -0
- package/dist/Hyperttp/Core/RequestBuilder.d.ts.map +1 -0
- package/dist/Hyperttp/Core/RequestBuilder.js +174 -0
- package/dist/Hyperttp/Core/RequestBuilder.js.map +1 -0
- package/dist/Hyperttp/Core/index.d.ts +1 -0
- package/dist/Hyperttp/Core/index.d.ts.map +1 -1
- package/dist/Hyperttp/Core/index.js +3 -1
- package/dist/Hyperttp/Core/index.js.map +1 -1
- package/dist/Hyperttp/index.d.ts +1 -1
- package/dist/Hyperttp/index.d.ts.map +1 -1
- package/dist/Hyperttp/index.js +2 -1
- package/dist/Hyperttp/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,69 +1,55 @@
|
|
|
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
const tough_cookie_1 = require("tough-cookie");
|
|
4
37
|
const undici_1 = require("undici");
|
|
5
38
|
const undici_2 = require("http-cookie-agent/undici");
|
|
39
|
+
const zlib = __importStar(require("zlib"));
|
|
40
|
+
const util_1 = require("util");
|
|
6
41
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
7
|
-
const url_1 = require("url");
|
|
8
|
-
const crypto_1 = require("crypto");
|
|
9
42
|
const CacheManager_1 = require("./CacheManager");
|
|
10
43
|
const QueueManager_1 = require("./QueueManager");
|
|
11
44
|
const RateLimiter_1 = require("./RateLimiter");
|
|
12
45
|
const Types_1 = require("../../Types");
|
|
13
|
-
|
|
46
|
+
const RequestBuilder_1 = require("./RequestBuilder");
|
|
47
|
+
const gunzip = (0, util_1.promisify)(zlib.gunzip);
|
|
48
|
+
const inflate = (0, util_1.promisify)(zlib.inflate);
|
|
49
|
+
const brotliDecompress = (0, util_1.promisify)(zlib.brotliDecompress);
|
|
14
50
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Предоставляет надежный интерфейс для выполнения HTTP-запросов с автоматической обработкой
|
|
18
|
-
* распространенных паттернов, таких как повторные попытки, кэширование и перехват запросов/ответов.
|
|
19
|
-
* @en
|
|
20
|
-
* Enhanced HTTP client with caching, rate limiting, retry logic, and advanced features.
|
|
21
|
-
* Provides a robust interface for making HTTP requests with automatic handling of
|
|
22
|
-
* common patterns like retries, caching, and request/response interception.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* const client = new HttpClientImproved({
|
|
27
|
-
* timeout: 10000,
|
|
28
|
-
* maxRetries: 3,
|
|
29
|
-
* cacheTTL: 300000,
|
|
30
|
-
* rateLimit: { maxRequests: 100, windowMs: 60000 }
|
|
31
|
-
* });
|
|
32
|
-
*
|
|
33
|
-
* const response = await client.get('https://api.example.com/data');
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```ts
|
|
38
|
-
* // Using the fluent request builder
|
|
39
|
-
* const client = new HttpClientImproved();
|
|
40
|
-
* const response = await client.request('https://api.example.com/data')
|
|
41
|
-
* .headers({ 'Authorization': 'Bearer token' })
|
|
42
|
-
* .json()
|
|
43
|
-
* .send();
|
|
44
|
-
* ```
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```ts
|
|
48
|
-
* // Using RequestInterface for complex requests
|
|
49
|
-
* import { RequestInterface } from './src';
|
|
50
|
-
*
|
|
51
|
-
* class ApiRequest implements RequestInterface {
|
|
52
|
-
* constructor(
|
|
53
|
-
* private url: string,
|
|
54
|
-
* private headers: Record<string, string> = {},
|
|
55
|
-
* private body?: any
|
|
56
|
-
* ) {}
|
|
57
|
-
*
|
|
58
|
-
* getURL(): string { return this.url; }
|
|
59
|
-
* getHeaders(): Record<string, string> { return this.headers; }
|
|
60
|
-
* getBodyData(): any { return this.body; }
|
|
61
|
-
* }
|
|
62
|
-
*
|
|
63
|
-
* const client = new HttpClientImproved();
|
|
64
|
-
* const request = new ApiRequest('https://api.example.com/data');
|
|
65
|
-
* const response = await client.get(request);
|
|
66
|
-
* ```
|
|
51
|
+
* Advanced HTTP client with built-in caching, rate limiting, request queuing,
|
|
52
|
+
* automatic retries, cookie management, and response decompression.
|
|
67
53
|
*/
|
|
68
54
|
class HttpClientImproved {
|
|
69
55
|
cookieJar = new tough_cookie_1.CookieJar();
|
|
@@ -73,14 +59,15 @@ class HttpClientImproved {
|
|
|
73
59
|
limiter;
|
|
74
60
|
inflight = new Map();
|
|
75
61
|
retryOptions;
|
|
76
|
-
|
|
77
|
-
Accept: "application/json, text/plain, */*",
|
|
78
|
-
"User-Agent": "Hyperttp/0.1.0 Node.js",
|
|
79
|
-
});
|
|
62
|
+
defaultHeaders = {};
|
|
80
63
|
options;
|
|
81
64
|
requestInterceptors = [];
|
|
82
65
|
responseInterceptors = [];
|
|
83
66
|
requestMetrics = new Map();
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new instance of HttpClientImproved.
|
|
69
|
+
* @param options Optional configuration options for the HTTP client
|
|
70
|
+
*/
|
|
84
71
|
constructor(options) {
|
|
85
72
|
this.options = {
|
|
86
73
|
timeout: options?.timeout ?? 15000,
|
|
@@ -126,7 +113,7 @@ class HttpClientImproved {
|
|
|
126
113
|
this.limiter = new RateLimiter_1.RateLimiter(this.options.rateLimit);
|
|
127
114
|
}
|
|
128
115
|
this.retryOptions = {
|
|
129
|
-
maxRetries: this.options.maxRetries,
|
|
116
|
+
maxRetries: this.options.maxRetries ?? 5,
|
|
130
117
|
baseDelay: this.options.retryOptions?.baseDelay ?? 1000,
|
|
131
118
|
maxDelay: this.options.retryOptions?.maxDelay ?? 30000,
|
|
132
119
|
retryStatusCodes: this.options.retryOptions?.retryStatusCodes ?? [
|
|
@@ -134,6 +121,11 @@ class HttpClientImproved {
|
|
|
134
121
|
],
|
|
135
122
|
jitter: this.options.retryOptions?.jitter ?? true,
|
|
136
123
|
};
|
|
124
|
+
this.defaultHeaders = {
|
|
125
|
+
Accept: "application/json, text/plain, */*",
|
|
126
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
127
|
+
"User-Agent": this.options.userAgent ?? "Hyperttp/0.1.0 Node.js",
|
|
128
|
+
};
|
|
137
129
|
this.agent = new undici_1.Agent({
|
|
138
130
|
connections: 1000,
|
|
139
131
|
pipelining: 10,
|
|
@@ -144,97 +136,85 @@ class HttpClientImproved {
|
|
|
144
136
|
},
|
|
145
137
|
});
|
|
146
138
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const minLevelIndex = levels.indexOf(minLevel);
|
|
154
|
-
if (currentLevelIndex < minLevelIndex)
|
|
155
|
-
return;
|
|
156
|
-
if (this.options.verbose || level !== "info") {
|
|
157
|
-
this.options.logger(level, msg, meta);
|
|
158
|
-
}
|
|
139
|
+
/**
|
|
140
|
+
* Sets default headers that will be applied to all outgoing requests.
|
|
141
|
+
* @param headers An object containing header names and values
|
|
142
|
+
*/
|
|
143
|
+
setDefaultHeaders(headers) {
|
|
144
|
+
Object.assign(this.defaultHeaders, headers);
|
|
159
145
|
}
|
|
160
146
|
/**
|
|
161
|
-
*
|
|
162
|
-
* @
|
|
163
|
-
* @returns SHA1 hash of the body, truncated to 8 characters
|
|
147
|
+
* Returns the cookie jar used for managing HTTP cookies.
|
|
148
|
+
* @returns The CookieJar instance
|
|
164
149
|
*/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return "";
|
|
168
|
-
if (typeof body === "string")
|
|
169
|
-
body = Buffer.from(body, "utf-8");
|
|
170
|
-
return (0, crypto_1.createHash)("sha1").update(body).digest("hex").slice(0, 8);
|
|
150
|
+
getCookieJar() {
|
|
151
|
+
return this.cookieJar;
|
|
171
152
|
}
|
|
172
153
|
/**
|
|
173
|
-
*
|
|
174
|
-
* @param
|
|
175
|
-
* @returns Delay in milliseconds
|
|
154
|
+
* Adds a request interceptor to modify requests before they are sent.
|
|
155
|
+
* @param interceptor The interceptor function to add
|
|
176
156
|
*/
|
|
157
|
+
addRequestInterceptor(interceptor) {
|
|
158
|
+
this.requestInterceptors.push(interceptor);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Adds a response interceptor to modify responses after they are received.
|
|
162
|
+
* @param interceptor The interceptor function to add
|
|
163
|
+
*/
|
|
164
|
+
addResponseInterceptor(interceptor) {
|
|
165
|
+
this.responseInterceptors.push(interceptor);
|
|
166
|
+
}
|
|
167
|
+
/** Closes the HTTP agent to properly terminate keep-alive connections. */
|
|
168
|
+
close() {
|
|
169
|
+
this.agent.close();
|
|
170
|
+
}
|
|
171
|
+
log(level, msg, meta) {
|
|
172
|
+
if (this.options.verbose) {
|
|
173
|
+
if (this.options.logger)
|
|
174
|
+
this.options.logger(level, msg, meta);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async decompress(buf, enc, charset = "utf-8") {
|
|
178
|
+
if (!enc)
|
|
179
|
+
return buf.toString(charset);
|
|
180
|
+
try {
|
|
181
|
+
switch (enc.toLowerCase()) {
|
|
182
|
+
case "gzip":
|
|
183
|
+
return (await gunzip(buf)).toString(charset);
|
|
184
|
+
case "deflate":
|
|
185
|
+
return (await inflate(buf)).toString(charset);
|
|
186
|
+
case "br":
|
|
187
|
+
return (await brotliDecompress(buf)).toString(charset);
|
|
188
|
+
default:
|
|
189
|
+
return buf.toString(charset);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
this.log("error", `Decompression failed for encoding ${enc}`, error);
|
|
194
|
+
return buf.toString(charset);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
177
197
|
calcDelay(attempt) {
|
|
178
198
|
const base = Math.min(this.retryOptions.baseDelay * 2 ** attempt, this.retryOptions.maxDelay);
|
|
179
199
|
return this.retryOptions.jitter
|
|
180
200
|
? base * (0.75 + Math.random() * 0.5)
|
|
181
201
|
: base;
|
|
182
202
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Creates a promise that resolves after the specified delay.
|
|
185
|
-
* @param ms - Delay in milliseconds
|
|
186
|
-
* @returns Promise that resolves after the delay
|
|
187
|
-
*/
|
|
188
203
|
sleep(ms) {
|
|
189
204
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
190
205
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Applies all registered request interceptors to modify the request configuration.
|
|
193
|
-
* Interceptors are executed in sequence, with each one receiving the output of the previous.
|
|
194
|
-
* @param config - Original request configuration
|
|
195
|
-
* @returns Modified request configuration
|
|
196
|
-
*/
|
|
197
206
|
async applyRequestInterceptors(config) {
|
|
198
|
-
if (!this.requestInterceptors.length)
|
|
199
|
-
return config;
|
|
200
207
|
let result = config;
|
|
201
|
-
for (const interceptor of this.requestInterceptors)
|
|
202
|
-
|
|
203
|
-
result = await interceptor(result);
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
this.log("error", "Request interceptor failed", { error });
|
|
207
|
-
throw error;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
208
|
+
for (const interceptor of this.requestInterceptors)
|
|
209
|
+
result = await interceptor(result);
|
|
210
210
|
return result;
|
|
211
211
|
}
|
|
212
|
-
/**
|
|
213
|
-
* Applies all registered response interceptors to modify the response data.
|
|
214
|
-
* Interceptors are executed in sequence, with each one receiving the output of the previous.
|
|
215
|
-
* @param response - Original response data
|
|
216
|
-
* @returns Modified response data
|
|
217
|
-
*/
|
|
218
212
|
async applyResponseInterceptors(response) {
|
|
219
213
|
let result = response;
|
|
220
|
-
for (const interceptor of this.responseInterceptors)
|
|
221
|
-
|
|
222
|
-
result = await interceptor(result);
|
|
223
|
-
}
|
|
224
|
-
catch (error) {
|
|
225
|
-
this.log("error", "Response interceptor failed", { error });
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
214
|
+
for (const interceptor of this.responseInterceptors)
|
|
215
|
+
result = await interceptor(result);
|
|
229
216
|
return result;
|
|
230
217
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Resolves a redirect location relative to the base URL.
|
|
233
|
-
* Handles both absolute and relative redirect URLs.
|
|
234
|
-
* @param location - The redirect location from the response
|
|
235
|
-
* @param baseUrl - The original request URL
|
|
236
|
-
* @returns The resolved absolute URL
|
|
237
|
-
*/
|
|
238
218
|
resolveRedirect(location, baseUrl) {
|
|
239
219
|
try {
|
|
240
220
|
return new URL(location, baseUrl).toString();
|
|
@@ -243,12 +223,6 @@ class HttpClientImproved {
|
|
|
243
223
|
return location;
|
|
244
224
|
}
|
|
245
225
|
}
|
|
246
|
-
/**
|
|
247
|
-
* Parses the Retry-After header to determine when to retry a request.
|
|
248
|
-
* Supports both seconds and HTTP date formats.
|
|
249
|
-
* @param retryAfterHeader - The Retry-After header value
|
|
250
|
-
* @returns Delay in milliseconds, or undefined if not parseable
|
|
251
|
-
*/
|
|
252
226
|
parseRetryAfterMs(retryAfterHeader) {
|
|
253
227
|
if (!retryAfterHeader)
|
|
254
228
|
return undefined;
|
|
@@ -263,159 +237,16 @@ class HttpClientImproved {
|
|
|
263
237
|
return Math.max(0, asDate - Date.now());
|
|
264
238
|
return undefined;
|
|
265
239
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
*/
|
|
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;
|
|
288
|
-
}
|
|
289
|
-
return Buffer.concat(chunks);
|
|
290
|
-
}
|
|
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}`);
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Removes old metrics entries to prevent memory leaks.
|
|
299
|
-
* Keeps only metrics from the last 24 hours.
|
|
300
|
-
*/
|
|
301
|
-
trimMetrics() {
|
|
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);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Sends an HTTP request with retry logic and rate limiting.
|
|
316
|
-
* Handles timeouts, redirects, and various retry scenarios.
|
|
317
|
-
* @param method - HTTP method (GET, POST, etc.)
|
|
318
|
-
* @param url - Target URL
|
|
319
|
-
* @param headers - HTTP headers
|
|
320
|
-
* @param body - Request body (optional)
|
|
321
|
-
* @param metrics - Optional metrics object to track request details
|
|
322
|
-
* @param redirects - Number of redirects followed so far
|
|
323
|
-
* @returns Promise resolving to the response data
|
|
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
|
-
}
|
|
240
|
+
async readBodyWithLimit(body) {
|
|
241
|
+
const buf = Buffer.from(await body.arrayBuffer());
|
|
242
|
+
const limit = this.options.maxResponseBytes;
|
|
243
|
+
if (typeof limit === "number" && limit > 0 && buf.length > limit) {
|
|
244
|
+
throw new Types_1.HttpClientError(`Response too large (${buf.length} bytes), limit is ${limit}`, 0);
|
|
402
245
|
}
|
|
403
|
-
|
|
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);
|
|
246
|
+
return buf;
|
|
413
247
|
}
|
|
414
248
|
async sendWithRetry(method, url, headers, body, metrics, redirects = 0) {
|
|
415
249
|
let lastError;
|
|
416
|
-
if (this.retryOptions.maxRetries === 0) {
|
|
417
|
-
return this.sendOnce(method, url, headers, body, metrics, redirects);
|
|
418
|
-
}
|
|
419
250
|
for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
|
|
420
251
|
try {
|
|
421
252
|
if (this.limiter && this.options.enableRateLimit) {
|
|
@@ -428,7 +259,7 @@ class HttpClientImproved {
|
|
|
428
259
|
body,
|
|
429
260
|
});
|
|
430
261
|
const controller = new AbortController();
|
|
431
|
-
const timeout = this.options.timeout;
|
|
262
|
+
const timeout = this.options.timeout ?? 15000;
|
|
432
263
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
433
264
|
try {
|
|
434
265
|
const res = await (0, undici_1.request)(finalConfig.url, {
|
|
@@ -438,15 +269,6 @@ class HttpClientImproved {
|
|
|
438
269
|
dispatcher: this.agent,
|
|
439
270
|
signal: controller.signal,
|
|
440
271
|
});
|
|
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
|
-
}
|
|
450
272
|
clearTimeout(timer);
|
|
451
273
|
const buf = await this.readBodyWithLimit(res.body);
|
|
452
274
|
let response = await this.applyResponseInterceptors({
|
|
@@ -455,9 +277,7 @@ class HttpClientImproved {
|
|
|
455
277
|
body: buf,
|
|
456
278
|
url: finalConfig.url,
|
|
457
279
|
});
|
|
458
|
-
|
|
459
|
-
throw new Types_1.HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
|
|
460
|
-
}
|
|
280
|
+
// Redirects
|
|
461
281
|
if (this.options.followRedirects &&
|
|
462
282
|
[301, 302, 303, 307, 308].includes(response.status) &&
|
|
463
283
|
redirects < (this.options.maxRedirects ?? 5)) {
|
|
@@ -467,6 +287,7 @@ class HttpClientImproved {
|
|
|
467
287
|
const redirectMethod = response.status === 303 ? "GET" : method;
|
|
468
288
|
const nextHeaders = { ...headers };
|
|
469
289
|
let nextBody = body;
|
|
290
|
+
// If switching to GET, drop body-related headers.
|
|
470
291
|
if (redirectMethod === "GET") {
|
|
471
292
|
nextBody = undefined;
|
|
472
293
|
delete nextHeaders["content-type"];
|
|
@@ -481,6 +302,7 @@ class HttpClientImproved {
|
|
|
481
302
|
return this.sendWithRetry(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, redirects + 1);
|
|
482
303
|
}
|
|
483
304
|
}
|
|
305
|
+
// Retry by status
|
|
484
306
|
if (this.retryOptions.retryStatusCodes.includes(response.status)) {
|
|
485
307
|
metrics && (metrics.retries += 1);
|
|
486
308
|
if (response.status === 429) {
|
|
@@ -532,11 +354,6 @@ class HttpClientImproved {
|
|
|
532
354
|
throw lastError;
|
|
533
355
|
throw new Types_1.HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
|
|
534
356
|
}
|
|
535
|
-
/**
|
|
536
|
-
* Parses the Content-Type header to extract MIME type and character encoding.
|
|
537
|
-
* @param contentType - Content-Type header value
|
|
538
|
-
* @returns Object containing type and charset information
|
|
539
|
-
*/
|
|
540
357
|
parseContentType(contentType) {
|
|
541
358
|
if (!contentType)
|
|
542
359
|
return { type: "text/plain", charset: "utf-8" };
|
|
@@ -560,180 +377,69 @@ class HttpClientImproved {
|
|
|
560
377
|
"base64",
|
|
561
378
|
"hex",
|
|
562
379
|
];
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
* Handles JSON, XML, text, and buffer responses with fallback parsing.
|
|
573
|
-
* @param res - HTTP response object
|
|
574
|
-
* @param responseType - Desired response type
|
|
575
|
-
* @returns Parsed response data
|
|
576
|
-
*/
|
|
380
|
+
const charset = (allowed.includes(normalized)
|
|
381
|
+
? normalized
|
|
382
|
+
: "utf-8");
|
|
383
|
+
return { type, charset };
|
|
384
|
+
}
|
|
385
|
+
xmlParser = new fast_xml_parser_1.XMLParser({
|
|
386
|
+
ignoreAttributes: false,
|
|
387
|
+
allowBooleanAttributes: true,
|
|
388
|
+
});
|
|
577
389
|
async parseResponse(res, responseType) {
|
|
578
|
-
const { type } = this.parseContentType(res.headers["content-type"]);
|
|
579
|
-
const finalType = responseType ?? (type.includes("application/json") ? "json" : "text");
|
|
580
390
|
try {
|
|
391
|
+
const contentType = res.headers["content-type"] || "";
|
|
392
|
+
const text = await this.decompress(res.body, res.headers["content-encoding"]);
|
|
393
|
+
const finalType = responseType ?? "json";
|
|
581
394
|
switch (finalType) {
|
|
582
|
-
case "json":
|
|
583
|
-
|
|
395
|
+
case "json": {
|
|
396
|
+
if (contentType.includes("json")) {
|
|
397
|
+
return JSON.parse(text);
|
|
398
|
+
}
|
|
399
|
+
if (text.trim().startsWith("<")) {
|
|
400
|
+
return this.xmlParser.parse(text);
|
|
401
|
+
}
|
|
402
|
+
return { text };
|
|
403
|
+
}
|
|
584
404
|
case "xml": {
|
|
405
|
+
const text = await this.decompress(res.body, res.headers["content-encoding"]);
|
|
406
|
+
if (text.trim().startsWith("<")) {
|
|
407
|
+
return text;
|
|
408
|
+
}
|
|
585
409
|
try {
|
|
586
|
-
const
|
|
587
|
-
return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root:
|
|
410
|
+
const json = JSON.parse(text);
|
|
411
|
+
return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root: json });
|
|
588
412
|
}
|
|
589
413
|
catch {
|
|
590
|
-
return
|
|
414
|
+
return String(text);
|
|
591
415
|
}
|
|
592
416
|
}
|
|
593
417
|
case "text":
|
|
594
|
-
return
|
|
418
|
+
return text;
|
|
595
419
|
case "buffer":
|
|
596
420
|
return res.body;
|
|
597
421
|
case "stream":
|
|
598
|
-
|
|
422
|
+
return res.body;
|
|
599
423
|
default:
|
|
600
|
-
return
|
|
424
|
+
return text;
|
|
601
425
|
}
|
|
602
426
|
}
|
|
603
|
-
catch (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
* @param method - HTTP method
|
|
611
|
-
* @param req - Request configuration
|
|
612
|
-
* @param responseType - Expected response type
|
|
613
|
-
* @returns Promise resolving to the response data
|
|
614
|
-
*/
|
|
615
|
-
async requestInternalWithoutCache(method, req, responseType) {
|
|
616
|
-
const url = req.getURL();
|
|
617
|
-
const rawBody = req.getBodyData();
|
|
618
|
-
const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
|
|
619
|
-
const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
620
|
-
let body;
|
|
621
|
-
const contentType = headers["content-type"] || headers["Content-Type"] || "";
|
|
622
|
-
if (isBodyAllowed && rawBody !== undefined && rawBody !== null) {
|
|
623
|
-
if (Buffer.isBuffer(rawBody)) {
|
|
624
|
-
body = rawBody;
|
|
625
|
-
}
|
|
626
|
-
else if (typeof rawBody === "string") {
|
|
627
|
-
body = rawBody;
|
|
628
|
-
}
|
|
629
|
-
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
630
|
-
body = new url_1.URLSearchParams(rawBody).toString();
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
body = JSON.stringify(rawBody);
|
|
634
|
-
if (!contentType)
|
|
635
|
-
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
const metrics = {
|
|
639
|
-
startTime: Date.now(),
|
|
640
|
-
endTime: 0,
|
|
641
|
-
duration: 0,
|
|
642
|
-
bytesReceived: 0,
|
|
643
|
-
bytesSent: 0,
|
|
644
|
-
retries: 0,
|
|
645
|
-
cached: false,
|
|
646
|
-
url,
|
|
647
|
-
method,
|
|
648
|
-
};
|
|
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
|
-
})());
|
|
678
|
-
metrics.endTime = Date.now();
|
|
679
|
-
metrics.duration = metrics.endTime - metrics.startTime;
|
|
680
|
-
this.requestMetrics.set(url, metrics);
|
|
681
|
-
this.trimMetrics();
|
|
682
|
-
return result;
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Makes an HTTP request with caching support.
|
|
686
|
-
* Handles cache lookups, request deduplication, and automatic cache storage.
|
|
687
|
-
* @param method - HTTP method
|
|
688
|
-
* @param req - Request configuration
|
|
689
|
-
* @param useCache - Whether to use caching (default: true)
|
|
690
|
-
* @param responseType - Expected response type
|
|
691
|
-
* @returns Promise resolving to the response data
|
|
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;
|
|
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);
|
|
719
434
|
}
|
|
720
|
-
return Buffer.from(await res.body.arrayBuffer());
|
|
721
435
|
}
|
|
722
436
|
async requestInternal(method, req, useCache = true, responseType) {
|
|
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) {
|
|
732
|
-
return this.requestInternalWithoutCache(method, req, responseType);
|
|
733
|
-
}
|
|
734
437
|
const url = req.getURL();
|
|
735
438
|
const rawBody = req.getBodyData();
|
|
736
|
-
const headers =
|
|
439
|
+
const headers = {
|
|
440
|
+
...this.defaultHeaders,
|
|
441
|
+
...req.getHeaders(),
|
|
442
|
+
};
|
|
737
443
|
const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
738
444
|
let body;
|
|
739
445
|
const contentType = headers["content-type"] || headers["Content-Type"] || "";
|
|
@@ -745,31 +451,26 @@ class HttpClientImproved {
|
|
|
745
451
|
body = rawBody;
|
|
746
452
|
}
|
|
747
453
|
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
748
|
-
body = new
|
|
454
|
+
body = new URLSearchParams(rawBody).toString();
|
|
749
455
|
}
|
|
750
456
|
else {
|
|
751
457
|
body = JSON.stringify(rawBody);
|
|
752
|
-
if (!contentType)
|
|
458
|
+
if (!contentType) {
|
|
753
459
|
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
460
|
+
}
|
|
754
461
|
}
|
|
755
462
|
}
|
|
756
|
-
|
|
757
|
-
if (
|
|
758
|
-
const
|
|
759
|
-
|
|
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) {
|
|
463
|
+
const key = `${method}:${url}:${body ?? ""}`;
|
|
464
|
+
if (method === "GET" && useCache && this.cache) {
|
|
465
|
+
const cached = await this.cache.get(key);
|
|
466
|
+
if (cached) {
|
|
766
467
|
this.log("debug", `Cache hit for ${url}`);
|
|
767
468
|
return cached;
|
|
768
469
|
}
|
|
769
470
|
}
|
|
770
|
-
if (
|
|
471
|
+
if (this.inflight.has(key)) {
|
|
771
472
|
this.log("debug", `Deduplicating request for ${url}`);
|
|
772
|
-
return this.inflight.get(
|
|
473
|
+
return this.inflight.get(key);
|
|
773
474
|
}
|
|
774
475
|
const promise = (async () => {
|
|
775
476
|
const metrics = {
|
|
@@ -784,80 +485,53 @@ class HttpClientImproved {
|
|
|
784
485
|
method,
|
|
785
486
|
};
|
|
786
487
|
try {
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
})());
|
|
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 || "");
|
|
494
|
+
if (method === "HEAD") {
|
|
495
|
+
metrics.endTime = Date.now();
|
|
496
|
+
metrics.duration = metrics.endTime - metrics.startTime;
|
|
497
|
+
this.requestMetrics.set(key, metrics);
|
|
498
|
+
this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
|
|
499
|
+
return {
|
|
500
|
+
status: res.status,
|
|
501
|
+
headers: res.headers,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const parsed = await this.parseResponse(res, responseType);
|
|
505
|
+
if (method === "GET" && useCache && this.cache) {
|
|
506
|
+
this.cache.set(key, parsed);
|
|
507
|
+
metrics.cached = true;
|
|
508
|
+
}
|
|
509
|
+
result = parsed;
|
|
831
510
|
metrics.endTime = Date.now();
|
|
832
511
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
833
|
-
this.requestMetrics.set(
|
|
834
|
-
this.trimMetrics();
|
|
512
|
+
this.requestMetrics.set(key, metrics);
|
|
835
513
|
this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
|
|
836
514
|
return result;
|
|
837
515
|
}
|
|
838
516
|
catch (error) {
|
|
839
517
|
metrics.endTime = Date.now();
|
|
840
518
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
841
|
-
this.requestMetrics.set(
|
|
842
|
-
this.trimMetrics();
|
|
519
|
+
this.requestMetrics.set(key, metrics);
|
|
843
520
|
throw error;
|
|
844
521
|
}
|
|
845
522
|
finally {
|
|
846
|
-
|
|
847
|
-
this.inflight.delete(cacheKey);
|
|
523
|
+
this.inflight.delete(key);
|
|
848
524
|
}
|
|
849
525
|
})();
|
|
850
|
-
|
|
851
|
-
this.inflight.set(cacheKey, promise);
|
|
526
|
+
this.inflight.set(key, promise);
|
|
852
527
|
return promise;
|
|
853
528
|
}
|
|
854
529
|
/**
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
* @
|
|
859
|
-
* @
|
|
860
|
-
* @returns Promise resolving to the response data
|
|
530
|
+
* Performs an HTTP GET request.
|
|
531
|
+
* @param req The request object containing URL and headers
|
|
532
|
+
* @param responseType Optional response parsing type
|
|
533
|
+
* @returns A promise that resolves to the parsed response
|
|
534
|
+
* @template T The expected response type
|
|
861
535
|
*/
|
|
862
536
|
get(req, responseType = "json") {
|
|
863
537
|
if (typeof req === "string") {
|
|
@@ -873,13 +547,11 @@ class HttpClientImproved {
|
|
|
873
547
|
}
|
|
874
548
|
}
|
|
875
549
|
/**
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
* @
|
|
880
|
-
* @
|
|
881
|
-
* @param responseType - Expected response type (default: "json")
|
|
882
|
-
* @returns Promise resolving to the response data
|
|
550
|
+
* Performs an HTTP POST request.
|
|
551
|
+
* @param req The request object containing URL, body, and headers
|
|
552
|
+
* @param responseType Optional response parsing type
|
|
553
|
+
* @returns A promise that resolves to the parsed response
|
|
554
|
+
* @template T The expected response type
|
|
883
555
|
*/
|
|
884
556
|
post(req, body, responseType = "json") {
|
|
885
557
|
if (typeof req === "string") {
|
|
@@ -895,13 +567,11 @@ class HttpClientImproved {
|
|
|
895
567
|
}
|
|
896
568
|
}
|
|
897
569
|
/**
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
* @
|
|
902
|
-
* @
|
|
903
|
-
* @param responseType - Expected response type (default: "json")
|
|
904
|
-
* @returns Promise resolving to the response data
|
|
570
|
+
* Performs an HTTP PUT request.
|
|
571
|
+
* @param req The request object containing URL, body, and headers
|
|
572
|
+
* @param responseType Optional response parsing type
|
|
573
|
+
* @returns A promise that resolves to the parsed response
|
|
574
|
+
* @template T The expected response type
|
|
905
575
|
*/
|
|
906
576
|
put(req, body, responseType = "json") {
|
|
907
577
|
if (typeof req === "string") {
|
|
@@ -914,6 +584,43 @@ class HttpClientImproved {
|
|
|
914
584
|
}
|
|
915
585
|
return this.requestInternal("PUT", req, false, responseType);
|
|
916
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Performs an HTTP DELETE request.
|
|
589
|
+
* @param req The request object containing URL and headers
|
|
590
|
+
* @param responseType Optional response parsing type
|
|
591
|
+
* @returns A promise that resolves to the parsed response
|
|
592
|
+
* @template T The expected response type
|
|
593
|
+
*/
|
|
594
|
+
delete(req, responseType = "json") {
|
|
595
|
+
if (typeof req === "string") {
|
|
596
|
+
const client = new HttpClientImproved();
|
|
597
|
+
const simpleReq = {
|
|
598
|
+
getURL: () => req,
|
|
599
|
+
getBodyData: () => undefined,
|
|
600
|
+
getHeaders: () => ({}),
|
|
601
|
+
};
|
|
602
|
+
return client.delete(simpleReq, responseType);
|
|
603
|
+
}
|
|
604
|
+
return this.requestInternal("DELETE", req, false, responseType);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Performs an HTTP PATCH request.
|
|
608
|
+
* @param req The request object containing URL, body, and headers
|
|
609
|
+
* @param responseType Optional response parsing type
|
|
610
|
+
* @returns A promise that resolves to the parsed response
|
|
611
|
+
* @template T The expected response type
|
|
612
|
+
*/
|
|
613
|
+
patch(req, body, responseType = "json") {
|
|
614
|
+
if (typeof req === "string") {
|
|
615
|
+
const simpleReq = {
|
|
616
|
+
getURL: () => req,
|
|
617
|
+
getBodyData: () => body,
|
|
618
|
+
getHeaders: () => ({ "Content-Type": "application/json" }),
|
|
619
|
+
};
|
|
620
|
+
return this.requestInternal("PATCH", simpleReq, false, responseType);
|
|
621
|
+
}
|
|
622
|
+
return this.requestInternal("PATCH", req, false, responseType);
|
|
623
|
+
}
|
|
917
624
|
/**
|
|
918
625
|
* @ru Получает потоковый ответ (для SSE, больших файлов).
|
|
919
626
|
* @en Gets streaming response (for SSE, large files).
|
|
@@ -930,7 +637,10 @@ class HttpClientImproved {
|
|
|
930
637
|
return (this.queue && this.options.enableQueue
|
|
931
638
|
? this.queue.enqueue(async function () {
|
|
932
639
|
const url = req.getURL();
|
|
933
|
-
const headers =
|
|
640
|
+
const headers = {
|
|
641
|
+
...this.defaultHeaders,
|
|
642
|
+
...req.getHeaders(),
|
|
643
|
+
};
|
|
934
644
|
const response = await (0, undici_1.request)(url, {
|
|
935
645
|
method: "GET",
|
|
936
646
|
headers,
|
|
@@ -945,7 +655,7 @@ class HttpClientImproved {
|
|
|
945
655
|
}.bind(this))
|
|
946
656
|
: async function () {
|
|
947
657
|
const url = req.getURL();
|
|
948
|
-
const headers = Object.assign({}, this.
|
|
658
|
+
const headers = Object.assign({}, this.defaultHeaders, req.getHeaders());
|
|
949
659
|
const response = await (0, undici_1.request)(url, {
|
|
950
660
|
method: "GET",
|
|
951
661
|
headers,
|
|
@@ -960,49 +670,9 @@ class HttpClientImproved {
|
|
|
960
670
|
}.bind(this)());
|
|
961
671
|
}
|
|
962
672
|
/**
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
* @param req - Request configuration or URL string
|
|
967
|
-
* @param responseType - Expected response type (default: "json")
|
|
968
|
-
* @returns Promise resolving to the response data
|
|
969
|
-
*/
|
|
970
|
-
delete(req, responseType = "json") {
|
|
971
|
-
if (typeof req === "string") {
|
|
972
|
-
const client = defaultClient ?? (defaultClient = new HttpClientImproved());
|
|
973
|
-
const simpleReq = {
|
|
974
|
-
getURL: () => req,
|
|
975
|
-
getBodyData: () => undefined,
|
|
976
|
-
getHeaders: () => ({}),
|
|
977
|
-
};
|
|
978
|
-
return client.delete(simpleReq, responseType);
|
|
979
|
-
}
|
|
980
|
-
return this.requestInternal("DELETE", req, false, responseType);
|
|
981
|
-
}
|
|
982
|
-
/**
|
|
983
|
-
* Makes an HTTP PATCH request.
|
|
984
|
-
* PATCH requests are not cached by default due to their side effects.
|
|
985
|
-
* @param req - Request configuration
|
|
986
|
-
* @param responseType - Expected response type (default: "json")
|
|
987
|
-
* @returns Promise resolving to the response data
|
|
988
|
-
*/
|
|
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
|
-
}
|
|
998
|
-
return this.requestInternal("PATCH", req, false, responseType);
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* Makes an HTTP HEAD request.
|
|
1002
|
-
* Returns only the status code and headers without the response body.
|
|
1003
|
-
* HEAD requests are not cached by default.
|
|
1004
|
-
* @param req - Request configuration or URL string
|
|
1005
|
-
* @returns Promise resolving to status and headers
|
|
673
|
+
* Performs an HTTP HEAD request.
|
|
674
|
+
* @param req The request object containing URL and headers
|
|
675
|
+
* @returns A promise that resolves when the request completes
|
|
1006
676
|
*/
|
|
1007
677
|
async head(req) {
|
|
1008
678
|
if (typeof req === "string") {
|
|
@@ -1016,8 +686,7 @@ class HttpClientImproved {
|
|
|
1016
686
|
return this.requestInternal("HEAD", req, false);
|
|
1017
687
|
}
|
|
1018
688
|
/**
|
|
1019
|
-
* Clears the
|
|
1020
|
-
* Removes all cached responses and resets the cache state.
|
|
689
|
+
* Clears the request cache.
|
|
1021
690
|
*/
|
|
1022
691
|
async clearCache() {
|
|
1023
692
|
if (this.cache) {
|
|
@@ -1055,12 +724,11 @@ class HttpClientImproved {
|
|
|
1055
724
|
* @returns RequestBuilder instance for chaining
|
|
1056
725
|
*/
|
|
1057
726
|
request(url) {
|
|
1058
|
-
return new RequestBuilder(url);
|
|
727
|
+
return new RequestBuilder_1.RequestBuilder(url);
|
|
1059
728
|
}
|
|
1060
729
|
/**
|
|
1061
730
|
* Returns current statistics about the HTTP client's state.
|
|
1062
|
-
*
|
|
1063
|
-
* @returns Object containing various client statistics
|
|
731
|
+
* @returns An object containing cache size, request counts, and rate limit information
|
|
1064
732
|
*/
|
|
1065
733
|
getStats() {
|
|
1066
734
|
return {
|
|
@@ -1075,173 +743,8 @@ class HttpClientImproved {
|
|
|
1075
743
|
currentRateLimit: this.limiter && this.options.enableRateLimit
|
|
1076
744
|
? (this.limiter.currentCount ?? 0)
|
|
1077
745
|
: 0,
|
|
1078
|
-
metricsSize: this.requestMetrics.size,
|
|
1079
746
|
};
|
|
1080
747
|
}
|
|
1081
748
|
}
|
|
1082
749
|
exports.default = HttpClientImproved;
|
|
1083
|
-
/**
|
|
1084
|
-
* Fluent request builder for making HTTP requests with a chainable API.
|
|
1085
|
-
* Provides a convenient way to build and send HTTP requests with various options.
|
|
1086
|
-
*
|
|
1087
|
-
* @example
|
|
1088
|
-
* ```ts
|
|
1089
|
-
* const client = new HttpClientImproved();
|
|
1090
|
-
* const response = await client.request('https://api.example.com/data')
|
|
1091
|
-
* .headers({ 'Authorization': 'Bearer token' })
|
|
1092
|
-
* .query({ limit: 10, offset: 0 })
|
|
1093
|
-
* .json()
|
|
1094
|
-
* .send();
|
|
1095
|
-
* ```
|
|
1096
|
-
*/
|
|
1097
|
-
class RequestBuilder {
|
|
1098
|
-
_url;
|
|
1099
|
-
_method = "GET";
|
|
1100
|
-
_headers = {};
|
|
1101
|
-
_body;
|
|
1102
|
-
_responseType = "json";
|
|
1103
|
-
/**
|
|
1104
|
-
* Creates a new request builder for the specified URL.
|
|
1105
|
-
* @param url - The target URL for the request
|
|
1106
|
-
*/
|
|
1107
|
-
constructor(url) {
|
|
1108
|
-
this._url = url;
|
|
1109
|
-
}
|
|
1110
|
-
/**
|
|
1111
|
-
* Sets HTTP headers for the request.
|
|
1112
|
-
* @param headers - Object containing header key-value pairs
|
|
1113
|
-
* @returns The builder instance for chaining
|
|
1114
|
-
*/
|
|
1115
|
-
headers(headers) {
|
|
1116
|
-
this._headers = headers;
|
|
1117
|
-
return this;
|
|
1118
|
-
}
|
|
1119
|
-
/**
|
|
1120
|
-
* Sets the request body data.
|
|
1121
|
-
* @param bodyData - The body data to send with the request
|
|
1122
|
-
* @returns The builder instance for chaining
|
|
1123
|
-
*/
|
|
1124
|
-
body(bodyData) {
|
|
1125
|
-
this._body = bodyData;
|
|
1126
|
-
return this;
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Sets the response type to JSON.
|
|
1130
|
-
* @returns The builder instance for chaining
|
|
1131
|
-
*/
|
|
1132
|
-
json() {
|
|
1133
|
-
this._responseType = "json";
|
|
1134
|
-
return this;
|
|
1135
|
-
}
|
|
1136
|
-
/**
|
|
1137
|
-
* Sets the response type to plain text.
|
|
1138
|
-
* @returns The builder instance for chaining
|
|
1139
|
-
*/
|
|
1140
|
-
text() {
|
|
1141
|
-
this._responseType = "text";
|
|
1142
|
-
return this;
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Sets the response type to XML.
|
|
1146
|
-
* @returns The builder instance for chaining
|
|
1147
|
-
*/
|
|
1148
|
-
xml() {
|
|
1149
|
-
this._responseType = "xml";
|
|
1150
|
-
return this;
|
|
1151
|
-
}
|
|
1152
|
-
/**
|
|
1153
|
-
* Sets the HTTP method to POST.
|
|
1154
|
-
* @returns The builder instance for chaining
|
|
1155
|
-
*/
|
|
1156
|
-
post() {
|
|
1157
|
-
this._method = "POST";
|
|
1158
|
-
return this;
|
|
1159
|
-
}
|
|
1160
|
-
/**
|
|
1161
|
-
* @ru Устанавливает потоковый режим ответа.
|
|
1162
|
-
* @en Sets streaming response mode.
|
|
1163
|
-
*/
|
|
1164
|
-
stream() {
|
|
1165
|
-
this._responseType = "stream";
|
|
1166
|
-
return this;
|
|
1167
|
-
}
|
|
1168
|
-
/**
|
|
1169
|
-
* Sets the HTTP method to PUT.
|
|
1170
|
-
* @returns The builder instance for chaining
|
|
1171
|
-
*/
|
|
1172
|
-
put() {
|
|
1173
|
-
this._method = "PUT";
|
|
1174
|
-
return this;
|
|
1175
|
-
}
|
|
1176
|
-
/**
|
|
1177
|
-
* Sets the HTTP method to PATCH.
|
|
1178
|
-
* @returns The builder instance for chaining
|
|
1179
|
-
*/
|
|
1180
|
-
patch() {
|
|
1181
|
-
this._method = "PATCH";
|
|
1182
|
-
return this;
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* Sets the HTTP method to DELETE.
|
|
1186
|
-
* @returns The builder instance for chaining
|
|
1187
|
-
*/
|
|
1188
|
-
delete() {
|
|
1189
|
-
this._method = "DELETE";
|
|
1190
|
-
return this;
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Adds query parameters to the URL.
|
|
1194
|
-
* @param params - Object containing query parameter key-value pairs
|
|
1195
|
-
* @returns The builder instance for chaining
|
|
1196
|
-
*/
|
|
1197
|
-
query(params) {
|
|
1198
|
-
const urlObj = new URL(this._url);
|
|
1199
|
-
Object.entries(params).forEach(([k, v]) => urlObj.searchParams.set(k, String(v)));
|
|
1200
|
-
this._url = urlObj.toString();
|
|
1201
|
-
return this;
|
|
1202
|
-
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Sets a JSON body for the request.
|
|
1205
|
-
* Automatically sets the Content-Type header to application/json.
|
|
1206
|
-
* @param body - The JSON body data
|
|
1207
|
-
* @returns The builder instance for chaining
|
|
1208
|
-
*/
|
|
1209
|
-
jsonBody(body) {
|
|
1210
|
-
this._body = body;
|
|
1211
|
-
this._headers["Content-Type"] = "application/json; charset=utf-8";
|
|
1212
|
-
return this;
|
|
1213
|
-
}
|
|
1214
|
-
/**
|
|
1215
|
-
* Sends the HTTP request and returns the response.
|
|
1216
|
-
* @returns Promise resolving to the response data
|
|
1217
|
-
*/
|
|
1218
|
-
async send() {
|
|
1219
|
-
const client = defaultClient ?? (defaultClient = new HttpClientImproved());
|
|
1220
|
-
const req = {
|
|
1221
|
-
getURL: () => this._url,
|
|
1222
|
-
getBodyData: () => this._body,
|
|
1223
|
-
getHeaders: () => this._headers,
|
|
1224
|
-
};
|
|
1225
|
-
switch (this._method) {
|
|
1226
|
-
case "GET":
|
|
1227
|
-
if (this._responseType === "stream") {
|
|
1228
|
-
return client.stream(req);
|
|
1229
|
-
}
|
|
1230
|
-
return client.get(req, this._responseType);
|
|
1231
|
-
case "POST":
|
|
1232
|
-
return client.post(req, this._body, this._responseType);
|
|
1233
|
-
case "PUT":
|
|
1234
|
-
return client.put(req, this._body, this._responseType);
|
|
1235
|
-
case "DELETE":
|
|
1236
|
-
return client.delete(req, this._responseType);
|
|
1237
|
-
case "PATCH":
|
|
1238
|
-
return client.patch(req, this._body, this._responseType);
|
|
1239
|
-
default:
|
|
1240
|
-
if (this._responseType === "stream") {
|
|
1241
|
-
return client.stream(req);
|
|
1242
|
-
}
|
|
1243
|
-
return client.get(req, this._responseType);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
750
|
//# sourceMappingURL=HttpClientImproved.js.map
|