rezo 1.0.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/LICENSE +202 -0
- package/README.md +1507 -0
- package/assets/icon.svg +37 -0
- package/assets/logo-dark.svg +47 -0
- package/assets/logo.svg +58 -0
- package/dist/adapters/curl.cjs +1034 -0
- package/dist/adapters/curl.js +1031 -0
- package/dist/adapters/entries/curl.cjs +4 -0
- package/dist/adapters/entries/curl.d.ts +2136 -0
- package/dist/adapters/entries/curl.js +2 -0
- package/dist/adapters/entries/fetch.cjs +2 -0
- package/dist/adapters/entries/fetch.d.ts +2127 -0
- package/dist/adapters/entries/fetch.js +1 -0
- package/dist/adapters/entries/http.cjs +2 -0
- package/dist/adapters/entries/http.d.ts +2126 -0
- package/dist/adapters/entries/http.js +1 -0
- package/dist/adapters/entries/http2.cjs +4 -0
- package/dist/adapters/entries/http2.d.ts +2136 -0
- package/dist/adapters/entries/http2.js +2 -0
- package/dist/adapters/entries/react-native.cjs +2 -0
- package/dist/adapters/entries/react-native.d.ts +2126 -0
- package/dist/adapters/entries/react-native.js +1 -0
- package/dist/adapters/entries/xhr.cjs +2 -0
- package/dist/adapters/entries/xhr.d.ts +2127 -0
- package/dist/adapters/entries/xhr.js +1 -0
- package/dist/adapters/fetch.cjs +740 -0
- package/dist/adapters/fetch.js +739 -0
- package/dist/adapters/http.cjs +1153 -0
- package/dist/adapters/http.js +1151 -0
- package/dist/adapters/http2.cjs +957 -0
- package/dist/adapters/http2.js +956 -0
- package/dist/adapters/index.cjs +6 -0
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/picker.cjs +342 -0
- package/dist/adapters/picker.js +331 -0
- package/dist/adapters/react-native.cjs +545 -0
- package/dist/adapters/react-native.js +544 -0
- package/dist/adapters/xhr.cjs +622 -0
- package/dist/adapters/xhr.js +621 -0
- package/dist/cache/dns-cache.cjs +118 -0
- package/dist/cache/dns-cache.js +113 -0
- package/dist/cache/file-cacher.cjs +264 -0
- package/dist/cache/file-cacher.js +261 -0
- package/dist/cache/index.cjs +13 -0
- package/dist/cache/index.js +5 -0
- package/dist/cache/lru-cache.cjs +96 -0
- package/dist/cache/lru-cache.js +93 -0
- package/dist/cache/response-cache.cjs +314 -0
- package/dist/cache/response-cache.js +310 -0
- package/dist/cache/url-store.cjs +288 -0
- package/dist/cache/url-store.js +285 -0
- package/dist/core/hooks.cjs +133 -0
- package/dist/core/hooks.js +120 -0
- package/dist/core/rezo.cjs +464 -0
- package/dist/core/rezo.js +458 -0
- package/dist/crawler.d.ts +6255 -0
- package/dist/dom/index.cjs +1 -0
- package/dist/dom/index.d.ts +23 -0
- package/dist/dom/index.js +1 -0
- package/dist/entries/crawler.cjs +5 -0
- package/dist/entries/crawler.js +2 -0
- package/dist/errors/rezo-error.cjs +722 -0
- package/dist/errors/rezo-error.js +716 -0
- package/dist/index.cjs +34 -0
- package/dist/index.d.ts +3335 -0
- package/dist/index.js +26 -0
- package/dist/platform/browser.cjs +9 -0
- package/dist/platform/browser.d.ts +3203 -0
- package/dist/platform/browser.js +7 -0
- package/dist/platform/bun.cjs +9 -0
- package/dist/platform/bun.d.ts +3203 -0
- package/dist/platform/bun.js +7 -0
- package/dist/platform/deno.cjs +9 -0
- package/dist/platform/deno.d.ts +3203 -0
- package/dist/platform/deno.js +7 -0
- package/dist/platform/node.cjs +9 -0
- package/dist/platform/node.d.ts +3203 -0
- package/dist/platform/node.js +7 -0
- package/dist/platform/react-native.cjs +9 -0
- package/dist/platform/react-native.d.ts +3203 -0
- package/dist/platform/react-native.js +7 -0
- package/dist/platform/worker.cjs +9 -0
- package/dist/platform/worker.d.ts +3203 -0
- package/dist/platform/worker.js +7 -0
- package/dist/plugin/addon/decodo/index.cjs +1 -0
- package/dist/plugin/addon/decodo/index.js +1 -0
- package/dist/plugin/addon/decodo/options.cjs +1 -0
- package/dist/plugin/addon/decodo/options.js +1 -0
- package/dist/plugin/addon/oxylabs/index.cjs +1 -0
- package/dist/plugin/addon/oxylabs/index.js +1 -0
- package/dist/plugin/addon/oxylabs/options.cjs +1 -0
- package/dist/plugin/addon/oxylabs/options.js +1 -0
- package/dist/plugin/crawler-options.cjs +1 -0
- package/dist/plugin/crawler-options.js +1 -0
- package/dist/plugin/crawler.cjs +519 -0
- package/dist/plugin/crawler.js +517 -0
- package/dist/plugin/index.cjs +36 -0
- package/dist/plugin/index.js +32 -0
- package/dist/proxy/index.cjs +142 -0
- package/dist/proxy/index.js +139 -0
- package/dist/responses/buildError.cjs +452 -0
- package/dist/responses/buildError.js +441 -0
- package/dist/responses/buildResponse.cjs +365 -0
- package/dist/responses/buildResponse.js +361 -0
- package/dist/responses/download.cjs +54 -0
- package/dist/responses/download.js +52 -0
- package/dist/responses/stream.cjs +60 -0
- package/dist/responses/stream.js +58 -0
- package/dist/responses/upload.cjs +54 -0
- package/dist/responses/upload.js +52 -0
- package/dist/types/cookies.cjs +394 -0
- package/dist/types/cookies.js +391 -0
- package/dist/types/download.cjs +10 -0
- package/dist/types/download.js +10 -0
- package/dist/types/rezo-request.cjs +131 -0
- package/dist/types/rezo-request.js +131 -0
- package/dist/utils/agent-merger.cjs +111 -0
- package/dist/utils/agent-merger.js +108 -0
- package/dist/utils/compression.cjs +84 -0
- package/dist/utils/compression.js +82 -0
- package/dist/utils/cookies.cjs +514 -0
- package/dist/utils/cookies.js +511 -0
- package/dist/utils/data-operations.cjs +75 -0
- package/dist/utils/data-operations.js +73 -0
- package/dist/utils/form-data.cjs +164 -0
- package/dist/utils/form-data.js +161 -0
- package/dist/utils/headers.cjs +162 -0
- package/dist/utils/headers.js +161 -0
- package/dist/utils/http-config.cjs +723 -0
- package/dist/utils/http-config.js +718 -0
- package/dist/utils/index.cjs +8 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/tools.cjs +18 -0
- package/dist/utils/tools.js +15 -0
- package/package.json +172 -0
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
import * as http2 from "node:http2";
|
|
2
|
+
import * as zlib from "node:zlib";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { RezoError } from '../errors/rezo-error.js';
|
|
6
|
+
import { buildSmartError, buildDecompressionError, builErrorFromResponse, buildDownloadError } from '../responses/buildError.js';
|
|
7
|
+
import { Cookie } from '../utils/cookies.js';
|
|
8
|
+
import RezoFormData from '../utils/form-data.js';
|
|
9
|
+
import { getDefaultConfig, prepareHTTPOptions } from '../utils/http-config.js';
|
|
10
|
+
import { RezoHeaders } from '../utils/headers.js';
|
|
11
|
+
import { RezoURLSearchParams } from '../utils/data-operations.js';
|
|
12
|
+
import { StreamResponse } from '../responses/stream.js';
|
|
13
|
+
import { DownloadResponse } from '../responses/download.js';
|
|
14
|
+
import { UploadResponse } from '../responses/upload.js';
|
|
15
|
+
import { isSameDomain, RezoPerformance } from '../utils/tools.js';
|
|
16
|
+
import { ResponseCache } from '../cache/response-cache.js';
|
|
17
|
+
let zstdDecompressSync = null;
|
|
18
|
+
let zstdChecked = false;
|
|
19
|
+
async function decompressBuffer(buffer, contentEncoding) {
|
|
20
|
+
const encoding = contentEncoding.toLowerCase();
|
|
21
|
+
switch (encoding) {
|
|
22
|
+
case "gzip":
|
|
23
|
+
case "x-gzip":
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
zlib.gunzip(buffer, (err, result) => {
|
|
26
|
+
if (err)
|
|
27
|
+
reject(err);
|
|
28
|
+
else
|
|
29
|
+
resolve(result);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
case "deflate":
|
|
33
|
+
case "x-deflate":
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
zlib.inflate(buffer, (err, result) => {
|
|
36
|
+
if (err)
|
|
37
|
+
reject(err);
|
|
38
|
+
else
|
|
39
|
+
resolve(result);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
case "br":
|
|
43
|
+
case "brotli":
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
zlib.brotliDecompress(buffer, (err, result) => {
|
|
46
|
+
if (err)
|
|
47
|
+
reject(err);
|
|
48
|
+
else
|
|
49
|
+
resolve(result);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
case "zstd":
|
|
53
|
+
if (!zstdChecked) {
|
|
54
|
+
zstdChecked = true;
|
|
55
|
+
try {
|
|
56
|
+
const zlibModule = await import("node:zlib");
|
|
57
|
+
if (typeof zlibModule.zstdDecompressSync === "function") {
|
|
58
|
+
zstdDecompressSync = zlibModule.zstdDecompressSync;
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
if (!zstdDecompressSync) {
|
|
63
|
+
throw new Error("zstd decompression not available: requires Node.js 22.15+ with native zstd support");
|
|
64
|
+
}
|
|
65
|
+
return zstdDecompressSync(buffer);
|
|
66
|
+
default:
|
|
67
|
+
return buffer;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class Http2SessionPool {
|
|
72
|
+
static instance;
|
|
73
|
+
sessions = new Map;
|
|
74
|
+
cleanupInterval = null;
|
|
75
|
+
SESSION_TIMEOUT = 60000;
|
|
76
|
+
CLEANUP_INTERVAL = 30000;
|
|
77
|
+
static getInstance() {
|
|
78
|
+
if (!Http2SessionPool.instance) {
|
|
79
|
+
Http2SessionPool.instance = new Http2SessionPool;
|
|
80
|
+
}
|
|
81
|
+
return Http2SessionPool.instance;
|
|
82
|
+
}
|
|
83
|
+
constructor() {
|
|
84
|
+
this.startCleanup();
|
|
85
|
+
}
|
|
86
|
+
startCleanup() {
|
|
87
|
+
if (this.cleanupInterval)
|
|
88
|
+
return;
|
|
89
|
+
this.cleanupInterval = setInterval(() => {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
for (const [key, entry] of this.sessions.entries()) {
|
|
92
|
+
if (entry.refCount === 0 && now - entry.lastUsed > this.SESSION_TIMEOUT) {
|
|
93
|
+
entry.session.close();
|
|
94
|
+
this.sessions.delete(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}, this.CLEANUP_INTERVAL);
|
|
98
|
+
if (this.cleanupInterval.unref) {
|
|
99
|
+
this.cleanupInterval.unref();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
getSessionKey(url, options) {
|
|
103
|
+
return `${url.protocol}//${url.host}`;
|
|
104
|
+
}
|
|
105
|
+
async getSession(url, options, timeout) {
|
|
106
|
+
const key = this.getSessionKey(url, options);
|
|
107
|
+
const existing = this.sessions.get(key);
|
|
108
|
+
if (existing && !existing.session.closed && !existing.session.destroyed) {
|
|
109
|
+
existing.lastUsed = Date.now();
|
|
110
|
+
existing.refCount++;
|
|
111
|
+
return existing.session;
|
|
112
|
+
}
|
|
113
|
+
const session = await this.createSession(url, options, timeout);
|
|
114
|
+
this.sessions.set(key, {
|
|
115
|
+
session,
|
|
116
|
+
lastUsed: Date.now(),
|
|
117
|
+
refCount: 1
|
|
118
|
+
});
|
|
119
|
+
session.on("close", () => {
|
|
120
|
+
this.sessions.delete(key);
|
|
121
|
+
});
|
|
122
|
+
session.on("error", () => {
|
|
123
|
+
this.sessions.delete(key);
|
|
124
|
+
});
|
|
125
|
+
return session;
|
|
126
|
+
}
|
|
127
|
+
createSession(url, options, timeout) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const authority = `${url.protocol}//${url.host}`;
|
|
130
|
+
const sessionOptions = {
|
|
131
|
+
...options,
|
|
132
|
+
rejectUnauthorized: options?.rejectUnauthorized !== false,
|
|
133
|
+
ALPNProtocols: ["h2", "http/1.1"],
|
|
134
|
+
timeout
|
|
135
|
+
};
|
|
136
|
+
const session = http2.connect(authority, sessionOptions);
|
|
137
|
+
let settled = false;
|
|
138
|
+
const timeoutId = timeout ? setTimeout(() => {
|
|
139
|
+
if (!settled) {
|
|
140
|
+
settled = true;
|
|
141
|
+
session.destroy();
|
|
142
|
+
reject(new Error(`HTTP/2 connection timeout after ${timeout}ms`));
|
|
143
|
+
}
|
|
144
|
+
}, timeout) : null;
|
|
145
|
+
session.on("connect", () => {
|
|
146
|
+
if (!settled) {
|
|
147
|
+
settled = true;
|
|
148
|
+
if (timeoutId)
|
|
149
|
+
clearTimeout(timeoutId);
|
|
150
|
+
resolve(session);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
session.on("error", (err) => {
|
|
154
|
+
if (!settled) {
|
|
155
|
+
settled = true;
|
|
156
|
+
if (timeoutId)
|
|
157
|
+
clearTimeout(timeoutId);
|
|
158
|
+
reject(err);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
releaseSession(url) {
|
|
164
|
+
const key = this.getSessionKey(url);
|
|
165
|
+
const entry = this.sessions.get(key);
|
|
166
|
+
if (entry) {
|
|
167
|
+
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
168
|
+
entry.lastUsed = Date.now();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
closeSession(url) {
|
|
172
|
+
const key = this.getSessionKey(url);
|
|
173
|
+
const entry = this.sessions.get(key);
|
|
174
|
+
if (entry) {
|
|
175
|
+
entry.session.close();
|
|
176
|
+
this.sessions.delete(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
closeAllSessions() {
|
|
180
|
+
for (const [key, entry] of this.sessions.entries()) {
|
|
181
|
+
entry.session.close();
|
|
182
|
+
}
|
|
183
|
+
this.sessions.clear();
|
|
184
|
+
}
|
|
185
|
+
destroy() {
|
|
186
|
+
if (this.cleanupInterval) {
|
|
187
|
+
clearInterval(this.cleanupInterval);
|
|
188
|
+
this.cleanupInterval = null;
|
|
189
|
+
}
|
|
190
|
+
this.closeAllSessions();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const responseCacheInstances = new Map;
|
|
194
|
+
function getCacheConfigKey(option) {
|
|
195
|
+
if (option === true)
|
|
196
|
+
return "default";
|
|
197
|
+
if (option === false)
|
|
198
|
+
return "disabled";
|
|
199
|
+
const cfg = option;
|
|
200
|
+
return JSON.stringify({
|
|
201
|
+
cacheDir: cfg.cacheDir || null,
|
|
202
|
+
ttl: cfg.ttl || 300000,
|
|
203
|
+
maxEntries: cfg.maxEntries || 500,
|
|
204
|
+
methods: cfg.methods || ["GET", "HEAD"],
|
|
205
|
+
respectHeaders: cfg.respectHeaders !== false
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function getResponseCache(option) {
|
|
209
|
+
const key = getCacheConfigKey(option);
|
|
210
|
+
let cache = responseCacheInstances.get(key);
|
|
211
|
+
if (!cache) {
|
|
212
|
+
cache = new ResponseCache(option);
|
|
213
|
+
responseCacheInstances.set(key, cache);
|
|
214
|
+
}
|
|
215
|
+
return cache;
|
|
216
|
+
}
|
|
217
|
+
function parseCacheControlFromHeaders(headers) {
|
|
218
|
+
const cacheControl = headers["cache-control"] || "";
|
|
219
|
+
return {
|
|
220
|
+
noCache: cacheControl.includes("no-cache"),
|
|
221
|
+
mustRevalidate: cacheControl.includes("must-revalidate")
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function buildCachedRezoResponse(cached, config) {
|
|
225
|
+
const headers = new RezoHeaders(cached.headers);
|
|
226
|
+
return {
|
|
227
|
+
data: cached.data,
|
|
228
|
+
status: cached.status,
|
|
229
|
+
statusText: cached.statusText,
|
|
230
|
+
headers,
|
|
231
|
+
finalUrl: cached.url,
|
|
232
|
+
urls: [cached.url],
|
|
233
|
+
contentType: cached.headers["content-type"],
|
|
234
|
+
contentLength: parseInt(cached.headers["content-length"] || "0", 10) || 0,
|
|
235
|
+
cookies: {
|
|
236
|
+
array: [],
|
|
237
|
+
serialized: [],
|
|
238
|
+
netscape: "",
|
|
239
|
+
string: "",
|
|
240
|
+
setCookiesString: []
|
|
241
|
+
},
|
|
242
|
+
config: {
|
|
243
|
+
...config,
|
|
244
|
+
url: cached.url,
|
|
245
|
+
method: "GET",
|
|
246
|
+
headers,
|
|
247
|
+
adapterUsed: "http2",
|
|
248
|
+
fromCache: true
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function buildUrlTree(config, finalUrl) {
|
|
253
|
+
const urls = [];
|
|
254
|
+
if (config.rawUrl) {
|
|
255
|
+
urls.push(config.rawUrl);
|
|
256
|
+
} else if (config.url) {
|
|
257
|
+
const urlStr = typeof config.url === "string" ? config.url : config.url.toString();
|
|
258
|
+
urls.push(urlStr);
|
|
259
|
+
}
|
|
260
|
+
if (finalUrl && (urls.length === 0 || urls[0] !== finalUrl)) {
|
|
261
|
+
urls.push(finalUrl);
|
|
262
|
+
}
|
|
263
|
+
return urls.length > 0 ? urls : [finalUrl];
|
|
264
|
+
}
|
|
265
|
+
function sanitizeConfig(config) {
|
|
266
|
+
const { data: _data, ...sanitized } = config;
|
|
267
|
+
return sanitized;
|
|
268
|
+
}
|
|
269
|
+
function updateCookies(config, headers, url) {
|
|
270
|
+
const setCookieHeaders = headers["set-cookie"];
|
|
271
|
+
if (!setCookieHeaders)
|
|
272
|
+
return;
|
|
273
|
+
const cookieHeaderArray = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
|
|
274
|
+
if (!config.responseCookies) {
|
|
275
|
+
config.responseCookies = {
|
|
276
|
+
array: [],
|
|
277
|
+
serialized: [],
|
|
278
|
+
netscape: "",
|
|
279
|
+
string: "",
|
|
280
|
+
setCookiesString: []
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
for (const cookieStr of cookieHeaderArray) {
|
|
284
|
+
config.responseCookies.setCookiesString.push(cookieStr);
|
|
285
|
+
const parts = cookieStr.split(";");
|
|
286
|
+
const [nameValue] = parts;
|
|
287
|
+
const [name, ...valueParts] = nameValue.split("=");
|
|
288
|
+
const value = valueParts.join("=");
|
|
289
|
+
if (name && value !== undefined) {
|
|
290
|
+
const cookie = new Cookie({
|
|
291
|
+
key: name.trim(),
|
|
292
|
+
value: value.trim(),
|
|
293
|
+
domain: new URL(url).hostname,
|
|
294
|
+
path: "/",
|
|
295
|
+
httpOnly: cookieStr.toLowerCase().includes("httponly"),
|
|
296
|
+
secure: cookieStr.toLowerCase().includes("secure"),
|
|
297
|
+
sameSite: "lax"
|
|
298
|
+
});
|
|
299
|
+
config.responseCookies.array.push(cookie);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
config.responseCookies.string = config.responseCookies.array.map((c) => `${c.key}=${c.value}`).join("; ");
|
|
303
|
+
config.responseCookies.serialized = config.responseCookies.array.map((c) => c.toJSON());
|
|
304
|
+
config.responseCookies.netscape = config.responseCookies.array.map((c) => c.toNetscapeFormat()).join(`
|
|
305
|
+
`);
|
|
306
|
+
}
|
|
307
|
+
export async function executeRequest(options, defaultOptions, jar) {
|
|
308
|
+
if (!options.responseType) {
|
|
309
|
+
options.responseType = "auto";
|
|
310
|
+
}
|
|
311
|
+
const d_options = await getDefaultConfig(defaultOptions);
|
|
312
|
+
const configResult = prepareHTTPOptions(options, jar, { defaultOptions: d_options });
|
|
313
|
+
let mainConfig = configResult.config;
|
|
314
|
+
const fetchOptions = configResult.fetchOptions;
|
|
315
|
+
const perform = new RezoPerformance;
|
|
316
|
+
const cacheOption = options.cache;
|
|
317
|
+
const method = (options.method || "GET").toUpperCase();
|
|
318
|
+
const requestUrl = typeof fetchOptions.url === "string" ? fetchOptions.url : fetchOptions.url?.toString() || "";
|
|
319
|
+
let cache;
|
|
320
|
+
let requestHeaders;
|
|
321
|
+
let cachedEntry;
|
|
322
|
+
let needsRevalidation = false;
|
|
323
|
+
if (cacheOption) {
|
|
324
|
+
cache = getResponseCache(cacheOption);
|
|
325
|
+
requestHeaders = fetchOptions.headers instanceof RezoHeaders ? Object.fromEntries(fetchOptions.headers.entries()) : fetchOptions.headers;
|
|
326
|
+
cachedEntry = cache.get(method, requestUrl, requestHeaders);
|
|
327
|
+
if (cachedEntry) {
|
|
328
|
+
const cacheControl = parseCacheControlFromHeaders(cachedEntry.headers);
|
|
329
|
+
if (cacheControl.noCache || cacheControl.mustRevalidate) {
|
|
330
|
+
needsRevalidation = true;
|
|
331
|
+
} else {
|
|
332
|
+
return buildCachedRezoResponse(cachedEntry, mainConfig);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const conditionalHeaders = cache.getConditionalHeaders(method, requestUrl, requestHeaders);
|
|
336
|
+
if (conditionalHeaders) {
|
|
337
|
+
const headers = fetchOptions.headers instanceof RezoHeaders ? fetchOptions.headers : new RezoHeaders(fetchOptions.headers || {});
|
|
338
|
+
if (conditionalHeaders.etag) {
|
|
339
|
+
headers.set("If-None-Match", conditionalHeaders.etag);
|
|
340
|
+
}
|
|
341
|
+
if (conditionalHeaders.lastModified) {
|
|
342
|
+
headers.set("If-Modified-Since", conditionalHeaders.lastModified);
|
|
343
|
+
}
|
|
344
|
+
fetchOptions.headers = headers;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const isStream = options._isStream;
|
|
348
|
+
const isDownload = options._isDownload || !!options.fileName || !!options.saveTo;
|
|
349
|
+
const isUpload = options._isUpload;
|
|
350
|
+
let fs;
|
|
351
|
+
if (isDownload) {
|
|
352
|
+
try {
|
|
353
|
+
fs = await import("node:fs");
|
|
354
|
+
} catch {
|
|
355
|
+
fs = undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
let streamResponse;
|
|
359
|
+
let downloadResponse;
|
|
360
|
+
let uploadResponse;
|
|
361
|
+
if (isStream) {
|
|
362
|
+
streamResponse = new StreamResponse;
|
|
363
|
+
} else if (isDownload) {
|
|
364
|
+
const fileName = options.fileName || options.saveTo || "";
|
|
365
|
+
const url = typeof fetchOptions.url === "string" ? fetchOptions.url : fetchOptions.url?.toString() || "";
|
|
366
|
+
downloadResponse = new DownloadResponse(fileName, url);
|
|
367
|
+
} else if (isUpload) {
|
|
368
|
+
const url = typeof fetchOptions.url === "string" ? fetchOptions.url : fetchOptions.url?.toString() || "";
|
|
369
|
+
uploadResponse = new UploadResponse(url);
|
|
370
|
+
}
|
|
371
|
+
const res = executeHttp2Request(fetchOptions, mainConfig, options, perform, fs, streamResponse, downloadResponse, uploadResponse);
|
|
372
|
+
if (streamResponse) {
|
|
373
|
+
return streamResponse;
|
|
374
|
+
} else if (downloadResponse) {
|
|
375
|
+
return downloadResponse;
|
|
376
|
+
} else if (uploadResponse) {
|
|
377
|
+
return uploadResponse;
|
|
378
|
+
}
|
|
379
|
+
const response = await res;
|
|
380
|
+
if (cache && !isStream && !isDownload && !isUpload) {
|
|
381
|
+
if (response.status === 304 && cachedEntry) {
|
|
382
|
+
const responseHeaders = response.headers instanceof RezoHeaders ? Object.fromEntries(response.headers.entries()) : response.headers;
|
|
383
|
+
const updatedCached = cache.updateRevalidated(method, requestUrl, responseHeaders, requestHeaders);
|
|
384
|
+
if (updatedCached) {
|
|
385
|
+
return buildCachedRezoResponse(updatedCached, mainConfig);
|
|
386
|
+
}
|
|
387
|
+
return buildCachedRezoResponse(cachedEntry, mainConfig);
|
|
388
|
+
}
|
|
389
|
+
if (response.status >= 200 && response.status < 300) {
|
|
390
|
+
cache.set(method, requestUrl, response, requestHeaders);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return response;
|
|
394
|
+
}
|
|
395
|
+
async function executeHttp2Request(fetchOptions, config, options, perform, fs, streamResult, downloadResult, uploadResult) {
|
|
396
|
+
let requestCount = 0;
|
|
397
|
+
const _stats = { statusOnNext: "abort" };
|
|
398
|
+
let responseStatusCode;
|
|
399
|
+
let retries = 0;
|
|
400
|
+
const retryDelay = config?.retry?.retryDelay || 0;
|
|
401
|
+
const maxRetries = config?.retry?.maxRetries || 0;
|
|
402
|
+
const incrementDelay = config?.retry?.incrementDelay || false;
|
|
403
|
+
const statusCodes = config?.retry?.statusCodes;
|
|
404
|
+
const timing = {
|
|
405
|
+
startTime: performance.now(),
|
|
406
|
+
startTimestamp: Date.now()
|
|
407
|
+
};
|
|
408
|
+
const ABSOLUTE_MAX_ATTEMPTS = 50;
|
|
409
|
+
const visitedUrls = new Set;
|
|
410
|
+
let totalAttempts = 0;
|
|
411
|
+
config.setSignal();
|
|
412
|
+
const timeoutClearInstance = config.timeoutClearInstanse;
|
|
413
|
+
delete config.timeoutClearInstanse;
|
|
414
|
+
const eventEmitter = streamResult || downloadResult || uploadResult;
|
|
415
|
+
if (eventEmitter) {
|
|
416
|
+
eventEmitter.emit("initiated");
|
|
417
|
+
}
|
|
418
|
+
const sessionPool = Http2SessionPool.getInstance();
|
|
419
|
+
while (true) {
|
|
420
|
+
totalAttempts++;
|
|
421
|
+
if (totalAttempts > ABSOLUTE_MAX_ATTEMPTS) {
|
|
422
|
+
const error = builErrorFromResponse(`Absolute maximum attempts (${ABSOLUTE_MAX_ATTEMPTS}) exceeded.`, { status: 0, statusText: "Max Attempts Exceeded" }, config, fetchOptions);
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
const response = await executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool);
|
|
427
|
+
const statusOnNext = _stats.statusOnNext;
|
|
428
|
+
if (response instanceof RezoError) {
|
|
429
|
+
const fileName = config.fileName;
|
|
430
|
+
if (fileName && fs && fs.existsSync(fileName)) {
|
|
431
|
+
fs.unlinkSync(fileName);
|
|
432
|
+
}
|
|
433
|
+
config.errors.push({
|
|
434
|
+
attempt: config.retryAttempts + 1,
|
|
435
|
+
error: response,
|
|
436
|
+
duration: perform.now()
|
|
437
|
+
});
|
|
438
|
+
perform.reset();
|
|
439
|
+
if (!responseStatusCode || !config.retry) {
|
|
440
|
+
throw response;
|
|
441
|
+
}
|
|
442
|
+
if (config.retry) {
|
|
443
|
+
if (config.retry.condition) {
|
|
444
|
+
const isPassed = await config.retry.condition(response);
|
|
445
|
+
if (typeof isPassed === "boolean" && isPassed === false) {
|
|
446
|
+
throw response;
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
if (!statusCodes.includes(responseStatusCode)) {
|
|
450
|
+
throw response;
|
|
451
|
+
}
|
|
452
|
+
if (maxRetries <= retries) {
|
|
453
|
+
if (config.debug) {
|
|
454
|
+
console.log(`Max retries (${maxRetries}) reached`);
|
|
455
|
+
}
|
|
456
|
+
throw response;
|
|
457
|
+
}
|
|
458
|
+
retries++;
|
|
459
|
+
if (config.debug) {
|
|
460
|
+
console.log(`Retrying... ${retryDelay > 0 ? "in " + (incrementDelay ? retryDelay * retries : retryDelay) + "ms" : ""}`);
|
|
461
|
+
}
|
|
462
|
+
if (retryDelay > 0) {
|
|
463
|
+
await new Promise((resolve) => setTimeout(resolve, incrementDelay ? retryDelay * retries : retryDelay));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
config.retryAttempts++;
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (statusOnNext === "success") {
|
|
471
|
+
return response;
|
|
472
|
+
}
|
|
473
|
+
if (statusOnNext === "redirect") {
|
|
474
|
+
const location = _stats.redirectUrl;
|
|
475
|
+
if (!location) {
|
|
476
|
+
throw builErrorFromResponse("Redirect location not found", response, config, fetchOptions);
|
|
477
|
+
}
|
|
478
|
+
if (config.maxRedirects === 0) {
|
|
479
|
+
config.maxRedirectsReached = true;
|
|
480
|
+
throw builErrorFromResponse("Redirects are disabled (maxRedirects=0)", response, config, fetchOptions);
|
|
481
|
+
}
|
|
482
|
+
const enableCycleDetection = config.enableRedirectCycleDetection === true;
|
|
483
|
+
if (enableCycleDetection) {
|
|
484
|
+
const normalizedRedirectUrl = location.toLowerCase();
|
|
485
|
+
if (visitedUrls.has(normalizedRedirectUrl)) {
|
|
486
|
+
throw builErrorFromResponse(`Redirect cycle detected: ${location}`, response, config, fetchOptions);
|
|
487
|
+
}
|
|
488
|
+
visitedUrls.add(normalizedRedirectUrl);
|
|
489
|
+
}
|
|
490
|
+
const redirectCode = response.status;
|
|
491
|
+
const onRedirect = config.beforeRedirect ? config.beforeRedirect({
|
|
492
|
+
url: new URL(location),
|
|
493
|
+
status: response.status,
|
|
494
|
+
headers: response.headers,
|
|
495
|
+
sameDomain: isSameDomain(fetchOptions.fullUrl, location),
|
|
496
|
+
method: fetchOptions.method.toUpperCase()
|
|
497
|
+
}) : undefined;
|
|
498
|
+
if (typeof onRedirect !== "undefined") {
|
|
499
|
+
if (typeof onRedirect === "boolean" && !onRedirect) {
|
|
500
|
+
throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
|
|
501
|
+
} else if (typeof onRedirect === "object" && !onRedirect.redirect) {
|
|
502
|
+
throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (config.redirectCount >= config.maxRedirects && config.maxRedirects > 0) {
|
|
506
|
+
config.maxRedirectsReached = true;
|
|
507
|
+
throw builErrorFromResponse(`Max redirects (${config.maxRedirects}) reached`, response, config, fetchOptions);
|
|
508
|
+
}
|
|
509
|
+
config.redirectHistory.push({
|
|
510
|
+
url: fetchOptions.fullUrl,
|
|
511
|
+
statusCode: redirectCode,
|
|
512
|
+
statusText: response.statusText,
|
|
513
|
+
headers: response.headers,
|
|
514
|
+
method: fetchOptions.method.toUpperCase(),
|
|
515
|
+
cookies: response.cookies.array,
|
|
516
|
+
duration: perform.now(),
|
|
517
|
+
request: fetchOptions
|
|
518
|
+
});
|
|
519
|
+
perform.reset();
|
|
520
|
+
config.redirectCount++;
|
|
521
|
+
if (response.status === 301 || response.status === 302 || response.status === 303) {
|
|
522
|
+
if (config.treat302As303 !== false || response.status === 303) {
|
|
523
|
+
options.method = "GET";
|
|
524
|
+
delete options.body;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
fetchOptions.fullUrl = location;
|
|
528
|
+
delete options.params;
|
|
529
|
+
requestCount++;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
throw builErrorFromResponse("Unexpected state", response, config, fetchOptions);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (error instanceof RezoError) {
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
throw buildSmartError(config, fetchOptions, error);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool) {
|
|
542
|
+
return new Promise(async (resolve) => {
|
|
543
|
+
try {
|
|
544
|
+
const { fullUrl, body } = fetchOptions;
|
|
545
|
+
const url = new URL(fullUrl || fetchOptions.url);
|
|
546
|
+
const isSecure = url.protocol === "https:";
|
|
547
|
+
if (requestCount === 0) {
|
|
548
|
+
config.adapterUsed = "http2";
|
|
549
|
+
config.isSecure = isSecure;
|
|
550
|
+
config.finalUrl = url.href;
|
|
551
|
+
config.network.protocol = "h2";
|
|
552
|
+
config.timing.startTimestamp = timing.startTimestamp;
|
|
553
|
+
}
|
|
554
|
+
const headers = {
|
|
555
|
+
[http2.constants.HTTP2_HEADER_METHOD]: fetchOptions.method.toUpperCase(),
|
|
556
|
+
[http2.constants.HTTP2_HEADER_PATH]: url.pathname + url.search,
|
|
557
|
+
[http2.constants.HTTP2_HEADER_SCHEME]: url.protocol.replace(":", ""),
|
|
558
|
+
[http2.constants.HTTP2_HEADER_AUTHORITY]: url.host
|
|
559
|
+
};
|
|
560
|
+
const reqHeaders = fetchOptions.headers instanceof RezoHeaders ? fetchOptions.headers.toObject() : fetchOptions.headers || {};
|
|
561
|
+
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
562
|
+
if (value !== undefined && value !== null) {
|
|
563
|
+
headers[key.toLowerCase()] = String(value);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (!headers["accept-encoding"]) {
|
|
567
|
+
headers["accept-encoding"] = "gzip, deflate, br";
|
|
568
|
+
}
|
|
569
|
+
const eventEmitter = streamResult || downloadResult || uploadResult;
|
|
570
|
+
if (eventEmitter && requestCount === 0) {
|
|
571
|
+
const startEvent = {
|
|
572
|
+
url: url.toString(),
|
|
573
|
+
method: fetchOptions.method.toUpperCase(),
|
|
574
|
+
headers: new RezoHeaders(reqHeaders),
|
|
575
|
+
timestamp: timing.startTime,
|
|
576
|
+
timeout: fetchOptions.timeout,
|
|
577
|
+
maxRedirects: config.maxRedirects
|
|
578
|
+
};
|
|
579
|
+
eventEmitter.emit("start", startEvent);
|
|
580
|
+
}
|
|
581
|
+
const sessionOptions = {
|
|
582
|
+
rejectUnauthorized: config.rejectUnauthorized !== false
|
|
583
|
+
};
|
|
584
|
+
const securityContext = config.secureContext || config.security;
|
|
585
|
+
if (securityContext?.ca)
|
|
586
|
+
sessionOptions.ca = securityContext.ca;
|
|
587
|
+
if (securityContext?.cert)
|
|
588
|
+
sessionOptions.cert = securityContext.cert;
|
|
589
|
+
if (securityContext?.key)
|
|
590
|
+
sessionOptions.key = securityContext.key;
|
|
591
|
+
if (securityContext?.pfx)
|
|
592
|
+
sessionOptions.pfx = securityContext.pfx;
|
|
593
|
+
if (securityContext?.passphrase)
|
|
594
|
+
sessionOptions.passphrase = securityContext.passphrase;
|
|
595
|
+
let session;
|
|
596
|
+
try {
|
|
597
|
+
session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
const error = buildSmartError(config, fetchOptions, err);
|
|
600
|
+
_stats.statusOnNext = "error";
|
|
601
|
+
resolve(error);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const req = session.request(headers);
|
|
605
|
+
if (config.timeout) {
|
|
606
|
+
req.setTimeout(config.timeout, () => {
|
|
607
|
+
req.close(http2.constants.NGHTTP2_CANCEL);
|
|
608
|
+
const error = buildSmartError(config, fetchOptions, new Error(`Request timeout after ${config.timeout}ms`));
|
|
609
|
+
_stats.statusOnNext = "error";
|
|
610
|
+
resolve(error);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
let chunks = [];
|
|
614
|
+
let contentLengthCounter = 0;
|
|
615
|
+
let responseHeaders = {};
|
|
616
|
+
let status = 0;
|
|
617
|
+
let statusText = "";
|
|
618
|
+
req.on("response", (headers) => {
|
|
619
|
+
responseHeaders = headers;
|
|
620
|
+
status = Number(headers[http2.constants.HTTP2_HEADER_STATUS]) || 200;
|
|
621
|
+
statusText = getStatusText(status);
|
|
622
|
+
if (!config.timing.ttfbMs) {
|
|
623
|
+
timing.firstByteTime = performance.now();
|
|
624
|
+
config.timing.ttfbMs = timing.firstByteTime - timing.startTime;
|
|
625
|
+
}
|
|
626
|
+
const location = headers["location"];
|
|
627
|
+
const isRedirect = status >= 300 && status < 400 && location;
|
|
628
|
+
if (isRedirect) {
|
|
629
|
+
_stats.statusOnNext = "redirect";
|
|
630
|
+
_stats.redirectUrl = new URL(location, url).href;
|
|
631
|
+
}
|
|
632
|
+
config.network.httpVersion = "h2";
|
|
633
|
+
updateCookies(config, headers, url.href);
|
|
634
|
+
if (eventEmitter && !isRedirect) {
|
|
635
|
+
const headersEvent = {
|
|
636
|
+
status,
|
|
637
|
+
statusText,
|
|
638
|
+
headers: new RezoHeaders(headers),
|
|
639
|
+
contentType: headers["content-type"],
|
|
640
|
+
contentLength: headers["content-length"] ? parseInt(headers["content-length"], 10) : undefined,
|
|
641
|
+
cookies: config.responseCookies?.array || [],
|
|
642
|
+
timing: {
|
|
643
|
+
firstByte: config.timing.ttfbMs,
|
|
644
|
+
total: performance.now() - timing.startTime
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
eventEmitter.emit("headers", headersEvent);
|
|
648
|
+
eventEmitter.emit("status", status, statusText);
|
|
649
|
+
eventEmitter.emit("cookies", config.responseCookies?.array || []);
|
|
650
|
+
if (downloadResult) {
|
|
651
|
+
downloadResult.status = status;
|
|
652
|
+
downloadResult.statusText = statusText;
|
|
653
|
+
} else if (uploadResult) {
|
|
654
|
+
uploadResult.status = status;
|
|
655
|
+
uploadResult.statusText = statusText;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
req.on("data", (chunk) => {
|
|
660
|
+
chunks.push(chunk);
|
|
661
|
+
contentLengthCounter += chunk.length;
|
|
662
|
+
if (streamResult) {
|
|
663
|
+
streamResult.emit("data", chunk);
|
|
664
|
+
}
|
|
665
|
+
const contentLength = responseHeaders["content-length"] ? parseInt(responseHeaders["content-length"], 10) : undefined;
|
|
666
|
+
if (eventEmitter) {
|
|
667
|
+
const progressEvent = {
|
|
668
|
+
loaded: contentLengthCounter,
|
|
669
|
+
total: contentLength || 0,
|
|
670
|
+
percentage: contentLength ? Math.round(contentLengthCounter / contentLength * 100) : 0,
|
|
671
|
+
speed: 0,
|
|
672
|
+
averageSpeed: 0,
|
|
673
|
+
estimatedTime: 0,
|
|
674
|
+
timestamp: Date.now()
|
|
675
|
+
};
|
|
676
|
+
eventEmitter.emit("progress", progressEvent);
|
|
677
|
+
eventEmitter.emit("download-progress", progressEvent);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
req.on("end", async () => {
|
|
681
|
+
config.timing.endTimestamp = Date.now();
|
|
682
|
+
config.timing.durationMs = performance.now() - timing.startTime;
|
|
683
|
+
config.timing.transferMs = timing.firstByteTime ? performance.now() - timing.firstByteTime : config.timing.durationMs;
|
|
684
|
+
config.transfer.bodySize = contentLengthCounter;
|
|
685
|
+
config.transfer.responseSize = contentLengthCounter;
|
|
686
|
+
(sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
|
|
687
|
+
if (_stats.statusOnNext === "redirect") {
|
|
688
|
+
const partialResponse = {
|
|
689
|
+
data: "",
|
|
690
|
+
status,
|
|
691
|
+
statusText,
|
|
692
|
+
headers: new RezoHeaders(responseHeaders),
|
|
693
|
+
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
694
|
+
config,
|
|
695
|
+
contentType: responseHeaders["content-type"],
|
|
696
|
+
contentLength: contentLengthCounter,
|
|
697
|
+
finalUrl: url.href,
|
|
698
|
+
urls: buildUrlTree(config, url.href)
|
|
699
|
+
};
|
|
700
|
+
resolve(partialResponse);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
let responseBody = Buffer.concat(chunks);
|
|
704
|
+
const contentEncoding = responseHeaders["content-encoding"];
|
|
705
|
+
if (contentEncoding && contentLengthCounter > 0) {
|
|
706
|
+
try {
|
|
707
|
+
const decompressed = await decompressBuffer(responseBody, contentEncoding);
|
|
708
|
+
responseBody = decompressed;
|
|
709
|
+
} catch (err) {
|
|
710
|
+
const error = buildDecompressionError({
|
|
711
|
+
statusCode: status,
|
|
712
|
+
headers: responseHeaders,
|
|
713
|
+
contentType: responseHeaders["content-type"],
|
|
714
|
+
contentLength: String(contentLengthCounter),
|
|
715
|
+
cookies: config.responseCookies?.setCookiesString || [],
|
|
716
|
+
statusText: err.message,
|
|
717
|
+
url: url.href,
|
|
718
|
+
body: responseBody,
|
|
719
|
+
finalUrl: url.href,
|
|
720
|
+
config,
|
|
721
|
+
request: fetchOptions
|
|
722
|
+
});
|
|
723
|
+
_stats.statusOnNext = "error";
|
|
724
|
+
resolve(error);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
let data;
|
|
729
|
+
const contentType = responseHeaders["content-type"] || "";
|
|
730
|
+
const responseType = config.responseType || fetchOptions.responseType || "auto";
|
|
731
|
+
if (responseType === "buffer" || responseType === "arrayBuffer") {
|
|
732
|
+
data = responseBody;
|
|
733
|
+
} else if (responseType === "text") {
|
|
734
|
+
data = responseBody.toString("utf-8");
|
|
735
|
+
} else if (responseType === "json" || responseType === "auto" && contentType.includes("application/json")) {
|
|
736
|
+
try {
|
|
737
|
+
data = JSON.parse(responseBody.toString("utf-8"));
|
|
738
|
+
} catch {
|
|
739
|
+
data = responseBody.toString("utf-8");
|
|
740
|
+
}
|
|
741
|
+
} else {
|
|
742
|
+
if (contentType.includes("application/json")) {
|
|
743
|
+
try {
|
|
744
|
+
data = JSON.parse(responseBody.toString("utf-8"));
|
|
745
|
+
} catch {
|
|
746
|
+
data = responseBody.toString("utf-8");
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
data = responseBody.toString("utf-8");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (status >= 400) {
|
|
753
|
+
const error = builErrorFromResponse(`HTTP Error ${status}: ${statusText}`, {
|
|
754
|
+
status,
|
|
755
|
+
statusText,
|
|
756
|
+
headers: new RezoHeaders(responseHeaders),
|
|
757
|
+
data
|
|
758
|
+
}, config, fetchOptions);
|
|
759
|
+
_stats.statusOnNext = "error";
|
|
760
|
+
resolve(error);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
_stats.statusOnNext = "success";
|
|
764
|
+
const finalResponse = {
|
|
765
|
+
data,
|
|
766
|
+
status,
|
|
767
|
+
statusText,
|
|
768
|
+
headers: new RezoHeaders(responseHeaders),
|
|
769
|
+
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
770
|
+
config,
|
|
771
|
+
contentType,
|
|
772
|
+
contentLength: contentLengthCounter,
|
|
773
|
+
finalUrl: url.href,
|
|
774
|
+
urls: buildUrlTree(config, url.href)
|
|
775
|
+
};
|
|
776
|
+
if (downloadResult && fs && config.fileName) {
|
|
777
|
+
try {
|
|
778
|
+
fs.writeFileSync(config.fileName, responseBody);
|
|
779
|
+
const downloadFinishEvent = {
|
|
780
|
+
status,
|
|
781
|
+
statusText,
|
|
782
|
+
headers: new RezoHeaders(responseHeaders),
|
|
783
|
+
contentType,
|
|
784
|
+
contentLength: responseBody.length,
|
|
785
|
+
finalUrl: url.href,
|
|
786
|
+
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
787
|
+
urls: buildUrlTree(config, url.href),
|
|
788
|
+
fileName: config.fileName,
|
|
789
|
+
fileSize: responseBody.length,
|
|
790
|
+
timing: {
|
|
791
|
+
total: config.timing.durationMs || 0,
|
|
792
|
+
dns: config.timing.dnsMs,
|
|
793
|
+
tcp: config.timing.tcpMs,
|
|
794
|
+
tls: config.timing.tlsMs,
|
|
795
|
+
firstByte: config.timing.ttfbMs,
|
|
796
|
+
download: config.timing.transferMs || 0
|
|
797
|
+
},
|
|
798
|
+
averageSpeed: config.timing.transferMs ? responseBody.length / config.timing.transferMs * 1000 : 0,
|
|
799
|
+
config: sanitizeConfig(config)
|
|
800
|
+
};
|
|
801
|
+
downloadResult.emit("finish", downloadFinishEvent);
|
|
802
|
+
downloadResult.emit("done", downloadFinishEvent);
|
|
803
|
+
downloadResult._markFinished();
|
|
804
|
+
} catch (err) {
|
|
805
|
+
const error = buildDownloadError({
|
|
806
|
+
statusCode: status,
|
|
807
|
+
headers: responseHeaders,
|
|
808
|
+
contentType,
|
|
809
|
+
contentLength: String(contentLengthCounter),
|
|
810
|
+
cookies: config.responseCookies?.setCookiesString || [],
|
|
811
|
+
statusText: err.message,
|
|
812
|
+
url: url.href,
|
|
813
|
+
body: responseBody,
|
|
814
|
+
finalUrl: url.href,
|
|
815
|
+
config,
|
|
816
|
+
request: fetchOptions
|
|
817
|
+
});
|
|
818
|
+
downloadResult.emit("error", error);
|
|
819
|
+
resolve(error);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (streamResult) {
|
|
824
|
+
const streamFinishEvent = {
|
|
825
|
+
status,
|
|
826
|
+
statusText,
|
|
827
|
+
headers: new RezoHeaders(responseHeaders),
|
|
828
|
+
contentType,
|
|
829
|
+
contentLength: contentLengthCounter,
|
|
830
|
+
finalUrl: url.href,
|
|
831
|
+
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
832
|
+
urls: buildUrlTree(config, url.href),
|
|
833
|
+
timing: {
|
|
834
|
+
total: config.timing.durationMs || 0,
|
|
835
|
+
dns: config.timing.dnsMs,
|
|
836
|
+
tcp: config.timing.tcpMs,
|
|
837
|
+
tls: config.timing.tlsMs,
|
|
838
|
+
firstByte: config.timing.ttfbMs,
|
|
839
|
+
download: config.timing.transferMs
|
|
840
|
+
},
|
|
841
|
+
config: sanitizeConfig(config)
|
|
842
|
+
};
|
|
843
|
+
streamResult.emit("finish", streamFinishEvent);
|
|
844
|
+
streamResult.emit("done", streamFinishEvent);
|
|
845
|
+
streamResult.emit("end");
|
|
846
|
+
streamResult._markFinished();
|
|
847
|
+
}
|
|
848
|
+
if (uploadResult) {
|
|
849
|
+
const uploadFinishEvent = {
|
|
850
|
+
response: {
|
|
851
|
+
status,
|
|
852
|
+
statusText,
|
|
853
|
+
headers: new RezoHeaders(responseHeaders),
|
|
854
|
+
data,
|
|
855
|
+
contentType,
|
|
856
|
+
contentLength: contentLengthCounter
|
|
857
|
+
},
|
|
858
|
+
finalUrl: url.href,
|
|
859
|
+
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
860
|
+
urls: buildUrlTree(config, url.href),
|
|
861
|
+
uploadSize: config.transfer.requestSize || 0,
|
|
862
|
+
timing: {
|
|
863
|
+
total: config.timing.durationMs || 0,
|
|
864
|
+
dns: config.timing.dnsMs,
|
|
865
|
+
tcp: config.timing.tcpMs,
|
|
866
|
+
tls: config.timing.tlsMs,
|
|
867
|
+
upload: config.timing.transferMs || 0,
|
|
868
|
+
waiting: config.timing.ttfbMs || 0,
|
|
869
|
+
download: config.timing.transferMs
|
|
870
|
+
},
|
|
871
|
+
averageUploadSpeed: config.timing.transferMs ? (config.transfer.requestSize || 0) / config.timing.transferMs * 1000 : 0,
|
|
872
|
+
averageDownloadSpeed: config.timing.transferMs ? contentLengthCounter / config.timing.transferMs * 1000 : 0,
|
|
873
|
+
config: sanitizeConfig(config)
|
|
874
|
+
};
|
|
875
|
+
uploadResult.emit("finish", uploadFinishEvent);
|
|
876
|
+
uploadResult.emit("done", uploadFinishEvent);
|
|
877
|
+
uploadResult._markFinished();
|
|
878
|
+
}
|
|
879
|
+
resolve(finalResponse);
|
|
880
|
+
});
|
|
881
|
+
req.on("error", (err) => {
|
|
882
|
+
_stats.statusOnNext = "error";
|
|
883
|
+
(sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
|
|
884
|
+
const error = buildSmartError(config, fetchOptions, err);
|
|
885
|
+
if (eventEmitter) {
|
|
886
|
+
eventEmitter.emit("error", error);
|
|
887
|
+
}
|
|
888
|
+
resolve(error);
|
|
889
|
+
});
|
|
890
|
+
if (body) {
|
|
891
|
+
if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
|
|
892
|
+
req.write(body.toString());
|
|
893
|
+
} else if (body instanceof FormData || body instanceof RezoFormData) {
|
|
894
|
+
if (body instanceof RezoFormData) {
|
|
895
|
+
body.pipe(req);
|
|
896
|
+
return;
|
|
897
|
+
} else {
|
|
898
|
+
const form = await RezoFormData.fromNativeFormData(body);
|
|
899
|
+
form.pipe(req);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
} else if (typeof body === "object" && !(body instanceof Buffer) && !(body instanceof Uint8Array) && !(body instanceof Readable)) {
|
|
903
|
+
req.write(JSON.stringify(body));
|
|
904
|
+
} else if (body instanceof Readable) {
|
|
905
|
+
body.pipe(req);
|
|
906
|
+
return;
|
|
907
|
+
} else {
|
|
908
|
+
req.write(body);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
req.end();
|
|
912
|
+
} catch (error) {
|
|
913
|
+
_stats.statusOnNext = "error";
|
|
914
|
+
const rezoError = buildSmartError(config, fetchOptions, error);
|
|
915
|
+
const eventEmitter = streamResult || downloadResult || uploadResult;
|
|
916
|
+
if (eventEmitter) {
|
|
917
|
+
eventEmitter.emit("error", rezoError);
|
|
918
|
+
}
|
|
919
|
+
resolve(rezoError);
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
function getStatusText(status) {
|
|
924
|
+
const statusTexts = {
|
|
925
|
+
200: "OK",
|
|
926
|
+
201: "Created",
|
|
927
|
+
202: "Accepted",
|
|
928
|
+
204: "No Content",
|
|
929
|
+
206: "Partial Content",
|
|
930
|
+
301: "Moved Permanently",
|
|
931
|
+
302: "Found",
|
|
932
|
+
303: "See Other",
|
|
933
|
+
304: "Not Modified",
|
|
934
|
+
307: "Temporary Redirect",
|
|
935
|
+
308: "Permanent Redirect",
|
|
936
|
+
400: "Bad Request",
|
|
937
|
+
401: "Unauthorized",
|
|
938
|
+
403: "Forbidden",
|
|
939
|
+
404: "Not Found",
|
|
940
|
+
405: "Method Not Allowed",
|
|
941
|
+
408: "Request Timeout",
|
|
942
|
+
409: "Conflict",
|
|
943
|
+
410: "Gone",
|
|
944
|
+
413: "Payload Too Large",
|
|
945
|
+
415: "Unsupported Media Type",
|
|
946
|
+
429: "Too Many Requests",
|
|
947
|
+
500: "Internal Server Error",
|
|
948
|
+
501: "Not Implemented",
|
|
949
|
+
502: "Bad Gateway",
|
|
950
|
+
503: "Service Unavailable",
|
|
951
|
+
504: "Gateway Timeout"
|
|
952
|
+
};
|
|
953
|
+
return statusTexts[status] || "Unknown";
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export { Http2SessionPool };
|