httpcloak 1.5.1 → 1.5.3
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/README.md +323 -14
- package/lib/index.d.ts +89 -34
- package/lib/index.js +815 -9
- package/npm/darwin-arm64/package.json +1 -1
- package/npm/darwin-x64/package.json +1 -1
- package/npm/linux-arm64/package.json +1 -1
- package/npm/linux-x64/package.json +1 -1
- package/npm/win32-arm64/package.json +1 -1
- package/npm/win32-x64/package.json +1 -1
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -235,6 +235,364 @@ class Response {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* High-performance buffer pool using SharedArrayBuffer for zero-allocation copies.
|
|
240
|
+
* Pre-allocates a large buffer once and reuses it across requests.
|
|
241
|
+
*/
|
|
242
|
+
class FastBufferPool {
|
|
243
|
+
constructor() {
|
|
244
|
+
// Pre-allocate 256MB SharedArrayBuffer for maximum performance
|
|
245
|
+
this._sharedBuffer = new SharedArrayBuffer(256 * 1024 * 1024);
|
|
246
|
+
this._bufferView = Buffer.from(this._sharedBuffer);
|
|
247
|
+
this._inUse = false;
|
|
248
|
+
|
|
249
|
+
// Fallback pool for concurrent requests or very large files
|
|
250
|
+
this._fallbackPools = new Map();
|
|
251
|
+
this._tiers = [1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 134217728];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get a buffer of at least the requested size
|
|
256
|
+
* @param {number} size - Minimum buffer size needed
|
|
257
|
+
* @returns {Buffer} - A buffer (may be larger than requested)
|
|
258
|
+
*/
|
|
259
|
+
acquire(size) {
|
|
260
|
+
// Use pre-allocated SharedArrayBuffer if available and large enough
|
|
261
|
+
if (!this._inUse && size <= this._sharedBuffer.byteLength) {
|
|
262
|
+
this._inUse = true;
|
|
263
|
+
return this._bufferView;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Fallback to regular buffer pool for concurrent requests
|
|
267
|
+
let tier = this._tiers[this._tiers.length - 1];
|
|
268
|
+
for (const t of this._tiers) {
|
|
269
|
+
if (t >= size) {
|
|
270
|
+
tier = t;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const pool = this._fallbackPools.get(tier);
|
|
276
|
+
if (pool && pool.length > 0) {
|
|
277
|
+
return pool.pop();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return Buffer.allocUnsafe(Math.max(tier, size));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Return a buffer to the pool for reuse
|
|
285
|
+
* @param {Buffer} buffer - Buffer to return
|
|
286
|
+
*/
|
|
287
|
+
release(buffer) {
|
|
288
|
+
// Check if this is our shared buffer
|
|
289
|
+
if (buffer.buffer === this._sharedBuffer) {
|
|
290
|
+
this._inUse = false;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Otherwise add to fallback pool
|
|
295
|
+
const size = buffer.length;
|
|
296
|
+
if (!this._tiers.includes(size)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let pool = this._fallbackPools.get(size);
|
|
301
|
+
if (!pool) {
|
|
302
|
+
pool = [];
|
|
303
|
+
this._fallbackPools.set(size, pool);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (pool.length < 2) {
|
|
307
|
+
pool.push(buffer);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Global buffer pool instance
|
|
313
|
+
const _bufferPool = new FastBufferPool();
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Fast response object with zero-copy buffer transfer.
|
|
317
|
+
*
|
|
318
|
+
* This response type avoids JSON serialization and base64 encoding for the body,
|
|
319
|
+
* copying data directly from Go's memory to a Node.js Buffer.
|
|
320
|
+
*
|
|
321
|
+
* Use session.getFast() for maximum download performance.
|
|
322
|
+
*/
|
|
323
|
+
class FastResponse {
|
|
324
|
+
/**
|
|
325
|
+
* @param {Object} metadata - Response metadata from native library
|
|
326
|
+
* @param {Buffer} body - Response body as Buffer (view of pooled buffer)
|
|
327
|
+
* @param {number} [elapsed=0] - Elapsed time in milliseconds
|
|
328
|
+
* @param {Buffer} [pooledBuffer=null] - The underlying pooled buffer for release
|
|
329
|
+
*/
|
|
330
|
+
constructor(metadata, body, elapsed = 0, pooledBuffer = null) {
|
|
331
|
+
this.statusCode = metadata.status_code || 0;
|
|
332
|
+
this.headers = metadata.headers || {};
|
|
333
|
+
this._body = body;
|
|
334
|
+
this._pooledBuffer = pooledBuffer;
|
|
335
|
+
this.finalUrl = metadata.final_url || "";
|
|
336
|
+
this.protocol = metadata.protocol || "";
|
|
337
|
+
this.elapsed = elapsed;
|
|
338
|
+
|
|
339
|
+
// Parse cookies from response
|
|
340
|
+
this._cookies = (metadata.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
|
|
341
|
+
|
|
342
|
+
// Parse redirect history
|
|
343
|
+
this._history = (metadata.history || []).map(h => new RedirectInfo(
|
|
344
|
+
h.status_code || 0,
|
|
345
|
+
h.url || "",
|
|
346
|
+
h.headers || {}
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Release the underlying buffer back to the pool.
|
|
352
|
+
* Call this when done with the response to enable buffer reuse.
|
|
353
|
+
* After calling release(), the body buffer should not be used.
|
|
354
|
+
*/
|
|
355
|
+
release() {
|
|
356
|
+
if (this._pooledBuffer) {
|
|
357
|
+
_bufferPool.release(this._pooledBuffer);
|
|
358
|
+
this._pooledBuffer = null;
|
|
359
|
+
this._body = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Cookies set by this response */
|
|
364
|
+
get cookies() {
|
|
365
|
+
return this._cookies;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Redirect history (list of RedirectInfo objects) */
|
|
369
|
+
get history() {
|
|
370
|
+
return this._history;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Response body as string */
|
|
374
|
+
get text() {
|
|
375
|
+
return this._body.toString("utf8");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Response body as Buffer */
|
|
379
|
+
get body() {
|
|
380
|
+
return this._body;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Response body as Buffer (requests compatibility alias) */
|
|
384
|
+
get content() {
|
|
385
|
+
return this._body;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Final URL after redirects (requests compatibility alias) */
|
|
389
|
+
get url() {
|
|
390
|
+
return this.finalUrl;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** True if status code < 400 (requests compatibility) */
|
|
394
|
+
get ok() {
|
|
395
|
+
return this.statusCode < 400;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** HTTP status reason phrase (e.g., 'OK', 'Not Found') */
|
|
399
|
+
get reason() {
|
|
400
|
+
return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Response encoding from Content-Type header.
|
|
405
|
+
* Returns null if not specified.
|
|
406
|
+
*/
|
|
407
|
+
get encoding() {
|
|
408
|
+
let contentType = this.headers["content-type"] || this.headers["Content-Type"] || "";
|
|
409
|
+
if (contentType.includes("charset=")) {
|
|
410
|
+
const parts = contentType.split(";");
|
|
411
|
+
for (const part of parts) {
|
|
412
|
+
const trimmed = part.trim();
|
|
413
|
+
if (trimmed.toLowerCase().startsWith("charset=")) {
|
|
414
|
+
return trimmed.split("=")[1].trim().replace(/['"]/g, "");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Parse response body as JSON
|
|
423
|
+
*/
|
|
424
|
+
json() {
|
|
425
|
+
return JSON.parse(this._body.toString("utf8"));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Raise error if status >= 400 (requests compatibility)
|
|
430
|
+
*/
|
|
431
|
+
raiseForStatus() {
|
|
432
|
+
if (!this.ok) {
|
|
433
|
+
throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Streaming HTTP Response for downloading large files.
|
|
440
|
+
*
|
|
441
|
+
* Example:
|
|
442
|
+
* const stream = session.getStream(url);
|
|
443
|
+
* for await (const chunk of stream) {
|
|
444
|
+
* file.write(chunk);
|
|
445
|
+
* }
|
|
446
|
+
* stream.close();
|
|
447
|
+
*/
|
|
448
|
+
class StreamResponse {
|
|
449
|
+
/**
|
|
450
|
+
* @param {number} streamHandle - Native stream handle
|
|
451
|
+
* @param {Object} lib - Native library
|
|
452
|
+
* @param {Object} metadata - Stream metadata
|
|
453
|
+
*/
|
|
454
|
+
constructor(streamHandle, lib, metadata) {
|
|
455
|
+
this._handle = streamHandle;
|
|
456
|
+
this._lib = lib;
|
|
457
|
+
this.statusCode = metadata.status_code || 0;
|
|
458
|
+
this.headers = metadata.headers || {};
|
|
459
|
+
this.finalUrl = metadata.final_url || "";
|
|
460
|
+
this.protocol = metadata.protocol || "";
|
|
461
|
+
this.contentLength = metadata.content_length || -1;
|
|
462
|
+
this._cookies = (metadata.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
|
|
463
|
+
this._closed = false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Cookies set by this response */
|
|
467
|
+
get cookies() {
|
|
468
|
+
return this._cookies;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Final URL after redirects */
|
|
472
|
+
get url() {
|
|
473
|
+
return this.finalUrl;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** True if status code < 400 */
|
|
477
|
+
get ok() {
|
|
478
|
+
return this.statusCode < 400;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** HTTP status reason phrase */
|
|
482
|
+
get reason() {
|
|
483
|
+
return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Read a chunk of data from the stream.
|
|
488
|
+
* @param {number} [chunkSize=8192] - Maximum bytes to read
|
|
489
|
+
* @returns {Buffer|null} - Chunk of data or null if EOF
|
|
490
|
+
*/
|
|
491
|
+
readChunk(chunkSize = 8192) {
|
|
492
|
+
if (this._closed) {
|
|
493
|
+
throw new HTTPCloakError("Stream is closed");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const result = this._lib.httpcloak_stream_read(this._handle, chunkSize);
|
|
497
|
+
if (!result || result === "") {
|
|
498
|
+
return null; // EOF
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Decode base64 to Buffer
|
|
502
|
+
return Buffer.from(result, "base64");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Async generator for iterating over chunks.
|
|
507
|
+
* @param {number} [chunkSize=8192] - Size of each chunk
|
|
508
|
+
* @yields {Buffer} - Chunks of response content
|
|
509
|
+
*
|
|
510
|
+
* Example:
|
|
511
|
+
* for await (const chunk of stream.iterate()) {
|
|
512
|
+
* file.write(chunk);
|
|
513
|
+
* }
|
|
514
|
+
*/
|
|
515
|
+
async *iterate(chunkSize = 8192) {
|
|
516
|
+
while (true) {
|
|
517
|
+
const chunk = this.readChunk(chunkSize);
|
|
518
|
+
if (!chunk) {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
yield chunk;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Symbol.asyncIterator for for-await-of loops.
|
|
527
|
+
* @yields {Buffer} - Chunks of response content
|
|
528
|
+
*
|
|
529
|
+
* Example:
|
|
530
|
+
* for await (const chunk of stream) {
|
|
531
|
+
* file.write(chunk);
|
|
532
|
+
* }
|
|
533
|
+
*/
|
|
534
|
+
[Symbol.asyncIterator]() {
|
|
535
|
+
return this.iterate();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Read the entire response body as Buffer.
|
|
540
|
+
* Warning: This defeats the purpose of streaming for large files.
|
|
541
|
+
* @returns {Buffer}
|
|
542
|
+
*/
|
|
543
|
+
readAll() {
|
|
544
|
+
const chunks = [];
|
|
545
|
+
let chunk;
|
|
546
|
+
while ((chunk = this.readChunk()) !== null) {
|
|
547
|
+
chunks.push(chunk);
|
|
548
|
+
}
|
|
549
|
+
return Buffer.concat(chunks);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Read the entire response body as string.
|
|
554
|
+
* @returns {string}
|
|
555
|
+
*/
|
|
556
|
+
get text() {
|
|
557
|
+
return this.readAll().toString("utf8");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Read the entire response body as Buffer.
|
|
562
|
+
* @returns {Buffer}
|
|
563
|
+
*/
|
|
564
|
+
get body() {
|
|
565
|
+
return this.readAll();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Parse the response body as JSON.
|
|
570
|
+
* @returns {any}
|
|
571
|
+
*/
|
|
572
|
+
json() {
|
|
573
|
+
return JSON.parse(this.text);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Close the stream and release resources.
|
|
578
|
+
*/
|
|
579
|
+
close() {
|
|
580
|
+
if (!this._closed) {
|
|
581
|
+
this._lib.httpcloak_stream_close(this._handle);
|
|
582
|
+
this._closed = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Raise error if status >= 400
|
|
588
|
+
*/
|
|
589
|
+
raiseForStatus() {
|
|
590
|
+
if (!this.ok) {
|
|
591
|
+
throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
238
596
|
/**
|
|
239
597
|
* Get the platform package name for the current platform
|
|
240
598
|
*/
|
|
@@ -357,6 +715,22 @@ function getLib() {
|
|
|
357
715
|
httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
|
|
358
716
|
httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
|
|
359
717
|
httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
|
|
718
|
+
// Streaming functions
|
|
719
|
+
httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
|
|
720
|
+
httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
|
|
721
|
+
httpcloak_stream_request: nativeLibHandle.func("httpcloak_stream_request", "int64", ["int64", "str"]),
|
|
722
|
+
httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", "str", ["int64"]),
|
|
723
|
+
httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", "str", ["int64", "int64"]),
|
|
724
|
+
httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
|
|
725
|
+
// Raw response functions for fast-path (zero-copy)
|
|
726
|
+
httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
|
|
727
|
+
httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
|
|
728
|
+
httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata", "str", ["int64"]),
|
|
729
|
+
httpcloak_response_get_body_len: nativeLibHandle.func("httpcloak_response_get_body_len", "int", ["int64"]),
|
|
730
|
+
httpcloak_response_copy_body_to: nativeLibHandle.func("httpcloak_response_copy_body_to", "int", ["int64", "void*", "int"]),
|
|
731
|
+
httpcloak_response_free: nativeLibHandle.func("httpcloak_response_free", "void", ["int64"]),
|
|
732
|
+
// Combined finalize function (copy + metadata + free in one call)
|
|
733
|
+
httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize", "str", ["int64", "void*", "int"]),
|
|
360
734
|
};
|
|
361
735
|
}
|
|
362
736
|
return lib;
|
|
@@ -674,7 +1048,9 @@ class Session {
|
|
|
674
1048
|
* Create a new session
|
|
675
1049
|
* @param {Object} options - Session options
|
|
676
1050
|
* @param {string} [options.preset="chrome-143"] - Browser preset to use
|
|
677
|
-
* @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port")
|
|
1051
|
+
* @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
|
|
1052
|
+
* @param {string} [options.tcpProxy] - Proxy URL for TCP protocols (HTTP/1.1, HTTP/2) - use with udpProxy for split config
|
|
1053
|
+
* @param {string} [options.udpProxy] - Proxy URL for UDP protocols (HTTP/3 via MASQUE) - use with tcpProxy for split config
|
|
678
1054
|
* @param {number} [options.timeout=30] - Request timeout in seconds
|
|
679
1055
|
* @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
|
|
680
1056
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
@@ -683,11 +1059,15 @@ class Session {
|
|
|
683
1059
|
* @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
|
|
684
1060
|
* @param {number[]} [options.retryOnStatus] - Status codes to retry on
|
|
685
1061
|
* @param {Array} [options.auth] - Default auth [username, password] for all requests
|
|
1062
|
+
* @param {Object} [options.connectTo] - Domain fronting map {requestHost: connectHost}
|
|
1063
|
+
* @param {string} [options.echConfigDomain] - Domain to fetch ECH config from (e.g., "cloudflare-ech.com")
|
|
686
1064
|
*/
|
|
687
1065
|
constructor(options = {}) {
|
|
688
1066
|
const {
|
|
689
1067
|
preset = "chrome-143",
|
|
690
1068
|
proxy = null,
|
|
1069
|
+
tcpProxy = null,
|
|
1070
|
+
udpProxy = null,
|
|
691
1071
|
timeout = 30,
|
|
692
1072
|
httpVersion = "auto",
|
|
693
1073
|
verify = true,
|
|
@@ -697,6 +1077,8 @@ class Session {
|
|
|
697
1077
|
retryOnStatus = null,
|
|
698
1078
|
preferIpv4 = false,
|
|
699
1079
|
auth = null,
|
|
1080
|
+
connectTo = null,
|
|
1081
|
+
echConfigDomain = null,
|
|
700
1082
|
} = options;
|
|
701
1083
|
|
|
702
1084
|
this._lib = getLib();
|
|
@@ -711,6 +1093,12 @@ class Session {
|
|
|
711
1093
|
if (proxy) {
|
|
712
1094
|
config.proxy = proxy;
|
|
713
1095
|
}
|
|
1096
|
+
if (tcpProxy) {
|
|
1097
|
+
config.tcp_proxy = tcpProxy;
|
|
1098
|
+
}
|
|
1099
|
+
if (udpProxy) {
|
|
1100
|
+
config.udp_proxy = udpProxy;
|
|
1101
|
+
}
|
|
714
1102
|
if (!verify) {
|
|
715
1103
|
config.verify = false;
|
|
716
1104
|
}
|
|
@@ -727,6 +1115,12 @@ class Session {
|
|
|
727
1115
|
if (preferIpv4) {
|
|
728
1116
|
config.prefer_ipv4 = true;
|
|
729
1117
|
}
|
|
1118
|
+
if (connectTo) {
|
|
1119
|
+
config.connect_to = connectTo;
|
|
1120
|
+
}
|
|
1121
|
+
if (echConfigDomain) {
|
|
1122
|
+
config.ech_config_domain = echConfigDomain;
|
|
1123
|
+
}
|
|
730
1124
|
|
|
731
1125
|
this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
|
|
732
1126
|
|
|
@@ -804,9 +1198,15 @@ class Session {
|
|
|
804
1198
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
805
1199
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
806
1200
|
|
|
807
|
-
|
|
1201
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1202
|
+
const reqOptions = {};
|
|
1203
|
+
if (mergedHeaders) {
|
|
1204
|
+
reqOptions.headers = mergedHeaders;
|
|
1205
|
+
}
|
|
1206
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1207
|
+
|
|
808
1208
|
const startTime = Date.now();
|
|
809
|
-
const result = this._lib.httpcloak_get(this._handle, url,
|
|
1209
|
+
const result = this._lib.httpcloak_get(this._handle, url, optionsJson);
|
|
810
1210
|
const elapsed = Date.now() - startTime;
|
|
811
1211
|
return parseResponse(result, elapsed);
|
|
812
1212
|
}
|
|
@@ -868,9 +1268,15 @@ class Session {
|
|
|
868
1268
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
869
1269
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
870
1270
|
|
|
871
|
-
|
|
1271
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1272
|
+
const reqOptions = {};
|
|
1273
|
+
if (mergedHeaders) {
|
|
1274
|
+
reqOptions.headers = mergedHeaders;
|
|
1275
|
+
}
|
|
1276
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1277
|
+
|
|
872
1278
|
const startTime = Date.now();
|
|
873
|
-
const result = this._lib.httpcloak_post(this._handle, url, body,
|
|
1279
|
+
const result = this._lib.httpcloak_post(this._handle, url, body, optionsJson);
|
|
874
1280
|
const elapsed = Date.now() - startTime;
|
|
875
1281
|
return parseResponse(result, elapsed);
|
|
876
1282
|
}
|
|
@@ -962,14 +1368,19 @@ class Session {
|
|
|
962
1368
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
963
1369
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
964
1370
|
|
|
965
|
-
|
|
1371
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1372
|
+
const reqOptions = {};
|
|
1373
|
+
if (mergedHeaders) {
|
|
1374
|
+
reqOptions.headers = mergedHeaders;
|
|
1375
|
+
}
|
|
1376
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
966
1377
|
|
|
967
1378
|
// Register async request with callback manager
|
|
968
1379
|
const manager = getAsyncManager();
|
|
969
1380
|
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
970
1381
|
|
|
971
1382
|
// Start async request
|
|
972
|
-
this._lib.httpcloak_get_async(this._handle, url,
|
|
1383
|
+
this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
|
|
973
1384
|
|
|
974
1385
|
return promise;
|
|
975
1386
|
}
|
|
@@ -1021,14 +1432,19 @@ class Session {
|
|
|
1021
1432
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1022
1433
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1023
1434
|
|
|
1024
|
-
|
|
1435
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1436
|
+
const reqOptions = {};
|
|
1437
|
+
if (mergedHeaders) {
|
|
1438
|
+
reqOptions.headers = mergedHeaders;
|
|
1439
|
+
}
|
|
1440
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1025
1441
|
|
|
1026
1442
|
// Register async request with callback manager
|
|
1027
1443
|
const manager = getAsyncManager();
|
|
1028
1444
|
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
1029
1445
|
|
|
1030
1446
|
// Start async request
|
|
1031
|
-
this._lib.httpcloak_post_async(this._handle, url, body,
|
|
1447
|
+
this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
|
|
1032
1448
|
|
|
1033
1449
|
return promise;
|
|
1034
1450
|
}
|
|
@@ -1195,6 +1611,394 @@ class Session {
|
|
|
1195
1611
|
get cookies() {
|
|
1196
1612
|
return this.getCookies();
|
|
1197
1613
|
}
|
|
1614
|
+
|
|
1615
|
+
// ===========================================================================
|
|
1616
|
+
// Streaming Methods
|
|
1617
|
+
// ===========================================================================
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Perform a streaming GET request.
|
|
1621
|
+
*
|
|
1622
|
+
* @param {string} url - Request URL
|
|
1623
|
+
* @param {Object} [options] - Request options
|
|
1624
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1625
|
+
* @param {Object} [options.headers] - Request headers
|
|
1626
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1627
|
+
* @param {number} [options.timeout] - Request timeout in milliseconds
|
|
1628
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1629
|
+
*
|
|
1630
|
+
* Example:
|
|
1631
|
+
* const stream = session.getStream("https://example.com/large-file.zip");
|
|
1632
|
+
* for await (const chunk of stream) {
|
|
1633
|
+
* file.write(chunk);
|
|
1634
|
+
* }
|
|
1635
|
+
* stream.close();
|
|
1636
|
+
*/
|
|
1637
|
+
getStream(url, options = {}) {
|
|
1638
|
+
const { params, headers, cookies, timeout } = options;
|
|
1639
|
+
|
|
1640
|
+
// Add params to URL
|
|
1641
|
+
if (params) {
|
|
1642
|
+
url = addParamsToUrl(url, params);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Merge headers
|
|
1646
|
+
let mergedHeaders = { ...this.headers };
|
|
1647
|
+
if (headers) {
|
|
1648
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1649
|
+
}
|
|
1650
|
+
if (cookies) {
|
|
1651
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1652
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1653
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1654
|
+
: cookieStr;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Build options JSON
|
|
1658
|
+
const reqOptions = {};
|
|
1659
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1660
|
+
reqOptions.headers = mergedHeaders;
|
|
1661
|
+
}
|
|
1662
|
+
if (timeout) {
|
|
1663
|
+
reqOptions.timeout = timeout;
|
|
1664
|
+
}
|
|
1665
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1666
|
+
|
|
1667
|
+
// Start stream
|
|
1668
|
+
const streamHandle = this._lib.httpcloak_stream_get(this._handle, url, optionsJson);
|
|
1669
|
+
if (streamHandle < 0) {
|
|
1670
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Get metadata
|
|
1674
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1675
|
+
if (!metadataStr) {
|
|
1676
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1677
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const metadata = JSON.parse(metadataStr);
|
|
1681
|
+
if (metadata.error) {
|
|
1682
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1683
|
+
throw new HTTPCloakError(metadata.error);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Perform a streaming POST request.
|
|
1691
|
+
*
|
|
1692
|
+
* @param {string} url - Request URL
|
|
1693
|
+
* @param {Object} [options] - Request options
|
|
1694
|
+
* @param {string|Buffer|Object} [options.body] - Request body
|
|
1695
|
+
* @param {Object} [options.json] - JSON body (will be serialized)
|
|
1696
|
+
* @param {Object} [options.form] - Form data (will be URL-encoded)
|
|
1697
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1698
|
+
* @param {Object} [options.headers] - Request headers
|
|
1699
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1700
|
+
* @param {number} [options.timeout] - Request timeout in milliseconds
|
|
1701
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1702
|
+
*/
|
|
1703
|
+
postStream(url, options = {}) {
|
|
1704
|
+
const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
|
|
1705
|
+
|
|
1706
|
+
// Add params to URL
|
|
1707
|
+
if (params) {
|
|
1708
|
+
url = addParamsToUrl(url, params);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Merge headers
|
|
1712
|
+
let mergedHeaders = { ...this.headers };
|
|
1713
|
+
if (headers) {
|
|
1714
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Process body
|
|
1718
|
+
let body = null;
|
|
1719
|
+
if (jsonBody) {
|
|
1720
|
+
body = JSON.stringify(jsonBody);
|
|
1721
|
+
mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/json";
|
|
1722
|
+
} else if (form) {
|
|
1723
|
+
body = Object.entries(form).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
1724
|
+
mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/x-www-form-urlencoded";
|
|
1725
|
+
} else if (bodyOpt) {
|
|
1726
|
+
body = typeof bodyOpt === "string" ? bodyOpt : bodyOpt.toString();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (cookies) {
|
|
1730
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1731
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1732
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1733
|
+
: cookieStr;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Build options JSON
|
|
1737
|
+
const reqOptions = {};
|
|
1738
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1739
|
+
reqOptions.headers = mergedHeaders;
|
|
1740
|
+
}
|
|
1741
|
+
if (timeout) {
|
|
1742
|
+
reqOptions.timeout = timeout;
|
|
1743
|
+
}
|
|
1744
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1745
|
+
|
|
1746
|
+
// Start stream
|
|
1747
|
+
const streamHandle = this._lib.httpcloak_stream_post(this._handle, url, body, optionsJson);
|
|
1748
|
+
if (streamHandle < 0) {
|
|
1749
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Get metadata
|
|
1753
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1754
|
+
if (!metadataStr) {
|
|
1755
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1756
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const metadata = JSON.parse(metadataStr);
|
|
1760
|
+
if (metadata.error) {
|
|
1761
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1762
|
+
throw new HTTPCloakError(metadata.error);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Perform a streaming request with any HTTP method.
|
|
1770
|
+
*
|
|
1771
|
+
* @param {string} method - HTTP method
|
|
1772
|
+
* @param {string} url - Request URL
|
|
1773
|
+
* @param {Object} [options] - Request options
|
|
1774
|
+
* @param {string|Buffer} [options.body] - Request body
|
|
1775
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1776
|
+
* @param {Object} [options.headers] - Request headers
|
|
1777
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1778
|
+
* @param {number} [options.timeout] - Request timeout in seconds
|
|
1779
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1780
|
+
*/
|
|
1781
|
+
requestStream(method, url, options = {}) {
|
|
1782
|
+
const { body, params, headers, cookies, timeout } = options;
|
|
1783
|
+
|
|
1784
|
+
// Add params to URL
|
|
1785
|
+
if (params) {
|
|
1786
|
+
url = addParamsToUrl(url, params);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Merge headers
|
|
1790
|
+
let mergedHeaders = { ...this.headers };
|
|
1791
|
+
if (headers) {
|
|
1792
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1793
|
+
}
|
|
1794
|
+
if (cookies) {
|
|
1795
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1796
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1797
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1798
|
+
: cookieStr;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Build request config
|
|
1802
|
+
const requestConfig = {
|
|
1803
|
+
method: method.toUpperCase(),
|
|
1804
|
+
url,
|
|
1805
|
+
};
|
|
1806
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1807
|
+
requestConfig.headers = mergedHeaders;
|
|
1808
|
+
}
|
|
1809
|
+
if (body) {
|
|
1810
|
+
requestConfig.body = typeof body === "string" ? body : body.toString();
|
|
1811
|
+
}
|
|
1812
|
+
if (timeout) {
|
|
1813
|
+
requestConfig.timeout = timeout;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Start stream
|
|
1817
|
+
const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
|
|
1818
|
+
if (streamHandle < 0) {
|
|
1819
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Get metadata
|
|
1823
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1824
|
+
if (!metadataStr) {
|
|
1825
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1826
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const metadata = JSON.parse(metadataStr);
|
|
1830
|
+
if (metadata.error) {
|
|
1831
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1832
|
+
throw new HTTPCloakError(metadata.error);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// ===========================================================================
|
|
1839
|
+
// Fast-path Methods (Zero-copy for maximum performance)
|
|
1840
|
+
// ===========================================================================
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Perform a fast GET request with zero-copy buffer transfer.
|
|
1844
|
+
*
|
|
1845
|
+
* This method bypasses JSON serialization and base64 encoding for the response body,
|
|
1846
|
+
* copying data directly from Go's memory to a Node.js Buffer.
|
|
1847
|
+
*
|
|
1848
|
+
* Use this method for downloading large files when you need maximum throughput.
|
|
1849
|
+
*
|
|
1850
|
+
* @param {string} url - Request URL
|
|
1851
|
+
* @param {Object} [options] - Request options
|
|
1852
|
+
* @param {Object} [options.headers] - Custom headers
|
|
1853
|
+
* @param {Object} [options.params] - Query parameters
|
|
1854
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
1855
|
+
* @param {Array} [options.auth] - Basic auth [username, password]
|
|
1856
|
+
* @returns {FastResponse} Fast response object with Buffer body
|
|
1857
|
+
*
|
|
1858
|
+
* Example:
|
|
1859
|
+
* const response = session.getFast("https://example.com/large-file.zip");
|
|
1860
|
+
* console.log(`Downloaded ${response.body.length} bytes`);
|
|
1861
|
+
* fs.writeFileSync("file.zip", response.body);
|
|
1862
|
+
*/
|
|
1863
|
+
getFast(url, options = {}) {
|
|
1864
|
+
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
1865
|
+
|
|
1866
|
+
url = addParamsToUrl(url, params);
|
|
1867
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
1868
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
1869
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
1870
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1871
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1872
|
+
|
|
1873
|
+
// Build request options JSON with headers wrapper
|
|
1874
|
+
const reqOptions = {};
|
|
1875
|
+
if (mergedHeaders) {
|
|
1876
|
+
reqOptions.headers = mergedHeaders;
|
|
1877
|
+
}
|
|
1878
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1879
|
+
|
|
1880
|
+
const startTime = Date.now();
|
|
1881
|
+
|
|
1882
|
+
// Get raw response handle
|
|
1883
|
+
const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
|
|
1884
|
+
if (responseHandle === 0 || responseHandle === 0n) {
|
|
1885
|
+
throw new HTTPCloakError("Failed to make request");
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Get body length first (1 FFI call)
|
|
1889
|
+
const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
|
|
1890
|
+
if (bodyLen < 0) {
|
|
1891
|
+
this._lib.httpcloak_response_free(responseHandle);
|
|
1892
|
+
throw new HTTPCloakError("Failed to get response body length");
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Acquire pooled buffer
|
|
1896
|
+
const pooledBuffer = _bufferPool.acquire(bodyLen);
|
|
1897
|
+
|
|
1898
|
+
// Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
|
|
1899
|
+
const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
|
|
1900
|
+
if (!metadataStr) {
|
|
1901
|
+
_bufferPool.release(pooledBuffer);
|
|
1902
|
+
throw new HTTPCloakError("Failed to finalize response");
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
const metadata = JSON.parse(metadataStr);
|
|
1906
|
+
if (metadata.error) {
|
|
1907
|
+
_bufferPool.release(pooledBuffer);
|
|
1908
|
+
throw new HTTPCloakError(metadata.error);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// Create a view of just the used portion
|
|
1912
|
+
const buffer = pooledBuffer.subarray(0, bodyLen);
|
|
1913
|
+
|
|
1914
|
+
const elapsed = Date.now() - startTime;
|
|
1915
|
+
return new FastResponse(metadata, buffer, elapsed, pooledBuffer);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/**
|
|
1919
|
+
* High-performance POST request optimized for large uploads.
|
|
1920
|
+
*
|
|
1921
|
+
* Uses binary buffer passing and response pooling for maximum throughput.
|
|
1922
|
+
* Call response.release() when done to return buffers to pool.
|
|
1923
|
+
*
|
|
1924
|
+
* @param {string} url - Request URL
|
|
1925
|
+
* @param {Object} [options] - Request options
|
|
1926
|
+
* @param {Buffer} [options.body] - Request body as Buffer
|
|
1927
|
+
* @param {Object} [options.headers] - Request headers
|
|
1928
|
+
* @param {Object} [options.params] - Query parameters
|
|
1929
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
1930
|
+
* @param {Array} [options.auth] - Basic auth [username, password]
|
|
1931
|
+
* @returns {FastResponse} Fast response object with Buffer body
|
|
1932
|
+
*
|
|
1933
|
+
* Example:
|
|
1934
|
+
* const data = Buffer.alloc(10 * 1024 * 1024); // 10MB
|
|
1935
|
+
* const response = session.postFast("https://example.com/upload", { body: data });
|
|
1936
|
+
* console.log(`Uploaded, response: ${response.statusCode}`);
|
|
1937
|
+
* response.release();
|
|
1938
|
+
*/
|
|
1939
|
+
postFast(url, options = {}) {
|
|
1940
|
+
let { body = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
1941
|
+
|
|
1942
|
+
url = addParamsToUrl(url, params);
|
|
1943
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
1944
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
1945
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
1946
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1947
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1948
|
+
|
|
1949
|
+
// Ensure body is a Buffer
|
|
1950
|
+
if (body === null) {
|
|
1951
|
+
body = Buffer.alloc(0);
|
|
1952
|
+
} else if (typeof body === "string") {
|
|
1953
|
+
body = Buffer.from(body, "utf8");
|
|
1954
|
+
} else if (!Buffer.isBuffer(body)) {
|
|
1955
|
+
throw new HTTPCloakError("postFast body must be a Buffer or string");
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Build request options JSON with headers wrapper
|
|
1959
|
+
const reqOptions = {};
|
|
1960
|
+
if (mergedHeaders) {
|
|
1961
|
+
reqOptions.headers = mergedHeaders;
|
|
1962
|
+
}
|
|
1963
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1964
|
+
|
|
1965
|
+
const startTime = Date.now();
|
|
1966
|
+
|
|
1967
|
+
// Use httpcloak_post_raw with binary buffer (no string conversion!)
|
|
1968
|
+
const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, body, body.length, optionsJson);
|
|
1969
|
+
if (responseHandle === 0 || responseHandle === 0n || responseHandle < 0) {
|
|
1970
|
+
throw new HTTPCloakError("Failed to make POST request");
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// Get body length first (1 FFI call)
|
|
1974
|
+
const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
|
|
1975
|
+
if (bodyLen < 0) {
|
|
1976
|
+
this._lib.httpcloak_response_free(responseHandle);
|
|
1977
|
+
throw new HTTPCloakError("Failed to get response body length");
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// Acquire pooled buffer for response
|
|
1981
|
+
const pooledBuffer = _bufferPool.acquire(bodyLen);
|
|
1982
|
+
|
|
1983
|
+
// Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
|
|
1984
|
+
const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
|
|
1985
|
+
if (!metadataStr) {
|
|
1986
|
+
_bufferPool.release(pooledBuffer);
|
|
1987
|
+
throw new HTTPCloakError("Failed to finalize response");
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
const metadata = JSON.parse(metadataStr);
|
|
1991
|
+
if (metadata.error) {
|
|
1992
|
+
_bufferPool.release(pooledBuffer);
|
|
1993
|
+
throw new HTTPCloakError(metadata.error);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// Create a view of just the used portion
|
|
1997
|
+
const responseBuffer = pooledBuffer.subarray(0, bodyLen);
|
|
1998
|
+
|
|
1999
|
+
const elapsed = Date.now() - startTime;
|
|
2000
|
+
return new FastResponse(metadata, responseBuffer, elapsed, pooledBuffer);
|
|
2001
|
+
}
|
|
1198
2002
|
}
|
|
1199
2003
|
|
|
1200
2004
|
// =============================================================================
|
|
@@ -1374,6 +2178,8 @@ module.exports = {
|
|
|
1374
2178
|
// Classes
|
|
1375
2179
|
Session,
|
|
1376
2180
|
Response,
|
|
2181
|
+
FastResponse,
|
|
2182
|
+
StreamResponse,
|
|
1377
2183
|
Cookie,
|
|
1378
2184
|
RedirectInfo,
|
|
1379
2185
|
HTTPCloakError,
|