rezo 1.0.41 → 1.0.43
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/adapters/curl.cjs +143 -32
- package/dist/adapters/curl.js +143 -32
- package/dist/adapters/entries/curl.d.ts +65 -0
- package/dist/adapters/entries/fetch.d.ts +65 -0
- package/dist/adapters/entries/http.d.ts +65 -0
- package/dist/adapters/entries/http2.d.ts +65 -0
- package/dist/adapters/entries/react-native.d.ts +65 -0
- package/dist/adapters/entries/xhr.d.ts +65 -0
- package/dist/adapters/fetch.cjs +98 -12
- package/dist/adapters/fetch.js +98 -12
- package/dist/adapters/http.cjs +26 -14
- package/dist/adapters/http.js +26 -14
- package/dist/adapters/http2.cjs +756 -227
- package/dist/adapters/http2.js +756 -227
- package/dist/adapters/index.cjs +6 -6
- package/dist/adapters/xhr.cjs +94 -2
- package/dist/adapters/xhr.js +94 -2
- package/dist/cache/dns-cache.cjs +5 -3
- package/dist/cache/dns-cache.js +5 -3
- package/dist/cache/file-cacher.cjs +7 -1
- package/dist/cache/file-cacher.js +7 -1
- package/dist/cache/index.cjs +15 -13
- package/dist/cache/index.js +1 -0
- package/dist/cache/navigation-history.cjs +298 -0
- package/dist/cache/navigation-history.js +296 -0
- package/dist/cache/url-store.cjs +7 -1
- package/dist/cache/url-store.js +7 -1
- package/dist/core/rezo.cjs +7 -0
- package/dist/core/rezo.js +7 -0
- package/dist/crawler.d.ts +196 -11
- package/dist/entries/crawler.cjs +5 -5
- package/dist/index.cjs +27 -24
- package/dist/index.d.ts +73 -0
- package/dist/index.js +1 -0
- package/dist/internal/agents/base.cjs +113 -0
- package/dist/internal/agents/base.js +110 -0
- package/dist/internal/agents/http-proxy.cjs +89 -0
- package/dist/internal/agents/http-proxy.js +86 -0
- package/dist/internal/agents/https-proxy.cjs +176 -0
- package/dist/internal/agents/https-proxy.js +173 -0
- package/dist/internal/agents/index.cjs +10 -0
- package/dist/internal/agents/index.js +5 -0
- package/dist/internal/agents/socks-client.cjs +571 -0
- package/dist/internal/agents/socks-client.js +567 -0
- package/dist/internal/agents/socks-proxy.cjs +75 -0
- package/dist/internal/agents/socks-proxy.js +72 -0
- package/dist/platform/browser.d.ts +65 -0
- package/dist/platform/bun.d.ts +65 -0
- package/dist/platform/deno.d.ts +65 -0
- package/dist/platform/node.d.ts +65 -0
- package/dist/platform/react-native.d.ts +65 -0
- package/dist/platform/worker.d.ts +65 -0
- package/dist/plugin/crawler-options.cjs +1 -1
- package/dist/plugin/crawler-options.js +1 -1
- package/dist/plugin/crawler.cjs +192 -1
- package/dist/plugin/crawler.js +192 -1
- package/dist/plugin/index.cjs +36 -36
- package/dist/proxy/index.cjs +18 -16
- package/dist/proxy/index.js +17 -12
- package/dist/queue/index.cjs +8 -8
- package/dist/responses/buildError.cjs +11 -2
- package/dist/responses/buildError.js +11 -2
- package/dist/responses/universal/index.cjs +11 -11
- package/dist/utils/agent-pool.cjs +1 -17
- package/dist/utils/agent-pool.js +1 -17
- package/dist/utils/curl.cjs +317 -0
- package/dist/utils/curl.js +314 -0
- package/package.json +1 -1
package/dist/adapters/http2.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as http2 from "node:http2";
|
|
2
|
+
import * as tls from "node:tls";
|
|
2
3
|
import * as zlib from "node:zlib";
|
|
3
4
|
import { URL } from "node:url";
|
|
4
5
|
import { Readable } from "node:stream";
|
|
@@ -14,9 +15,105 @@ import { DownloadResponse } from '../responses/download.js';
|
|
|
14
15
|
import { UploadResponse } from '../responses/upload.js';
|
|
15
16
|
import { CompressionUtil } from '../utils/compression.js';
|
|
16
17
|
import { isSameDomain, RezoPerformance } from '../utils/tools.js';
|
|
18
|
+
import { SocksClient } from '../internal/agents/socks-client.js';
|
|
19
|
+
import * as net from "node:net";
|
|
17
20
|
import { ResponseCache } from '../cache/response-cache.js';
|
|
18
21
|
let zstdDecompressSync = null;
|
|
19
22
|
let zstdChecked = false;
|
|
23
|
+
const debugLog = {
|
|
24
|
+
requestStart: (config, url, method) => {
|
|
25
|
+
if (config.debug) {
|
|
26
|
+
console.log(`
|
|
27
|
+
[Rezo Debug] ─────────────────────────────────────`);
|
|
28
|
+
console.log(`[Rezo Debug] ${method} ${url}`);
|
|
29
|
+
console.log(`[Rezo Debug] Request ID: ${config.requestId}`);
|
|
30
|
+
if (config.originalRequest?.headers) {
|
|
31
|
+
const headers = config.originalRequest.headers instanceof RezoHeaders ? config.originalRequest.headers.toObject() : config.originalRequest.headers;
|
|
32
|
+
console.log(`[Rezo Debug] Request Headers:`, JSON.stringify(headers, null, 2));
|
|
33
|
+
}
|
|
34
|
+
if (config.proxy && typeof config.proxy === "object") {
|
|
35
|
+
console.log(`[Rezo Debug] Proxy: ${config.proxy.protocol}://${config.proxy.host}:${config.proxy.port}`);
|
|
36
|
+
} else if (config.proxy && typeof config.proxy === "string") {
|
|
37
|
+
console.log(`[Rezo Debug] Proxy: ${config.proxy}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (config.trackUrl) {
|
|
41
|
+
console.log(`[Rezo Track] → ${method} ${url}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
redirect: (config, fromUrl, toUrl, statusCode, method) => {
|
|
45
|
+
if (config.debug) {
|
|
46
|
+
console.log(`[Rezo Debug] Redirect ${statusCode}: ${fromUrl}`);
|
|
47
|
+
console.log(`[Rezo Debug] → ${toUrl} (${method})`);
|
|
48
|
+
}
|
|
49
|
+
if (config.trackUrl) {
|
|
50
|
+
console.log(`[Rezo Track] ↳ ${statusCode} → ${toUrl}`);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
retry: (config, attempt, maxRetries, statusCode, delay) => {
|
|
54
|
+
if (config.debug) {
|
|
55
|
+
console.log(`[Rezo Debug] Retry ${attempt}/${maxRetries} after status ${statusCode}${delay > 0 ? ` (waiting ${delay}ms)` : ""}`);
|
|
56
|
+
}
|
|
57
|
+
if (config.trackUrl) {
|
|
58
|
+
console.log(`[Rezo Track] ⟳ Retry ${attempt}/${maxRetries} (status ${statusCode})`);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
maxRetries: (config, maxRetries) => {
|
|
62
|
+
if (config.debug) {
|
|
63
|
+
console.log(`[Rezo Debug] Max retries (${maxRetries}) reached, throwing error`);
|
|
64
|
+
}
|
|
65
|
+
if (config.trackUrl) {
|
|
66
|
+
console.log(`[Rezo Track] ✗ Max retries reached`);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
response: (config, status, statusText, duration) => {
|
|
70
|
+
if (config.debug) {
|
|
71
|
+
console.log(`[Rezo Debug] Response: ${status} ${statusText} (${duration.toFixed(2)}ms)`);
|
|
72
|
+
}
|
|
73
|
+
if (config.trackUrl) {
|
|
74
|
+
console.log(`[Rezo Track] ✓ ${status} ${statusText}`);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
responseHeaders: (config, headers) => {
|
|
78
|
+
if (config.debug) {
|
|
79
|
+
console.log(`[Rezo Debug] Response Headers:`, JSON.stringify(headers, null, 2));
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
cookies: (config, cookieCount) => {
|
|
83
|
+
if (config.debug && cookieCount > 0) {
|
|
84
|
+
console.log(`[Rezo Debug] Cookies received: ${cookieCount}`);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
timing: (config, timing) => {
|
|
88
|
+
if (config.debug) {
|
|
89
|
+
const parts = [];
|
|
90
|
+
if (timing.dns)
|
|
91
|
+
parts.push(`DNS: ${timing.dns.toFixed(2)}ms`);
|
|
92
|
+
if (timing.connect)
|
|
93
|
+
parts.push(`Connect: ${timing.connect.toFixed(2)}ms`);
|
|
94
|
+
if (timing.tls)
|
|
95
|
+
parts.push(`TLS: ${timing.tls.toFixed(2)}ms`);
|
|
96
|
+
if (timing.ttfb)
|
|
97
|
+
parts.push(`TTFB: ${timing.ttfb.toFixed(2)}ms`);
|
|
98
|
+
if (timing.total)
|
|
99
|
+
parts.push(`Total: ${timing.total.toFixed(2)}ms`);
|
|
100
|
+
if (parts.length > 0) {
|
|
101
|
+
console.log(`[Rezo Debug] Timing: ${parts.join(" | ")}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
complete: (config, finalUrl, redirectCount, duration) => {
|
|
106
|
+
if (config.debug) {
|
|
107
|
+
console.log(`[Rezo Debug] Complete: ${finalUrl}`);
|
|
108
|
+
if (redirectCount > 0) {
|
|
109
|
+
console.log(`[Rezo Debug] Redirects: ${redirectCount}`);
|
|
110
|
+
}
|
|
111
|
+
console.log(`[Rezo Debug] Total Duration: ${duration.toFixed(2)}ms`);
|
|
112
|
+
console.log(`[Rezo Debug] ─────────────────────────────────────
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
20
117
|
async function decompressBuffer(buffer, contentEncoding) {
|
|
21
118
|
const encoding = contentEncoding.toLowerCase();
|
|
22
119
|
switch (encoding) {
|
|
@@ -100,40 +197,67 @@ class Http2SessionPool {
|
|
|
100
197
|
this.cleanupInterval.unref();
|
|
101
198
|
}
|
|
102
199
|
}
|
|
103
|
-
getSessionKey(url, options) {
|
|
104
|
-
|
|
200
|
+
getSessionKey(url, options, proxy) {
|
|
201
|
+
const proxyKey = proxy ? typeof proxy === "string" ? proxy : `${proxy.protocol}://${proxy.host}:${proxy.port}` : "";
|
|
202
|
+
return `${url.protocol}//${url.host}${proxyKey ? `@${proxyKey}` : ""}`;
|
|
105
203
|
}
|
|
106
|
-
|
|
107
|
-
|
|
204
|
+
isSessionHealthy(session, entry) {
|
|
205
|
+
if (session.closed || session.destroyed)
|
|
206
|
+
return false;
|
|
207
|
+
if (entry.goawayReceived)
|
|
208
|
+
return false;
|
|
209
|
+
const socket = session.socket;
|
|
210
|
+
if (socket && (socket.destroyed || socket.closed || !socket.writable))
|
|
211
|
+
return false;
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
async getSession(url, options, timeout, forceNew = false, proxy) {
|
|
215
|
+
const key = this.getSessionKey(url, options, proxy);
|
|
108
216
|
const existing = this.sessions.get(key);
|
|
109
|
-
if (
|
|
217
|
+
if (!forceNew && existing && this.isSessionHealthy(existing.session, existing)) {
|
|
110
218
|
existing.lastUsed = Date.now();
|
|
111
219
|
existing.refCount++;
|
|
112
220
|
return existing.session;
|
|
113
221
|
}
|
|
114
|
-
|
|
115
|
-
|
|
222
|
+
if (existing && !this.isSessionHealthy(existing.session, existing)) {
|
|
223
|
+
try {
|
|
224
|
+
existing.session.close();
|
|
225
|
+
} catch {}
|
|
226
|
+
this.sessions.delete(key);
|
|
227
|
+
}
|
|
228
|
+
const session = await this.createSession(url, options, timeout, proxy);
|
|
229
|
+
const entry = {
|
|
116
230
|
session,
|
|
117
231
|
lastUsed: Date.now(),
|
|
118
|
-
refCount: 1
|
|
119
|
-
|
|
232
|
+
refCount: 1,
|
|
233
|
+
goawayReceived: false,
|
|
234
|
+
proxy
|
|
235
|
+
};
|
|
236
|
+
this.sessions.set(key, entry);
|
|
120
237
|
session.on("close", () => {
|
|
121
238
|
this.sessions.delete(key);
|
|
122
239
|
});
|
|
123
240
|
session.on("error", () => {
|
|
124
241
|
this.sessions.delete(key);
|
|
125
242
|
});
|
|
243
|
+
session.on("goaway", () => {
|
|
244
|
+
entry.goawayReceived = true;
|
|
245
|
+
});
|
|
126
246
|
return session;
|
|
127
247
|
}
|
|
128
|
-
createSession(url, options, timeout) {
|
|
248
|
+
async createSession(url, options, timeout, proxy) {
|
|
249
|
+
const authority = `${url.protocol}//${url.host}`;
|
|
250
|
+
const sessionOptions = {
|
|
251
|
+
...options,
|
|
252
|
+
rejectUnauthorized: options?.rejectUnauthorized !== false,
|
|
253
|
+
ALPNProtocols: ["h2", "http/1.1"],
|
|
254
|
+
timeout
|
|
255
|
+
};
|
|
256
|
+
if (proxy) {
|
|
257
|
+
const tunnelSocket = await this.createProxyTunnel(url, proxy, timeout, options?.rejectUnauthorized);
|
|
258
|
+
sessionOptions.createConnection = () => tunnelSocket;
|
|
259
|
+
}
|
|
129
260
|
return new Promise((resolve, reject) => {
|
|
130
|
-
const authority = `${url.protocol}//${url.host}`;
|
|
131
|
-
const sessionOptions = {
|
|
132
|
-
...options,
|
|
133
|
-
rejectUnauthorized: options?.rejectUnauthorized !== false,
|
|
134
|
-
ALPNProtocols: ["h2", "http/1.1"],
|
|
135
|
-
timeout
|
|
136
|
-
};
|
|
137
261
|
const session = http2.connect(authority, sessionOptions);
|
|
138
262
|
let settled = false;
|
|
139
263
|
const timeoutId = timeout ? setTimeout(() => {
|
|
@@ -164,8 +288,186 @@ class Http2SessionPool {
|
|
|
164
288
|
});
|
|
165
289
|
});
|
|
166
290
|
}
|
|
167
|
-
|
|
168
|
-
|
|
291
|
+
async createProxyTunnel(url, proxy, timeout, rejectUnauthorized) {
|
|
292
|
+
return new Promise((resolve, reject) => {
|
|
293
|
+
let proxyUrl;
|
|
294
|
+
let proxyAuth;
|
|
295
|
+
if (typeof proxy === "string") {
|
|
296
|
+
proxyUrl = new URL(proxy);
|
|
297
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
298
|
+
proxyAuth = Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString("base64");
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
const protocol = proxy.protocol || "http";
|
|
302
|
+
let proxyUrlStr = `${protocol}://${proxy.host}:${proxy.port}`;
|
|
303
|
+
if (proxy.auth) {
|
|
304
|
+
const encodedUser = encodeURIComponent(proxy.auth.username);
|
|
305
|
+
const encodedPass = encodeURIComponent(proxy.auth.password);
|
|
306
|
+
proxyUrlStr = `${protocol}://${encodedUser}:${encodedPass}@${proxy.host}:${proxy.port}`;
|
|
307
|
+
proxyAuth = Buffer.from(`${proxy.auth.username}:${proxy.auth.password}`).toString("base64");
|
|
308
|
+
}
|
|
309
|
+
proxyUrl = new URL(proxyUrlStr);
|
|
310
|
+
}
|
|
311
|
+
const targetHost = url.hostname;
|
|
312
|
+
const targetPort = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
313
|
+
if (proxyUrl.protocol.startsWith("socks")) {
|
|
314
|
+
const socksType = proxyUrl.protocol === "socks5:" || proxyUrl.protocol === "socks5h:" ? 5 : 4;
|
|
315
|
+
const socksOpts = {
|
|
316
|
+
proxy: {
|
|
317
|
+
host: proxyUrl.hostname,
|
|
318
|
+
port: parseInt(proxyUrl.port || "1080", 10),
|
|
319
|
+
type: socksType,
|
|
320
|
+
userId: proxyUrl.username ? decodeURIComponent(proxyUrl.username) : undefined,
|
|
321
|
+
password: proxyUrl.password ? decodeURIComponent(proxyUrl.password) : undefined
|
|
322
|
+
},
|
|
323
|
+
destination: {
|
|
324
|
+
host: targetHost,
|
|
325
|
+
port: parseInt(targetPort, 10)
|
|
326
|
+
},
|
|
327
|
+
command: "connect",
|
|
328
|
+
timeout
|
|
329
|
+
};
|
|
330
|
+
SocksClient.createConnection(socksOpts).then(({ socket }) => {
|
|
331
|
+
if (url.protocol === "https:") {
|
|
332
|
+
const tlsSocket = tls.connect({
|
|
333
|
+
socket,
|
|
334
|
+
host: targetHost,
|
|
335
|
+
servername: targetHost,
|
|
336
|
+
rejectUnauthorized: rejectUnauthorized !== false,
|
|
337
|
+
ALPNProtocols: ["h2", "http/1.1"]
|
|
338
|
+
});
|
|
339
|
+
const tlsTimeoutId = timeout ? setTimeout(() => {
|
|
340
|
+
tlsSocket.destroy();
|
|
341
|
+
reject(new Error(`TLS handshake timeout after ${timeout}ms`));
|
|
342
|
+
}, timeout) : null;
|
|
343
|
+
tlsSocket.on("secureConnect", () => {
|
|
344
|
+
if (tlsTimeoutId)
|
|
345
|
+
clearTimeout(tlsTimeoutId);
|
|
346
|
+
const alpn = tlsSocket.alpnProtocol;
|
|
347
|
+
if (alpn && alpn !== "h2") {
|
|
348
|
+
tlsSocket.destroy();
|
|
349
|
+
reject(new Error(`Server does not support HTTP/2 (negotiated: ${alpn})`));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
resolve(tlsSocket);
|
|
353
|
+
});
|
|
354
|
+
tlsSocket.on("error", (err) => {
|
|
355
|
+
if (tlsTimeoutId)
|
|
356
|
+
clearTimeout(tlsTimeoutId);
|
|
357
|
+
reject(new Error(`TLS handshake failed: ${err.message}`));
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
resolve(socket);
|
|
361
|
+
}
|
|
362
|
+
}).catch((err) => {
|
|
363
|
+
reject(new Error(`SOCKS proxy connection failed: ${err.message}`));
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const proxyHost = proxyUrl.hostname;
|
|
368
|
+
const proxyPort = parseInt(proxyUrl.port || (proxyUrl.protocol === "https:" ? "443" : "80"), 10);
|
|
369
|
+
let proxySocket;
|
|
370
|
+
const connectToProxy = () => {
|
|
371
|
+
if (proxyUrl.protocol === "https:") {
|
|
372
|
+
proxySocket = tls.connect({
|
|
373
|
+
host: proxyHost,
|
|
374
|
+
port: proxyPort,
|
|
375
|
+
rejectUnauthorized: rejectUnauthorized !== false
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
proxySocket = net.connect({
|
|
379
|
+
host: proxyHost,
|
|
380
|
+
port: proxyPort
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
let settled = false;
|
|
384
|
+
const timeoutId = timeout ? setTimeout(() => {
|
|
385
|
+
if (!settled) {
|
|
386
|
+
settled = true;
|
|
387
|
+
proxySocket.destroy();
|
|
388
|
+
reject(new Error(`Proxy connection timeout after ${timeout}ms`));
|
|
389
|
+
}
|
|
390
|
+
}, timeout) : null;
|
|
391
|
+
proxySocket.on("error", (err) => {
|
|
392
|
+
if (!settled) {
|
|
393
|
+
settled = true;
|
|
394
|
+
if (timeoutId)
|
|
395
|
+
clearTimeout(timeoutId);
|
|
396
|
+
reject(new Error(`Proxy connection error: ${err.message}`));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
proxySocket.on("connect", () => {
|
|
400
|
+
const connectRequest = [
|
|
401
|
+
`CONNECT ${targetHost}:${targetPort} HTTP/1.1`,
|
|
402
|
+
`Host: ${targetHost}:${targetPort}`,
|
|
403
|
+
proxyAuth ? `Proxy-Authorization: Basic ${proxyAuth}` : "",
|
|
404
|
+
"",
|
|
405
|
+
""
|
|
406
|
+
].filter(Boolean).join(`\r
|
|
407
|
+
`);
|
|
408
|
+
proxySocket.write(connectRequest);
|
|
409
|
+
});
|
|
410
|
+
let responseBuffer = "";
|
|
411
|
+
proxySocket.on("data", function onData(data) {
|
|
412
|
+
if (settled)
|
|
413
|
+
return;
|
|
414
|
+
responseBuffer += data.toString();
|
|
415
|
+
const headerEnd = responseBuffer.indexOf(`\r
|
|
416
|
+
\r
|
|
417
|
+
`);
|
|
418
|
+
if (headerEnd !== -1) {
|
|
419
|
+
settled = true;
|
|
420
|
+
if (timeoutId)
|
|
421
|
+
clearTimeout(timeoutId);
|
|
422
|
+
proxySocket.removeListener("data", onData);
|
|
423
|
+
const statusLine = responseBuffer.split(`\r
|
|
424
|
+
`)[0];
|
|
425
|
+
const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d{3})/);
|
|
426
|
+
const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
|
427
|
+
if (statusCode === 200) {
|
|
428
|
+
if (url.protocol === "https:") {
|
|
429
|
+
const tlsSocket = tls.connect({
|
|
430
|
+
socket: proxySocket,
|
|
431
|
+
host: targetHost,
|
|
432
|
+
servername: targetHost,
|
|
433
|
+
rejectUnauthorized: rejectUnauthorized !== false,
|
|
434
|
+
ALPNProtocols: ["h2", "http/1.1"]
|
|
435
|
+
});
|
|
436
|
+
const tlsTimeoutId = timeout ? setTimeout(() => {
|
|
437
|
+
tlsSocket.destroy();
|
|
438
|
+
reject(new Error(`TLS handshake timeout after ${timeout}ms`));
|
|
439
|
+
}, timeout) : null;
|
|
440
|
+
tlsSocket.on("secureConnect", () => {
|
|
441
|
+
if (tlsTimeoutId)
|
|
442
|
+
clearTimeout(tlsTimeoutId);
|
|
443
|
+
const alpn = tlsSocket.alpnProtocol;
|
|
444
|
+
if (alpn && alpn !== "h2") {
|
|
445
|
+
tlsSocket.destroy();
|
|
446
|
+
reject(new Error(`Server does not support HTTP/2 (negotiated: ${alpn})`));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
resolve(tlsSocket);
|
|
450
|
+
});
|
|
451
|
+
tlsSocket.on("error", (err) => {
|
|
452
|
+
if (tlsTimeoutId)
|
|
453
|
+
clearTimeout(tlsTimeoutId);
|
|
454
|
+
reject(new Error(`TLS handshake failed: ${err.message}`));
|
|
455
|
+
});
|
|
456
|
+
} else {
|
|
457
|
+
resolve(proxySocket);
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
proxySocket.destroy();
|
|
461
|
+
reject(new Error(`Proxy CONNECT failed with status ${statusCode}: ${statusLine}`));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
connectToProxy();
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
releaseSession(url, proxy) {
|
|
470
|
+
const key = this.getSessionKey(url, undefined, proxy);
|
|
169
471
|
const entry = this.sessions.get(key);
|
|
170
472
|
if (entry) {
|
|
171
473
|
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
@@ -178,8 +480,8 @@ class Http2SessionPool {
|
|
|
178
480
|
}
|
|
179
481
|
}
|
|
180
482
|
}
|
|
181
|
-
closeSession(url) {
|
|
182
|
-
const key = this.getSessionKey(url);
|
|
483
|
+
closeSession(url, proxy) {
|
|
484
|
+
const key = this.getSessionKey(url, undefined, proxy);
|
|
183
485
|
const entry = this.sessions.get(key);
|
|
184
486
|
if (entry) {
|
|
185
487
|
entry.session.close();
|
|
@@ -308,7 +610,7 @@ function sanitizeConfig(config) {
|
|
|
308
610
|
const { data: _data, ...sanitized } = config;
|
|
309
611
|
return sanitized;
|
|
310
612
|
}
|
|
311
|
-
async function updateCookies(config, headers, url) {
|
|
613
|
+
async function updateCookies(config, headers, url, rootJar) {
|
|
312
614
|
const setCookieHeaders = headers["set-cookie"];
|
|
313
615
|
if (!setCookieHeaders)
|
|
314
616
|
return;
|
|
@@ -352,8 +654,9 @@ async function updateCookies(config, headers, url) {
|
|
|
352
654
|
const acceptedCookieStrings = acceptedCookies.map((c) => c.toSetCookieString());
|
|
353
655
|
const jar = new RezoCookieJar;
|
|
354
656
|
jar.setCookiesSync(acceptedCookieStrings, url);
|
|
355
|
-
|
|
356
|
-
|
|
657
|
+
const jarToSync = rootJar || config.cookieJar;
|
|
658
|
+
if (!config.disableCookieJar && jarToSync) {
|
|
659
|
+
jarToSync.setCookiesSync(acceptedCookieStrings, url);
|
|
357
660
|
}
|
|
358
661
|
const cookies = jar.cookies();
|
|
359
662
|
cookies.setCookiesString = cookieHeaderArray;
|
|
@@ -525,7 +828,7 @@ export async function executeRequest(options, defaultOptions, jar) {
|
|
|
525
828
|
}
|
|
526
829
|
}
|
|
527
830
|
try {
|
|
528
|
-
const res = executeHttp2Request(fetchOptions, mainConfig, options, perform, fs, streamResponse, downloadResponse, uploadResponse);
|
|
831
|
+
const res = executeHttp2Request(fetchOptions, mainConfig, options, perform, fs, streamResponse, downloadResponse, uploadResponse, jar);
|
|
529
832
|
if (streamResponse) {
|
|
530
833
|
return streamResponse;
|
|
531
834
|
} else if (downloadResponse) {
|
|
@@ -558,7 +861,7 @@ export async function executeRequest(options, defaultOptions, jar) {
|
|
|
558
861
|
throw error;
|
|
559
862
|
}
|
|
560
863
|
}
|
|
561
|
-
async function executeHttp2Request(fetchOptions, config, options, perform, fs, streamResult, downloadResult, uploadResult) {
|
|
864
|
+
async function executeHttp2Request(fetchOptions, config, options, perform, fs, streamResult, downloadResult, uploadResult, rootJar) {
|
|
562
865
|
let requestCount = 0;
|
|
563
866
|
const _stats = { statusOnNext: "abort" };
|
|
564
867
|
let responseStatusCode;
|
|
@@ -578,6 +881,11 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
|
|
|
578
881
|
config.setSignal();
|
|
579
882
|
const timeoutClearInstance = config.timeoutClearInstanse;
|
|
580
883
|
delete config.timeoutClearInstanse;
|
|
884
|
+
if (!config.requestId) {
|
|
885
|
+
config.requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
886
|
+
}
|
|
887
|
+
const requestUrl = fetchOptions.fullUrl ? String(fetchOptions.fullUrl) : "";
|
|
888
|
+
debugLog.requestStart(config, requestUrl, fetchOptions.method || "GET");
|
|
581
889
|
const eventEmitter = streamResult || downloadResult || uploadResult;
|
|
582
890
|
if (eventEmitter) {
|
|
583
891
|
eventEmitter.emit("initiated");
|
|
@@ -590,7 +898,7 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
|
|
|
590
898
|
throw error;
|
|
591
899
|
}
|
|
592
900
|
try {
|
|
593
|
-
const response = await executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool);
|
|
901
|
+
const response = await executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool, rootJar);
|
|
594
902
|
const statusOnNext = _stats.statusOnNext;
|
|
595
903
|
if (response instanceof RezoError) {
|
|
596
904
|
const fileName = config.fileName;
|
|
@@ -619,16 +927,12 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
|
|
|
619
927
|
throw response;
|
|
620
928
|
}
|
|
621
929
|
if (maxRetries <= retries) {
|
|
622
|
-
|
|
623
|
-
console.log(`Max retries (${maxRetries}) reached`);
|
|
624
|
-
}
|
|
930
|
+
debugLog.maxRetries(config, maxRetries);
|
|
625
931
|
throw response;
|
|
626
932
|
}
|
|
627
933
|
retries++;
|
|
628
934
|
const currentDelay = incrementDelay ? retryDelay * retries : retryDelay;
|
|
629
|
-
|
|
630
|
-
console.log(`Retrying... ${retryDelay > 0 ? "in " + currentDelay + "ms" : ""}`);
|
|
631
|
-
}
|
|
935
|
+
debugLog.retry(config, retries, maxRetries, responseStatusCode || 0, currentDelay);
|
|
632
936
|
if (config.hooks?.beforeRetry && config.hooks.beforeRetry.length > 0) {
|
|
633
937
|
for (const hook of config.hooks.beforeRetry) {
|
|
634
938
|
await hook(config, response, retries);
|
|
@@ -643,6 +947,16 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
|
|
|
643
947
|
continue;
|
|
644
948
|
}
|
|
645
949
|
if (statusOnNext === "success") {
|
|
950
|
+
const totalDuration = performance.now() - timing.startTime;
|
|
951
|
+
debugLog.response(config, response.status, response.statusText, totalDuration);
|
|
952
|
+
if (response.headers) {
|
|
953
|
+
const headersObj = response.headers instanceof RezoHeaders ? response.headers.toObject() : response.headers;
|
|
954
|
+
debugLog.responseHeaders(config, headersObj);
|
|
955
|
+
}
|
|
956
|
+
if (response.cookies?.array) {
|
|
957
|
+
debugLog.cookies(config, response.cookies.array.length);
|
|
958
|
+
}
|
|
959
|
+
debugLog.complete(config, response.finalUrl || requestUrl, config.redirectCount, totalDuration);
|
|
646
960
|
return response;
|
|
647
961
|
}
|
|
648
962
|
if (statusOnNext === "error") {
|
|
@@ -686,18 +1000,20 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
|
|
|
686
1000
|
visitedUrls.add(normalizedRedirectUrl);
|
|
687
1001
|
}
|
|
688
1002
|
const redirectCode = response.status;
|
|
1003
|
+
const fromUrl = fetchOptions.fullUrl;
|
|
689
1004
|
const redirectCallback = config.beforeRedirect || config.onRedirect;
|
|
690
1005
|
const onRedirect = redirectCallback ? redirectCallback({
|
|
691
1006
|
url: new URL(location),
|
|
692
1007
|
status: response.status,
|
|
693
1008
|
headers: response.headers,
|
|
694
1009
|
sameDomain: isSameDomain(fetchOptions.fullUrl, location),
|
|
695
|
-
method: fetchOptions.method.toUpperCase()
|
|
1010
|
+
method: fetchOptions.method.toUpperCase(),
|
|
1011
|
+
body: config.originalBody
|
|
696
1012
|
}) : undefined;
|
|
697
1013
|
if (typeof onRedirect !== "undefined") {
|
|
698
1014
|
if (typeof onRedirect === "boolean" && !onRedirect) {
|
|
699
1015
|
throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
|
|
700
|
-
} else if (typeof onRedirect === "object" && !onRedirect.redirect) {
|
|
1016
|
+
} else if (typeof onRedirect === "object" && !onRedirect.redirect && !onRedirect.withoutBody && !("body" in onRedirect)) {
|
|
701
1017
|
throw builErrorFromResponse("Redirect denied by user", response, config, fetchOptions);
|
|
702
1018
|
}
|
|
703
1019
|
}
|
|
@@ -717,14 +1033,88 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
|
|
|
717
1033
|
});
|
|
718
1034
|
perform.reset();
|
|
719
1035
|
config.redirectCount++;
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1036
|
+
fetchOptions.fullUrl = location;
|
|
1037
|
+
delete options.params;
|
|
1038
|
+
const normalizedRedirect = typeof onRedirect === "object" ? onRedirect.redirect || onRedirect.withoutBody || "body" in onRedirect : undefined;
|
|
1039
|
+
if (typeof onRedirect === "object" && normalizedRedirect) {
|
|
1040
|
+
let method;
|
|
1041
|
+
const userMethod = onRedirect.method;
|
|
1042
|
+
if (redirectCode === 301 || redirectCode === 302 || redirectCode === 303) {
|
|
1043
|
+
method = userMethod || "GET";
|
|
1044
|
+
} else {
|
|
1045
|
+
method = userMethod || fetchOptions.method;
|
|
1046
|
+
}
|
|
1047
|
+
config.method = method;
|
|
1048
|
+
options.method = method;
|
|
1049
|
+
fetchOptions.method = method;
|
|
1050
|
+
if (onRedirect.redirect && onRedirect.url) {
|
|
1051
|
+
options.fullUrl = onRedirect.url;
|
|
1052
|
+
fetchOptions.fullUrl = onRedirect.url;
|
|
1053
|
+
}
|
|
1054
|
+
if (onRedirect.withoutBody) {
|
|
1055
|
+
delete options.body;
|
|
1056
|
+
delete fetchOptions.body;
|
|
1057
|
+
config.originalBody = undefined;
|
|
1058
|
+
if (fetchOptions.headers instanceof RezoHeaders) {
|
|
1059
|
+
fetchOptions.headers.delete("Content-Type");
|
|
1060
|
+
fetchOptions.headers.delete("Content-Length");
|
|
1061
|
+
}
|
|
1062
|
+
} else if ("body" in onRedirect) {
|
|
1063
|
+
options.body = onRedirect.body;
|
|
1064
|
+
fetchOptions.body = onRedirect.body;
|
|
1065
|
+
config.originalBody = onRedirect.body;
|
|
1066
|
+
} else if (redirectCode === 307 || redirectCode === 308) {
|
|
1067
|
+
const methodUpper = method.toUpperCase();
|
|
1068
|
+
if ((methodUpper === "POST" || methodUpper === "PUT" || methodUpper === "PATCH") && config.originalBody !== undefined) {
|
|
1069
|
+
options.body = config.originalBody;
|
|
1070
|
+
fetchOptions.body = config.originalBody;
|
|
1071
|
+
}
|
|
1072
|
+
} else {
|
|
723
1073
|
delete options.body;
|
|
1074
|
+
delete fetchOptions.body;
|
|
1075
|
+
if (fetchOptions.headers instanceof RezoHeaders) {
|
|
1076
|
+
fetchOptions.headers.delete("Content-Type");
|
|
1077
|
+
fetchOptions.headers.delete("Content-Length");
|
|
1078
|
+
}
|
|
724
1079
|
}
|
|
1080
|
+
debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, method);
|
|
1081
|
+
} else if (response.status === 301 || response.status === 302 || response.status === 303) {
|
|
1082
|
+
debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, "GET");
|
|
1083
|
+
options.method = "GET";
|
|
1084
|
+
fetchOptions.method = "GET";
|
|
1085
|
+
config.method = "GET";
|
|
1086
|
+
delete options.body;
|
|
1087
|
+
delete fetchOptions.body;
|
|
1088
|
+
if (fetchOptions.headers instanceof RezoHeaders) {
|
|
1089
|
+
fetchOptions.headers.delete("Content-Type");
|
|
1090
|
+
fetchOptions.headers.delete("Content-Length");
|
|
1091
|
+
}
|
|
1092
|
+
} else {
|
|
1093
|
+
debugLog.redirect(config, fromUrl, fetchOptions.fullUrl, redirectCode, fetchOptions.method);
|
|
1094
|
+
}
|
|
1095
|
+
const jarToSync = rootJar || config.cookieJar;
|
|
1096
|
+
if (response.cookies?.setCookiesString?.length > 0 && jarToSync) {
|
|
1097
|
+
try {
|
|
1098
|
+
jarToSync.setCookiesSync(response.cookies.setCookiesString, fromUrl);
|
|
1099
|
+
} catch (e) {}
|
|
1100
|
+
}
|
|
1101
|
+
if (jarToSync && !config.disableCookieJar) {
|
|
1102
|
+
try {
|
|
1103
|
+
const cookieString = jarToSync.getCookieStringSync(fetchOptions.fullUrl);
|
|
1104
|
+
if (cookieString) {
|
|
1105
|
+
if (fetchOptions.headers instanceof RezoHeaders) {
|
|
1106
|
+
fetchOptions.headers.set("cookie", cookieString);
|
|
1107
|
+
} else if (fetchOptions.headers) {
|
|
1108
|
+
fetchOptions.headers["cookie"] = cookieString;
|
|
1109
|
+
} else {
|
|
1110
|
+
fetchOptions.headers = new RezoHeaders({ cookie: cookieString });
|
|
1111
|
+
}
|
|
1112
|
+
if (config.debug) {
|
|
1113
|
+
console.log(`[Rezo Debug] HTTP/2: Updated Cookie header for redirect: ${cookieString.substring(0, 100)}...`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
} catch (e) {}
|
|
725
1117
|
}
|
|
726
|
-
fetchOptions.fullUrl = location;
|
|
727
|
-
delete options.params;
|
|
728
1118
|
requestCount++;
|
|
729
1119
|
continue;
|
|
730
1120
|
}
|
|
@@ -737,7 +1127,7 @@ async function executeHttp2Request(fetchOptions, config, options, perform, fs, s
|
|
|
737
1127
|
}
|
|
738
1128
|
}
|
|
739
1129
|
}
|
|
740
|
-
async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool) {
|
|
1130
|
+
async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _stats, responseStatusCode, fs, streamResult, downloadResult, uploadResult, sessionPool, rootJar) {
|
|
741
1131
|
return new Promise(async (resolve) => {
|
|
742
1132
|
try {
|
|
743
1133
|
const { fullUrl, body } = fetchOptions;
|
|
@@ -764,6 +1154,11 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
|
|
|
764
1154
|
if (!headers["accept-encoding"]) {
|
|
765
1155
|
headers["accept-encoding"] = "gzip, deflate, br";
|
|
766
1156
|
}
|
|
1157
|
+
if (config.debug && headers["cookie"]) {
|
|
1158
|
+
console.log(`[Rezo Debug] HTTP/2: Sending Cookie header: ${String(headers["cookie"]).substring(0, 100)}...`);
|
|
1159
|
+
} else if (config.debug) {
|
|
1160
|
+
console.log(`[Rezo Debug] HTTP/2: No Cookie header in request`);
|
|
1161
|
+
}
|
|
767
1162
|
if (body instanceof RezoFormData) {
|
|
768
1163
|
headers["content-type"] = `multipart/form-data; boundary=${body.getBoundary()}`;
|
|
769
1164
|
} else if (body instanceof FormData) {
|
|
@@ -797,30 +1192,136 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
|
|
|
797
1192
|
sessionOptions.pfx = securityContext.pfx;
|
|
798
1193
|
if (securityContext?.passphrase)
|
|
799
1194
|
sessionOptions.passphrase = securityContext.passphrase;
|
|
1195
|
+
const forceNewSession = requestCount > 0;
|
|
800
1196
|
let session;
|
|
1197
|
+
if (config.debug) {
|
|
1198
|
+
console.log(`[Rezo Debug] HTTP/2: Acquiring session for ${url.host}${forceNewSession ? " (forcing new for redirect)" : ""}${fetchOptions.proxy ? " (via proxy)" : ""}...`);
|
|
1199
|
+
}
|
|
801
1200
|
try {
|
|
802
|
-
session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined);
|
|
1201
|
+
session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined, forceNewSession, fetchOptions.proxy);
|
|
1202
|
+
if (config.debug) {
|
|
1203
|
+
console.log(`[Rezo Debug] HTTP/2: Session acquired successfully`);
|
|
1204
|
+
}
|
|
803
1205
|
} catch (err) {
|
|
1206
|
+
if (config.debug) {
|
|
1207
|
+
console.log(`[Rezo Debug] HTTP/2: Session failed:`, err.message);
|
|
1208
|
+
}
|
|
804
1209
|
const error = buildSmartError(config, fetchOptions, err);
|
|
805
1210
|
_stats.statusOnNext = "error";
|
|
806
1211
|
resolve(error);
|
|
807
1212
|
return;
|
|
808
1213
|
}
|
|
809
|
-
const req = session.request(headers);
|
|
810
|
-
if (config.timeout) {
|
|
811
|
-
req.setTimeout(config.timeout, () => {
|
|
812
|
-
req.close(http2.constants.NGHTTP2_CANCEL);
|
|
813
|
-
const error = buildSmartError(config, fetchOptions, new Error(`Request timeout after ${config.timeout}ms`));
|
|
814
|
-
_stats.statusOnNext = "error";
|
|
815
|
-
resolve(error);
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
1214
|
let chunks = [];
|
|
819
1215
|
let contentLengthCounter = 0;
|
|
820
1216
|
let responseHeaders = {};
|
|
821
1217
|
let status = 0;
|
|
822
1218
|
let statusText = "";
|
|
1219
|
+
let resolved = false;
|
|
1220
|
+
let isRedirect = false;
|
|
1221
|
+
let timeoutId = null;
|
|
1222
|
+
const sessionErrorHandler = (err) => {
|
|
1223
|
+
if (config.debug) {
|
|
1224
|
+
console.log(`[Rezo Debug] HTTP/2: Session error:`, err.message);
|
|
1225
|
+
}
|
|
1226
|
+
if (!resolved) {
|
|
1227
|
+
resolved = true;
|
|
1228
|
+
if (timeoutId)
|
|
1229
|
+
clearTimeout(timeoutId);
|
|
1230
|
+
const error = buildSmartError(config, fetchOptions, err);
|
|
1231
|
+
_stats.statusOnNext = "error";
|
|
1232
|
+
resolve(error);
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
session.on("error", sessionErrorHandler);
|
|
1236
|
+
session.on("goaway", (errorCode, lastStreamID) => {
|
|
1237
|
+
if (config.debug) {
|
|
1238
|
+
console.log(`[Rezo Debug] HTTP/2: Session GOAWAY received (errorCode: ${errorCode}, lastStreamID: ${lastStreamID})`);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
if (config.debug) {
|
|
1242
|
+
console.log(`[Rezo Debug] HTTP/2: Creating request stream...`);
|
|
1243
|
+
}
|
|
1244
|
+
let req;
|
|
1245
|
+
try {
|
|
1246
|
+
req = session.request(headers);
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
if (config.debug) {
|
|
1249
|
+
console.log(`[Rezo Debug] HTTP/2: Failed to create request stream:`, err.message);
|
|
1250
|
+
}
|
|
1251
|
+
session.removeListener("error", sessionErrorHandler);
|
|
1252
|
+
const error = buildSmartError(config, fetchOptions, err);
|
|
1253
|
+
_stats.statusOnNext = "error";
|
|
1254
|
+
resolve(error);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (config.debug) {
|
|
1258
|
+
console.log(`[Rezo Debug] HTTP/2: Request stream created`);
|
|
1259
|
+
}
|
|
1260
|
+
const requestTimeout = config.timeout || 30000;
|
|
1261
|
+
timeoutId = setTimeout(() => {
|
|
1262
|
+
if (!resolved) {
|
|
1263
|
+
resolved = true;
|
|
1264
|
+
if (config.debug) {
|
|
1265
|
+
console.log(`[Rezo Debug] HTTP/2: Request timeout after ${requestTimeout}ms (no response received)`);
|
|
1266
|
+
}
|
|
1267
|
+
req.close(http2.constants.NGHTTP2_CANCEL);
|
|
1268
|
+
const error = buildSmartError(config, fetchOptions, new Error(`Request timeout after ${requestTimeout}ms`));
|
|
1269
|
+
_stats.statusOnNext = "error";
|
|
1270
|
+
resolve(error);
|
|
1271
|
+
}
|
|
1272
|
+
}, requestTimeout);
|
|
1273
|
+
const sessionSocket = session.socket;
|
|
1274
|
+
if (sessionSocket && typeof sessionSocket.ref === "function") {
|
|
1275
|
+
sessionSocket.ref();
|
|
1276
|
+
}
|
|
1277
|
+
req.on("close", () => {
|
|
1278
|
+
if (config.debug && !resolved) {
|
|
1279
|
+
console.log(`[Rezo Debug] HTTP/2: Stream closed (status: ${status}, resolved: ${resolved})`);
|
|
1280
|
+
}
|
|
1281
|
+
if (!resolved && status === 0) {
|
|
1282
|
+
resolved = true;
|
|
1283
|
+
clearTimeout(timeoutId);
|
|
1284
|
+
if (config.debug) {
|
|
1285
|
+
console.log(`[Rezo Debug] HTTP/2: Stream closed without response - retrying with new session`);
|
|
1286
|
+
}
|
|
1287
|
+
const error = buildSmartError(config, fetchOptions, new Error("HTTP/2 stream closed without response"));
|
|
1288
|
+
_stats.statusOnNext = "error";
|
|
1289
|
+
resolve(error);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
req.on("aborted", () => {
|
|
1293
|
+
if (config.debug) {
|
|
1294
|
+
console.log(`[Rezo Debug] HTTP/2: Stream aborted`);
|
|
1295
|
+
}
|
|
1296
|
+
if (!resolved) {
|
|
1297
|
+
resolved = true;
|
|
1298
|
+
clearTimeout(timeoutId);
|
|
1299
|
+
const error = buildSmartError(config, fetchOptions, new Error("HTTP/2 stream aborted"));
|
|
1300
|
+
_stats.statusOnNext = "error";
|
|
1301
|
+
resolve(error);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
req.on("error", (err) => {
|
|
1305
|
+
if (config.debug) {
|
|
1306
|
+
console.log(`[Rezo Debug] HTTP/2: Stream error:`, err.message);
|
|
1307
|
+
}
|
|
1308
|
+
if (!resolved) {
|
|
1309
|
+
resolved = true;
|
|
1310
|
+
clearTimeout(timeoutId);
|
|
1311
|
+
const error = buildSmartError(config, fetchOptions, err);
|
|
1312
|
+
_stats.statusOnNext = "error";
|
|
1313
|
+
resolve(error);
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
req.on("frameError", (type, code, id) => {
|
|
1317
|
+
if (config.debug) {
|
|
1318
|
+
console.log(`[Rezo Debug] HTTP/2: Frame error - type: ${type}, code: ${code}, id: ${id}`);
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
823
1321
|
req.on("response", (headers) => {
|
|
1322
|
+
if (config.debug) {
|
|
1323
|
+
console.log(`[Rezo Debug] HTTP/2: Response received, status: ${headers[":status"]}`);
|
|
1324
|
+
}
|
|
824
1325
|
responseHeaders = headers;
|
|
825
1326
|
status = Number(headers[http2.constants.HTTP2_HEADER_STATUS]) || 200;
|
|
826
1327
|
statusText = getStatusText(status);
|
|
@@ -829,7 +1330,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
|
|
|
829
1330
|
config.timing.responseStart = timing.firstByteTime;
|
|
830
1331
|
}
|
|
831
1332
|
const location = headers["location"];
|
|
832
|
-
|
|
1333
|
+
isRedirect = status >= 300 && status < 400 && !!location;
|
|
833
1334
|
if (isRedirect) {
|
|
834
1335
|
_stats.statusOnNext = "redirect";
|
|
835
1336
|
const redirectUrlObj = new URL(location, url);
|
|
@@ -841,7 +1342,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
|
|
|
841
1342
|
config.network.httpVersion = "h2";
|
|
842
1343
|
(async () => {
|
|
843
1344
|
try {
|
|
844
|
-
await updateCookies(config, headers, url.href);
|
|
1345
|
+
await updateCookies(config, headers, url.href, rootJar);
|
|
845
1346
|
} catch (err) {
|
|
846
1347
|
if (config.debug) {
|
|
847
1348
|
console.log("[Rezo Debug] Cookie hook error:", err);
|
|
@@ -874,6 +1375,9 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
|
|
|
874
1375
|
}
|
|
875
1376
|
});
|
|
876
1377
|
req.on("data", (chunk) => {
|
|
1378
|
+
if (config.debug) {
|
|
1379
|
+
console.log(`[Rezo Debug] HTTP/2: Received data chunk: ${chunk.length} bytes (total: ${contentLengthCounter + chunk.length})`);
|
|
1380
|
+
}
|
|
877
1381
|
chunks.push(chunk);
|
|
878
1382
|
contentLengthCounter += chunk.length;
|
|
879
1383
|
if (streamResult) {
|
|
@@ -895,209 +1399,221 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
|
|
|
895
1399
|
}
|
|
896
1400
|
});
|
|
897
1401
|
req.on("end", async () => {
|
|
898
|
-
|
|
899
|
-
if (!config.transfer) {
|
|
900
|
-
config.transfer = { requestSize: 0, responseSize: 0, headerSize: 0, bodySize: 0 };
|
|
901
|
-
}
|
|
902
|
-
if (config.transfer.requestSize === undefined) {
|
|
903
|
-
config.transfer.requestSize = 0;
|
|
904
|
-
}
|
|
905
|
-
if (config.transfer.requestSize === 0 && body) {
|
|
906
|
-
if (typeof body === "string") {
|
|
907
|
-
config.transfer.requestSize = Buffer.byteLength(body, "utf8");
|
|
908
|
-
} else if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
|
|
909
|
-
config.transfer.requestSize = body.length;
|
|
910
|
-
} else if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
|
|
911
|
-
config.transfer.requestSize = Buffer.byteLength(body.toString(), "utf8");
|
|
912
|
-
} else if (body instanceof RezoFormData) {
|
|
913
|
-
config.transfer.requestSize = await body.getLength() || 0;
|
|
914
|
-
} else if (typeof body === "object") {
|
|
915
|
-
config.transfer.requestSize = Buffer.byteLength(JSON.stringify(body), "utf8");
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
(sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
|
|
919
|
-
if (_stats.statusOnNext === "redirect") {
|
|
920
|
-
const partialResponse = {
|
|
921
|
-
data: "",
|
|
922
|
-
status,
|
|
923
|
-
statusText,
|
|
924
|
-
headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
|
|
925
|
-
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
926
|
-
config,
|
|
927
|
-
contentType: responseHeaders["content-type"],
|
|
928
|
-
contentLength: contentLengthCounter,
|
|
929
|
-
finalUrl: url.href,
|
|
930
|
-
urls: buildUrlTree(config, url.href)
|
|
931
|
-
};
|
|
932
|
-
resolve(partialResponse);
|
|
1402
|
+
if (resolved)
|
|
933
1403
|
return;
|
|
1404
|
+
if (config.debug) {
|
|
1405
|
+
console.log(`[Rezo Debug] HTTP/2: Stream 'end' event fired (status: ${status}, chunks: ${chunks.length}, bytes: ${contentLengthCounter})`);
|
|
934
1406
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1407
|
+
resolved = true;
|
|
1408
|
+
clearTimeout(timeoutId);
|
|
1409
|
+
try {
|
|
1410
|
+
updateTiming(config, timing, contentLengthCounter);
|
|
1411
|
+
if (!config.transfer) {
|
|
1412
|
+
config.transfer = { requestSize: 0, responseSize: 0, headerSize: 0, bodySize: 0 };
|
|
1413
|
+
}
|
|
1414
|
+
if (config.transfer.requestSize === undefined) {
|
|
1415
|
+
config.transfer.requestSize = 0;
|
|
1416
|
+
}
|
|
1417
|
+
if (config.transfer.requestSize === 0 && body) {
|
|
1418
|
+
if (typeof body === "string") {
|
|
1419
|
+
config.transfer.requestSize = Buffer.byteLength(body, "utf8");
|
|
1420
|
+
} else if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
|
|
1421
|
+
config.transfer.requestSize = body.length;
|
|
1422
|
+
} else if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
|
|
1423
|
+
config.transfer.requestSize = Buffer.byteLength(body.toString(), "utf8");
|
|
1424
|
+
} else if (body instanceof RezoFormData) {
|
|
1425
|
+
config.transfer.requestSize = await body.getLength() || 0;
|
|
1426
|
+
} else if (typeof body === "object") {
|
|
1427
|
+
config.transfer.requestSize = Buffer.byteLength(JSON.stringify(body), "utf8");
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
(sessionPool || Http2SessionPool.getInstance()).releaseSession(url, fetchOptions.proxy);
|
|
1431
|
+
if (isRedirect) {
|
|
1432
|
+
_stats.statusOnNext = "redirect";
|
|
1433
|
+
const partialResponse = {
|
|
1434
|
+
data: "",
|
|
1435
|
+
status,
|
|
1436
|
+
statusText,
|
|
1437
|
+
headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
|
|
1438
|
+
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
1439
|
+
config,
|
|
945
1440
|
contentType: responseHeaders["content-type"],
|
|
946
|
-
contentLength:
|
|
947
|
-
cookies: config.responseCookies?.setCookiesString || [],
|
|
948
|
-
statusText: err.message,
|
|
949
|
-
url: url.href,
|
|
950
|
-
body: responseBody,
|
|
1441
|
+
contentLength: contentLengthCounter,
|
|
951
1442
|
finalUrl: url.href,
|
|
952
|
-
config,
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
_stats.statusOnNext = "error";
|
|
956
|
-
resolve(error);
|
|
1443
|
+
urls: buildUrlTree(config, url.href)
|
|
1444
|
+
};
|
|
1445
|
+
resolve(partialResponse);
|
|
957
1446
|
return;
|
|
958
1447
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1448
|
+
let responseBody = Buffer.concat(chunks);
|
|
1449
|
+
const contentEncoding = responseHeaders["content-encoding"];
|
|
1450
|
+
if (contentEncoding && contentLengthCounter > 0 && CompressionUtil.shouldDecompress(contentEncoding, config)) {
|
|
1451
|
+
try {
|
|
1452
|
+
const decompressed = await decompressBuffer(responseBody, contentEncoding);
|
|
1453
|
+
responseBody = decompressed;
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
const error = buildDecompressionError({
|
|
1456
|
+
statusCode: status,
|
|
1457
|
+
headers: sanitizeHttp2Headers(responseHeaders),
|
|
1458
|
+
contentType: responseHeaders["content-type"],
|
|
1459
|
+
contentLength: String(contentLengthCounter),
|
|
1460
|
+
cookies: config.responseCookies?.setCookiesString || [],
|
|
1461
|
+
statusText: err.message,
|
|
1462
|
+
url: url.href,
|
|
1463
|
+
body: responseBody,
|
|
1464
|
+
finalUrl: url.href,
|
|
1465
|
+
config,
|
|
1466
|
+
request: fetchOptions
|
|
1467
|
+
});
|
|
1468
|
+
_stats.statusOnNext = "error";
|
|
1469
|
+
resolve(error);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
972
1472
|
}
|
|
973
|
-
|
|
974
|
-
|
|
1473
|
+
let data;
|
|
1474
|
+
const contentType = responseHeaders["content-type"] || "";
|
|
1475
|
+
const responseType = config.responseType || fetchOptions.responseType || "auto";
|
|
1476
|
+
if (responseType === "buffer" || responseType === "arrayBuffer") {
|
|
1477
|
+
data = responseBody;
|
|
1478
|
+
} else if (responseType === "text") {
|
|
1479
|
+
data = responseBody.toString("utf-8");
|
|
1480
|
+
} else if (responseType === "json" || responseType === "auto" && contentType.includes("application/json")) {
|
|
975
1481
|
try {
|
|
976
1482
|
data = JSON.parse(responseBody.toString("utf-8"));
|
|
977
1483
|
} catch {
|
|
978
1484
|
data = responseBody.toString("utf-8");
|
|
979
1485
|
}
|
|
980
1486
|
} else {
|
|
981
|
-
|
|
1487
|
+
if (contentType.includes("application/json")) {
|
|
1488
|
+
try {
|
|
1489
|
+
data = JSON.parse(responseBody.toString("utf-8"));
|
|
1490
|
+
} catch {
|
|
1491
|
+
data = responseBody.toString("utf-8");
|
|
1492
|
+
}
|
|
1493
|
+
} else {
|
|
1494
|
+
data = responseBody.toString("utf-8");
|
|
1495
|
+
}
|
|
982
1496
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1497
|
+
config.status = status;
|
|
1498
|
+
config.statusText = statusText;
|
|
1499
|
+
_stats.statusOnNext = status >= 400 ? "error" : "success";
|
|
1500
|
+
const responseCookies = config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] };
|
|
1501
|
+
const mergedCookies = mergeRequestAndResponseCookies(config, responseCookies, url.href);
|
|
1502
|
+
const finalResponse = {
|
|
1503
|
+
data,
|
|
1504
|
+
status,
|
|
1505
|
+
statusText,
|
|
1506
|
+
headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
|
|
1507
|
+
cookies: mergedCookies,
|
|
1508
|
+
config,
|
|
1509
|
+
contentType,
|
|
1510
|
+
contentLength: contentLengthCounter,
|
|
1511
|
+
finalUrl: url.href,
|
|
1512
|
+
urls: buildUrlTree(config, url.href)
|
|
1513
|
+
};
|
|
1514
|
+
if (downloadResult && fs && config.fileName) {
|
|
1515
|
+
try {
|
|
1516
|
+
fs.writeFileSync(config.fileName, responseBody);
|
|
1517
|
+
const downloadFinishEvent = {
|
|
1518
|
+
status,
|
|
1519
|
+
statusText,
|
|
1520
|
+
headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
|
|
1521
|
+
contentType,
|
|
1522
|
+
contentLength: responseBody.length,
|
|
1523
|
+
finalUrl: url.href,
|
|
1524
|
+
cookies: mergedCookies,
|
|
1525
|
+
urls: buildUrlTree(config, url.href),
|
|
1526
|
+
fileName: config.fileName,
|
|
1527
|
+
fileSize: responseBody.length,
|
|
1528
|
+
timing: {
|
|
1529
|
+
...getTimingDurations(config),
|
|
1530
|
+
download: getTimingDurations(config).download || 0
|
|
1531
|
+
},
|
|
1532
|
+
averageSpeed: getTimingDurations(config).download ? responseBody.length / getTimingDurations(config).download * 1000 : 0,
|
|
1533
|
+
config: sanitizeConfig(config)
|
|
1534
|
+
};
|
|
1535
|
+
downloadResult.emit("finish", downloadFinishEvent);
|
|
1536
|
+
downloadResult.emit("done", downloadFinishEvent);
|
|
1537
|
+
downloadResult._markFinished();
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
const error = buildDownloadError({
|
|
1540
|
+
statusCode: status,
|
|
1541
|
+
headers: sanitizeHttp2Headers(responseHeaders),
|
|
1542
|
+
contentType,
|
|
1543
|
+
contentLength: String(contentLengthCounter),
|
|
1544
|
+
cookies: config.responseCookies?.setCookiesString || [],
|
|
1545
|
+
statusText: err.message,
|
|
1546
|
+
url: url.href,
|
|
1547
|
+
body: responseBody,
|
|
1548
|
+
finalUrl: url.href,
|
|
1549
|
+
config,
|
|
1550
|
+
request: fetchOptions
|
|
1551
|
+
});
|
|
1552
|
+
downloadResult.emit("error", error);
|
|
1553
|
+
resolve(error);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (streamResult) {
|
|
1558
|
+
const streamFinishEvent = {
|
|
1005
1559
|
status,
|
|
1006
1560
|
statusText,
|
|
1007
1561
|
headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
|
|
1008
1562
|
contentType,
|
|
1009
|
-
contentLength:
|
|
1563
|
+
contentLength: contentLengthCounter,
|
|
1564
|
+
finalUrl: url.href,
|
|
1565
|
+
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
1566
|
+
urls: buildUrlTree(config, url.href),
|
|
1567
|
+
timing: getTimingDurations(config),
|
|
1568
|
+
config: sanitizeConfig(config)
|
|
1569
|
+
};
|
|
1570
|
+
streamResult.emit("finish", streamFinishEvent);
|
|
1571
|
+
streamResult.emit("done", streamFinishEvent);
|
|
1572
|
+
streamResult.emit("end");
|
|
1573
|
+
streamResult._markFinished();
|
|
1574
|
+
}
|
|
1575
|
+
if (uploadResult) {
|
|
1576
|
+
const uploadFinishEvent = {
|
|
1577
|
+
response: {
|
|
1578
|
+
status,
|
|
1579
|
+
statusText,
|
|
1580
|
+
headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
|
|
1581
|
+
data,
|
|
1582
|
+
contentType,
|
|
1583
|
+
contentLength: contentLengthCounter
|
|
1584
|
+
},
|
|
1010
1585
|
finalUrl: url.href,
|
|
1011
|
-
cookies:
|
|
1586
|
+
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
1012
1587
|
urls: buildUrlTree(config, url.href),
|
|
1013
|
-
|
|
1014
|
-
fileSize: responseBody.length,
|
|
1588
|
+
uploadSize: config.transfer.requestSize || 0,
|
|
1015
1589
|
timing: {
|
|
1016
1590
|
...getTimingDurations(config),
|
|
1017
|
-
|
|
1591
|
+
upload: getTimingDurations(config).firstByte || 0,
|
|
1592
|
+
waiting: getTimingDurations(config).download > 0 && getTimingDurations(config).firstByte > 0 ? getTimingDurations(config).download - getTimingDurations(config).firstByte : 0
|
|
1018
1593
|
},
|
|
1019
|
-
|
|
1594
|
+
averageUploadSpeed: getTimingDurations(config).firstByte && config.transfer.requestSize ? config.transfer.requestSize / getTimingDurations(config).firstByte * 1000 : 0,
|
|
1595
|
+
averageDownloadSpeed: getTimingDurations(config).download ? contentLengthCounter / getTimingDurations(config).download * 1000 : 0,
|
|
1020
1596
|
config: sanitizeConfig(config)
|
|
1021
1597
|
};
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
} catch (err) {
|
|
1026
|
-
const error = buildDownloadError({
|
|
1027
|
-
statusCode: status,
|
|
1028
|
-
headers: sanitizeHttp2Headers(responseHeaders),
|
|
1029
|
-
contentType,
|
|
1030
|
-
contentLength: String(contentLengthCounter),
|
|
1031
|
-
cookies: config.responseCookies?.setCookiesString || [],
|
|
1032
|
-
statusText: err.message,
|
|
1033
|
-
url: url.href,
|
|
1034
|
-
body: responseBody,
|
|
1035
|
-
finalUrl: url.href,
|
|
1036
|
-
config,
|
|
1037
|
-
request: fetchOptions
|
|
1038
|
-
});
|
|
1039
|
-
downloadResult.emit("error", error);
|
|
1040
|
-
resolve(error);
|
|
1041
|
-
return;
|
|
1598
|
+
uploadResult.emit("finish", uploadFinishEvent);
|
|
1599
|
+
uploadResult.emit("done", uploadFinishEvent);
|
|
1600
|
+
uploadResult._markFinished();
|
|
1042
1601
|
}
|
|
1602
|
+
resolve(finalResponse);
|
|
1603
|
+
} catch (endError) {
|
|
1604
|
+
if (config.debug) {
|
|
1605
|
+
console.log(`[Rezo Debug] HTTP/2: Error in 'end' handler:`, endError.message);
|
|
1606
|
+
}
|
|
1607
|
+
(sessionPool || Http2SessionPool.getInstance()).releaseSession(url, fetchOptions.proxy);
|
|
1608
|
+
const error = buildSmartError(config, fetchOptions, endError);
|
|
1609
|
+
_stats.statusOnNext = "error";
|
|
1610
|
+
resolve(error);
|
|
1043
1611
|
}
|
|
1044
|
-
if (streamResult) {
|
|
1045
|
-
const streamFinishEvent = {
|
|
1046
|
-
status,
|
|
1047
|
-
statusText,
|
|
1048
|
-
headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
|
|
1049
|
-
contentType,
|
|
1050
|
-
contentLength: contentLengthCounter,
|
|
1051
|
-
finalUrl: url.href,
|
|
1052
|
-
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
1053
|
-
urls: buildUrlTree(config, url.href),
|
|
1054
|
-
timing: getTimingDurations(config),
|
|
1055
|
-
config: sanitizeConfig(config)
|
|
1056
|
-
};
|
|
1057
|
-
streamResult.emit("finish", streamFinishEvent);
|
|
1058
|
-
streamResult.emit("done", streamFinishEvent);
|
|
1059
|
-
streamResult.emit("end");
|
|
1060
|
-
streamResult._markFinished();
|
|
1061
|
-
}
|
|
1062
|
-
if (uploadResult) {
|
|
1063
|
-
const uploadFinishEvent = {
|
|
1064
|
-
response: {
|
|
1065
|
-
status,
|
|
1066
|
-
statusText,
|
|
1067
|
-
headers: new RezoHeaders(sanitizeHttp2Headers(responseHeaders)),
|
|
1068
|
-
data,
|
|
1069
|
-
contentType,
|
|
1070
|
-
contentLength: contentLengthCounter
|
|
1071
|
-
},
|
|
1072
|
-
finalUrl: url.href,
|
|
1073
|
-
cookies: config.responseCookies || { array: [], serialized: [], netscape: "", string: "", setCookiesString: [] },
|
|
1074
|
-
urls: buildUrlTree(config, url.href),
|
|
1075
|
-
uploadSize: config.transfer.requestSize || 0,
|
|
1076
|
-
timing: {
|
|
1077
|
-
...getTimingDurations(config),
|
|
1078
|
-
upload: getTimingDurations(config).firstByte || 0,
|
|
1079
|
-
waiting: getTimingDurations(config).download > 0 && getTimingDurations(config).firstByte > 0 ? getTimingDurations(config).download - getTimingDurations(config).firstByte : 0
|
|
1080
|
-
},
|
|
1081
|
-
averageUploadSpeed: getTimingDurations(config).firstByte && config.transfer.requestSize ? config.transfer.requestSize / getTimingDurations(config).firstByte * 1000 : 0,
|
|
1082
|
-
averageDownloadSpeed: getTimingDurations(config).download ? contentLengthCounter / getTimingDurations(config).download * 1000 : 0,
|
|
1083
|
-
config: sanitizeConfig(config)
|
|
1084
|
-
};
|
|
1085
|
-
uploadResult.emit("finish", uploadFinishEvent);
|
|
1086
|
-
uploadResult.emit("done", uploadFinishEvent);
|
|
1087
|
-
uploadResult._markFinished();
|
|
1088
|
-
}
|
|
1089
|
-
resolve(finalResponse);
|
|
1090
|
-
});
|
|
1091
|
-
req.on("error", (err) => {
|
|
1092
|
-
_stats.statusOnNext = "error";
|
|
1093
|
-
(sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
|
|
1094
|
-
const error = buildSmartError(config, fetchOptions, err);
|
|
1095
|
-
if (eventEmitter) {
|
|
1096
|
-
eventEmitter.emit("error", error);
|
|
1097
|
-
}
|
|
1098
|
-
resolve(error);
|
|
1099
1612
|
});
|
|
1100
1613
|
if (body) {
|
|
1614
|
+
if (config.debug) {
|
|
1615
|
+
console.log(`[Rezo Debug] HTTP/2: Writing request body (type: ${body?.constructor?.name || typeof body})...`);
|
|
1616
|
+
}
|
|
1101
1617
|
if (body instanceof URLSearchParams || body instanceof RezoURLSearchParams) {
|
|
1102
1618
|
req.write(body.toString());
|
|
1103
1619
|
} else if (body instanceof FormData || body instanceof RezoFormData) {
|
|
@@ -1107,13 +1623,26 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
|
|
|
1107
1623
|
} else if (typeof body === "object" && !Buffer.isBuffer(body) && !(body instanceof Uint8Array) && !(body instanceof Readable)) {
|
|
1108
1624
|
req.write(JSON.stringify(body));
|
|
1109
1625
|
} else if (body instanceof Readable) {
|
|
1626
|
+
if (config.debug) {
|
|
1627
|
+
console.log(`[Rezo Debug] HTTP/2: Piping stream body...`);
|
|
1628
|
+
}
|
|
1110
1629
|
body.pipe(req);
|
|
1111
1630
|
return;
|
|
1112
1631
|
} else {
|
|
1113
1632
|
req.write(body);
|
|
1114
1633
|
}
|
|
1634
|
+
if (config.debug) {
|
|
1635
|
+
console.log(`[Rezo Debug] HTTP/2: Body written, calling req.end()...`);
|
|
1636
|
+
}
|
|
1637
|
+
} else {
|
|
1638
|
+
if (config.debug) {
|
|
1639
|
+
console.log(`[Rezo Debug] HTTP/2: No body, calling req.end()...`);
|
|
1640
|
+
}
|
|
1115
1641
|
}
|
|
1116
1642
|
req.end();
|
|
1643
|
+
if (config.debug) {
|
|
1644
|
+
console.log(`[Rezo Debug] HTTP/2: req.end() called, waiting for response...`);
|
|
1645
|
+
}
|
|
1117
1646
|
} catch (error) {
|
|
1118
1647
|
_stats.statusOnNext = "error";
|
|
1119
1648
|
const rezoError = buildSmartError(config, fetchOptions, error);
|