react-native-nitro-net 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +70 -12
  2. package/android/libs/arm64-v8a/librust_c_net.so +0 -0
  3. package/android/libs/armeabi-v7a/librust_c_net.so +0 -0
  4. package/android/libs/x86/librust_c_net.so +0 -0
  5. package/android/libs/x86_64/librust_c_net.so +0 -0
  6. package/cpp/HybridHttpParser.hpp +67 -0
  7. package/cpp/HybridNetDriver.hpp +6 -0
  8. package/cpp/HybridNetServerDriver.hpp +7 -0
  9. package/cpp/HybridNetSocketDriver.hpp +27 -0
  10. package/cpp/NetBindings.hpp +15 -0
  11. package/ios/Frameworks/RustCNet.xcframework/Info.plist +5 -5
  12. package/ios/Frameworks/RustCNet.xcframework/ios-arm64/RustCNet.framework/RustCNet +0 -0
  13. package/ios/Frameworks/RustCNet.xcframework/ios-arm64_x86_64-simulator/RustCNet.framework/RustCNet +0 -0
  14. package/lib/Driver.d.ts +1 -0
  15. package/lib/Driver.d.ts.map +1 -0
  16. package/lib/Driver.js +2 -5
  17. package/lib/Net.nitro.d.ts +20 -0
  18. package/lib/Net.nitro.d.ts.map +1 -0
  19. package/lib/Net.nitro.js +4 -7
  20. package/lib/http.d.ts +204 -0
  21. package/lib/http.d.ts.map +1 -0
  22. package/lib/http.js +1126 -0
  23. package/lib/https.d.ts +25 -0
  24. package/lib/https.d.ts.map +1 -0
  25. package/lib/https.js +102 -0
  26. package/lib/index.d.ts +41 -160
  27. package/lib/index.d.ts.map +1 -0
  28. package/lib/index.js +11 -821
  29. package/lib/net.d.ts +197 -0
  30. package/lib/net.d.ts.map +1 -0
  31. package/lib/net.js +875 -0
  32. package/lib/tls.d.ts +23 -1
  33. package/lib/tls.d.ts.map +1 -0
  34. package/lib/tls.js +108 -54
  35. package/nitrogen/generated/android/RustCNet+autolinking.cmake +2 -0
  36. package/nitrogen/generated/android/RustCNetOnLoad.cpp +2 -0
  37. package/nitrogen/generated/android/c++/JHybridHttpParserSpec.cpp +54 -0
  38. package/nitrogen/generated/android/c++/JHybridHttpParserSpec.hpp +65 -0
  39. package/nitrogen/generated/android/c++/JHybridNetDriverSpec.cpp +9 -0
  40. package/nitrogen/generated/android/c++/JHybridNetDriverSpec.hpp +1 -0
  41. package/nitrogen/generated/android/c++/JHybridNetServerDriverSpec.cpp +4 -0
  42. package/nitrogen/generated/android/c++/JHybridNetServerDriverSpec.hpp +1 -0
  43. package/nitrogen/generated/android/c++/JHybridNetSocketDriverSpec.cpp +9 -0
  44. package/nitrogen/generated/android/c++/JHybridNetSocketDriverSpec.hpp +2 -0
  45. package/nitrogen/generated/android/c++/JNetConfig.hpp +7 -3
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridHttpParserSpec.kt +58 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetDriverSpec.kt +4 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetServerDriverSpec.kt +4 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetSocketDriverSpec.kt +8 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/NetConfig.kt +6 -3
  51. package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Bridge.cpp +17 -0
  52. package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Bridge.hpp +26 -0
  53. package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Umbrella.hpp +5 -0
  54. package/nitrogen/generated/ios/c++/HybridHttpParserSpecSwift.cpp +11 -0
  55. package/nitrogen/generated/ios/c++/HybridHttpParserSpecSwift.hpp +79 -0
  56. package/nitrogen/generated/ios/c++/HybridNetDriverSpecSwift.hpp +11 -0
  57. package/nitrogen/generated/ios/c++/HybridNetServerDriverSpecSwift.hpp +6 -0
  58. package/nitrogen/generated/ios/c++/HybridNetSocketDriverSpecSwift.hpp +14 -0
  59. package/nitrogen/generated/ios/swift/HybridHttpParserSpec.swift +56 -0
  60. package/nitrogen/generated/ios/swift/HybridHttpParserSpec_cxx.swift +131 -0
  61. package/nitrogen/generated/ios/swift/HybridNetDriverSpec.swift +1 -0
  62. package/nitrogen/generated/ios/swift/HybridNetDriverSpec_cxx.swift +15 -0
  63. package/nitrogen/generated/ios/swift/HybridNetServerDriverSpec.swift +1 -0
  64. package/nitrogen/generated/ios/swift/HybridNetServerDriverSpec_cxx.swift +11 -0
  65. package/nitrogen/generated/ios/swift/HybridNetSocketDriverSpec.swift +2 -0
  66. package/nitrogen/generated/ios/swift/HybridNetSocketDriverSpec_cxx.swift +36 -0
  67. package/nitrogen/generated/ios/swift/NetConfig.swift +19 -1
  68. package/nitrogen/generated/shared/c++/HybridHttpParserSpec.cpp +21 -0
  69. package/nitrogen/generated/shared/c++/HybridHttpParserSpec.hpp +63 -0
  70. package/nitrogen/generated/shared/c++/HybridNetDriverSpec.cpp +1 -0
  71. package/nitrogen/generated/shared/c++/HybridNetDriverSpec.hpp +4 -0
  72. package/nitrogen/generated/shared/c++/HybridNetServerDriverSpec.cpp +1 -0
  73. package/nitrogen/generated/shared/c++/HybridNetServerDriverSpec.hpp +1 -0
  74. package/nitrogen/generated/shared/c++/HybridNetSocketDriverSpec.cpp +2 -0
  75. package/nitrogen/generated/shared/c++/HybridNetSocketDriverSpec.hpp +2 -0
  76. package/nitrogen/generated/shared/c++/NetConfig.hpp +6 -2
  77. package/package.json +9 -7
  78. package/src/Net.nitro.ts +17 -0
  79. package/src/http.ts +1303 -0
  80. package/src/https.ts +125 -0
  81. package/src/index.ts +13 -874
  82. package/src/net.ts +1005 -0
  83. package/src/tls.ts +82 -6
package/src/http.ts ADDED
@@ -0,0 +1,1303 @@
1
+ import { Writable, Readable } from 'readable-stream'
2
+ import { EventEmitter } from 'eventemitter3'
3
+ import { Driver } from './Driver'
4
+ import { Socket, isVerbose } from './net'
5
+ import { TLSSocket } from './tls'
6
+ import { Buffer } from 'react-native-nitro-buffer'
7
+
8
+ function debugLog(message: string) {
9
+ if (isVerbose()) {
10
+ const timestamp = new Date().toISOString().split('T')[1].split('Z')[0];
11
+ console.log(`[HTTP DEBUG ${timestamp}] ${message}`);
12
+ }
13
+ }
14
+
15
+ // ========== STATUS_CODES ==========
16
+
17
+ export const STATUS_CODES: Record<number, string> = {
18
+ 100: 'Continue',
19
+ 101: 'Switching Protocols',
20
+ 102: 'Processing',
21
+ 200: 'OK',
22
+ 201: 'Created',
23
+ 202: 'Accepted',
24
+ 203: 'Non-Authoritative Information',
25
+ 204: 'No Content',
26
+ 205: 'Reset Content',
27
+ 206: 'Partial Content',
28
+ 300: 'Multiple Choices',
29
+ 301: 'Moved Permanently',
30
+ 302: 'Found',
31
+ 303: 'See Other',
32
+ 304: 'Not Modified',
33
+ 307: 'Temporary Redirect',
34
+ 308: 'Permanent Redirect',
35
+ 400: 'Bad Request',
36
+ 401: 'Unauthorized',
37
+ 402: 'Payment Required',
38
+ 403: 'Forbidden',
39
+ 404: 'Not Found',
40
+ 405: 'Method Not Allowed',
41
+ 406: 'Not Acceptable',
42
+ 407: 'Proxy Authentication Required',
43
+ 408: 'Request Timeout',
44
+ 409: 'Conflict',
45
+ 410: 'Gone',
46
+ 411: 'Length Required',
47
+ 412: 'Precondition Failed',
48
+ 413: 'Payload Too Large',
49
+ 414: 'URI Too Long',
50
+ 415: 'Unsupported Media Type',
51
+ 416: 'Range Not Satisfiable',
52
+ 417: 'Expectation Failed',
53
+ 418: "I'm a teapot",
54
+ 421: 'Misdirected Request',
55
+ 422: 'Unprocessable Entity',
56
+ 423: 'Locked',
57
+ 424: 'Failed Dependency',
58
+ 425: 'Too Early',
59
+ 426: 'Upgrade Required',
60
+ 428: 'Precondition Required',
61
+ 429: 'Too Many Requests',
62
+ 431: 'Request Header Fields Too Large',
63
+ 451: 'Unavailable For Legal Reasons',
64
+ 500: 'Internal Server Error',
65
+ 501: 'Not Implemented',
66
+ 502: 'Bad Gateway',
67
+ 503: 'Service Unavailable',
68
+ 504: 'Gateway Timeout',
69
+ 505: 'HTTP Version Not Supported',
70
+ 506: 'Variant Also Negotiates',
71
+ 507: 'Insufficient Storage',
72
+ 508: 'Loop Detected',
73
+ 510: 'Not Extended',
74
+ 511: 'Network Authentication Required',
75
+ };
76
+
77
+ export const METHODS = [
78
+ 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD',
79
+ 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL',
80
+ 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH',
81
+ 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE',
82
+ 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE'
83
+ ];
84
+
85
+ // ========== IncomingMessage ==========
86
+
87
+ export class IncomingMessage extends Readable {
88
+ public httpVersion: string = '1.1';
89
+ public httpVersionMajor: number = 1;
90
+ public httpVersionMinor: number = 1;
91
+ public method?: string;
92
+ public url?: string;
93
+ public statusCode?: number;
94
+ public statusMessage?: string;
95
+ public headers: Record<string, string | string[]> = {};
96
+ public rawHeaders: string[] = [];
97
+ public socket: Socket;
98
+ public aborted: boolean = false;
99
+ public complete: boolean = false;
100
+ public trailers: Record<string, string> = {};
101
+
102
+ constructor(socket: Socket) {
103
+ // @ts-ignore
104
+ super({ autoDestroy: false });
105
+ this.socket = socket;
106
+ }
107
+
108
+ _read() {
109
+ this.socket.resume();
110
+ }
111
+
112
+ public setTimeout(msecs: number, callback?: () => void): this {
113
+ this.socket.setTimeout(msecs, callback);
114
+ return this;
115
+ }
116
+
117
+ public destroy(error?: Error): this {
118
+ super.destroy(error);
119
+ this.socket.destroy();
120
+ return this;
121
+ }
122
+
123
+ public setNoDelay(noDelay: boolean = true): void {
124
+ this.socket.setNoDelay(noDelay);
125
+ }
126
+
127
+ public setKeepAlive(enable: boolean = false, initialDelay: number = 0): void {
128
+ this.socket.setKeepAlive(enable, initialDelay);
129
+ }
130
+ }
131
+
132
+ // ========== OutgoingMessage ==========
133
+
134
+ export class OutgoingMessage extends Writable {
135
+ public headersSent: boolean = false;
136
+ protected _headers: Record<string, any> = {};
137
+ protected _headerNames: Record<string, string> = {};
138
+ public socket: Socket | null = null;
139
+
140
+ public chunkedEncoding: boolean = false;
141
+ protected _hasBody: boolean = true;
142
+ protected _sendHeadersSent: boolean = false;
143
+ public aborted: boolean = false;
144
+ protected _trailers: Record<string, string> | null = null;
145
+
146
+ constructor() {
147
+ // @ts-ignore - disable autoDestroy to prevent socket from being destroyed when stream ends
148
+ super({ autoDestroy: false });
149
+ this.once('finish', () => {
150
+ this.emit('close');
151
+ });
152
+ }
153
+
154
+ public destroy(error?: Error): this {
155
+ super.destroy(error);
156
+ if (this.socket) {
157
+ this.socket.destroy();
158
+ }
159
+ return this;
160
+ }
161
+
162
+ setHeader(name: string, value: any): this {
163
+ if (this.headersSent) throw new Error('Cannot set headers after they are sent');
164
+ const key = name.toLowerCase();
165
+ this._headers[key] = value;
166
+ this._headerNames[key] = name;
167
+ return this;
168
+ }
169
+
170
+ getHeader(name: string): any {
171
+ return this._headers[name.toLowerCase()];
172
+ }
173
+
174
+ removeHeader(name: string): void {
175
+ if (this.headersSent) throw new Error('Cannot remove headers after they are sent');
176
+ const key = name.toLowerCase();
177
+ delete this._headers[key];
178
+ delete this._headerNames[key];
179
+ }
180
+
181
+ hasHeader(name: string): boolean {
182
+ return name.toLowerCase() in this._headers;
183
+ }
184
+
185
+ getHeaderNames(): string[] {
186
+ return Object.values(this._headerNames);
187
+ }
188
+
189
+ public setTimeout(ms: number, callback?: () => void): this {
190
+ if (this.socket) {
191
+ this.socket.setTimeout(ms, () => {
192
+ this.emit('timeout');
193
+ if (callback) callback();
194
+ });
195
+ } else {
196
+ this.once('socket', (s: Socket) => {
197
+ s.setTimeout(ms, () => {
198
+ this.emit('timeout');
199
+ if (callback) callback();
200
+ });
201
+ });
202
+ }
203
+ return this;
204
+ }
205
+
206
+ protected _renderHeaders(firstLine: string): string {
207
+ let headerStr = firstLine + '\r\n';
208
+ for (const key in this._headers) {
209
+ const name = this._headerNames[key];
210
+ const value = this._headers[key];
211
+ if (Array.isArray(value)) {
212
+ for (const v of value) {
213
+ headerStr += `${name}: ${v}\r\n`;
214
+ }
215
+ } else {
216
+ headerStr += `${name}: ${value}\r\n`;
217
+ }
218
+ }
219
+ headerStr += '\r\n';
220
+ return headerStr;
221
+ }
222
+
223
+ protected _sendHeaders(firstLine: string) {
224
+ if (this.headersSent) return;
225
+
226
+ // Check for Chunked Encoding
227
+ if (!this.hasHeader('Content-Length') && this._hasBody) {
228
+ this.setHeader('Transfer-Encoding', 'chunked');
229
+ this.chunkedEncoding = true;
230
+ }
231
+
232
+ this.headersSent = true;
233
+ const headerStr = this._renderHeaders(firstLine);
234
+ debugLog(`OutgoingMessage._sendHeaders: writing ${headerStr.length} bytes to socket (socket=${!!this.socket})`);
235
+ this.socket!.write(Buffer.from(headerStr));
236
+ }
237
+
238
+ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void) {
239
+ if (!this.socket) {
240
+ callback(new Error('Socket not assigned'));
241
+ return;
242
+ }
243
+
244
+ if (this.chunkedEncoding) {
245
+ const len = typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding as any) : chunk.length;
246
+ const header = len.toString(16) + '\r\n';
247
+ this.socket.write(Buffer.from(header));
248
+ // Note: We don't return the backpressure status here because we are doing multiple writes
249
+ // The final write determines the callback.
250
+ this.socket.write(chunk, encoding as any, (err) => {
251
+ if (err) return callback(err);
252
+ this.socket!.write(Buffer.from('\r\n'), undefined, callback);
253
+ });
254
+ } else {
255
+ this.socket.write(chunk, encoding as any, callback);
256
+ }
257
+ }
258
+
259
+ public write(chunk: any, encoding?: any, callback?: any): boolean {
260
+ const ret = super.write(chunk, encoding, callback);
261
+ // If writableLength is too high, return false
262
+ // But since we are proxying to socket, we should also check socket backpressure
263
+ if (this.socket && (this.socket as any)._writableState) {
264
+ // This is a bit hacky but if we have a real Node-like socket, we respect its state
265
+ return ret && !(this.socket as any)._writableState.needDrain;
266
+ }
267
+ return ret;
268
+ }
269
+
270
+ // _final is called by the stream when all writes are complete before 'finish' event
271
+ _final(callback: (error?: Error | null) => void) {
272
+ if (this.chunkedEncoding && this.socket) {
273
+ let terminator = '0\r\n';
274
+ if (this._trailers) {
275
+ for (const [key, value] of Object.entries(this._trailers)) {
276
+ terminator += `${key}: ${value}\r\n`;
277
+ }
278
+ }
279
+ terminator += '\r\n';
280
+ this.socket.write(Buffer.from(terminator), undefined, (err) => {
281
+ callback(err);
282
+ });
283
+ } else {
284
+ callback();
285
+ }
286
+ }
287
+
288
+ public addTrailers(headers: Record<string, string>) {
289
+ if (this.headersSent && !this.chunkedEncoding) {
290
+ throw new Error('Trailers can only be used with chunked encoding');
291
+ }
292
+ this._trailers = headers;
293
+ }
294
+
295
+ end(chunk?: any, encoding?: any, callback?: any): this {
296
+ debugLog(`OutgoingMessage.end() called, already ending: ${(this as any)._writableState?.ending}, chunk: ${!!chunk}`);
297
+ if (chunk) {
298
+ this.write(chunk, encoding);
299
+ }
300
+ super.end(undefined, undefined, callback);
301
+ return this;
302
+ }
303
+
304
+ public setNoDelay(noDelay: boolean = true): void {
305
+ this.socket?.setNoDelay(noDelay);
306
+ }
307
+
308
+ public setSocketKeepAlive(enable: boolean = false, initialDelay: number = 0): void {
309
+ this.socket?.setKeepAlive(enable, initialDelay);
310
+ }
311
+ }
312
+
313
+ // ========== ServerResponse ==========
314
+
315
+ export class ServerResponse extends OutgoingMessage {
316
+ public statusCode: number = 200;
317
+ public statusMessage?: string;
318
+ public socket: Socket;
319
+
320
+ constructor(socket: Socket) {
321
+ super();
322
+ this.socket = socket;
323
+ }
324
+
325
+ writeHead(statusCode: number, statusMessage?: string | Record<string, any>, headers?: Record<string, any>): this {
326
+ if (this.headersSent) throw new Error('Cannot write headers after they are sent');
327
+ this.statusCode = statusCode;
328
+ if (typeof statusMessage === 'object') {
329
+ headers = statusMessage;
330
+ statusMessage = undefined;
331
+ }
332
+ if (statusMessage) this.statusMessage = statusMessage;
333
+ if (headers) {
334
+ for (const key in headers) {
335
+ this.setHeader(key, headers[key]);
336
+ }
337
+ }
338
+ // Note: Do NOT send headers here. They will be sent on first write/end
339
+ // when Content-Length can be determined.
340
+ return this;
341
+ }
342
+
343
+ private _sendResponseHeaders() {
344
+ if (this.headersSent) return;
345
+ const firstLine = `HTTP/1.1 ${this.statusCode} ${this.statusMessage || STATUS_CODES[this.statusCode] || 'OK'}`;
346
+ this._sendHeaders(firstLine);
347
+ }
348
+
349
+ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void) {
350
+ if (!this.headersSent) this._sendResponseHeaders();
351
+ super._write(chunk, encoding, callback);
352
+ }
353
+
354
+ write(chunk: any, encoding?: any, callback?: any): boolean {
355
+ return super.write(chunk, encoding, callback);
356
+ }
357
+
358
+ end(chunk?: any, encoding?: any, callback?: any): this {
359
+ if (!this.headersSent) {
360
+ // If we have a single chunk and no headers sent yet, we can add Content-Length
361
+ // to avoid chunked encoding for simple responses.
362
+ if (chunk) {
363
+ const len = typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.length;
364
+ this.setHeader('Content-Length', len);
365
+ } else {
366
+ this.setHeader('Content-Length', 0);
367
+ }
368
+ this._sendResponseHeaders();
369
+ }
370
+ // super.end will trigger _write if chunk was provided.
371
+ super.end(chunk, encoding, () => {
372
+ if (callback) callback();
373
+ });
374
+ return this;
375
+ }
376
+ }
377
+
378
+ // ========== Server ==========
379
+
380
+ export interface ServerOptions {
381
+ /**
382
+ * Optionally overrides all net.Server options.
383
+ */
384
+ IncomingMessage?: typeof IncomingMessage;
385
+ ServerResponse?: typeof ServerResponse;
386
+ /**
387
+ * Keep-Alive header timeout in milliseconds.
388
+ */
389
+ keepAliveTimeout?: number;
390
+ /**
391
+ * Request timeout in milliseconds.
392
+ */
393
+ requestTimeout?: number;
394
+ /**
395
+ * Headers timeout in milliseconds.
396
+ */
397
+ headersTimeout?: number;
398
+ /**
399
+ * Max header size in bytes.
400
+ */
401
+ maxHeaderSize?: number;
402
+ /**
403
+ * If defined, sets the maximum number of requests socket can handle.
404
+ */
405
+ maxRequestsPerSocket?: number;
406
+ }
407
+
408
+ export class Server extends EventEmitter {
409
+ protected _netServer: any;
410
+ protected _httpConnections = new Set<Socket>();
411
+ public maxHeaderSize: number = 16384;
412
+ public maxRequestsPerSocket: number = 0;
413
+ public headersTimeout: number = 60000;
414
+ public requestTimeout: number = 300000;
415
+ public keepAliveTimeout: number = 5000;
416
+
417
+ constructor(options?: ServerOptions | ((req: IncomingMessage, res: ServerResponse) => void), requestListener?: (req: IncomingMessage, res: ServerResponse) => void) {
418
+ super();
419
+ // Use net.Server from index.ts
420
+ const { Server: NetServer } = require('./net');
421
+ this._netServer = new NetServer();
422
+
423
+ let listener: ((req: IncomingMessage, res: ServerResponse) => void) | undefined;
424
+ if (typeof options === 'function') {
425
+ listener = options;
426
+ } else if (options) {
427
+ if (options.keepAliveTimeout !== undefined) this.keepAliveTimeout = options.keepAliveTimeout;
428
+ if (options.requestTimeout !== undefined) this.requestTimeout = options.requestTimeout;
429
+ if (options.headersTimeout !== undefined) this.headersTimeout = options.headersTimeout;
430
+ if (options.maxHeaderSize !== undefined) this.maxHeaderSize = options.maxHeaderSize;
431
+ if (options.maxRequestsPerSocket !== undefined) this.maxRequestsPerSocket = options.maxRequestsPerSocket;
432
+ listener = requestListener;
433
+ }
434
+
435
+ if (listener) {
436
+ this.on('request', listener);
437
+ }
438
+
439
+ // Forward net.Server events
440
+ this._netServer.on('listening', () => this.emit('listening'));
441
+ this._netServer.on('close', () => this.emit('close'));
442
+ this._netServer.on('error', (err: any) => this.emit('error', err));
443
+
444
+ this._netServer.on('connection', (socket: Socket) => {
445
+ this._setupHttpConnection(socket);
446
+ });
447
+ }
448
+
449
+ protected _setupHttpConnection(socket: Socket) {
450
+ this._httpConnections.add(socket);
451
+ let req: IncomingMessage | null = null;
452
+ let res: ServerResponse | null = null;
453
+ const parser = Driver.createHttpParser(0); // 0 = Request mode
454
+ // @ts-ignore
455
+ let bodyBytesRead = 0;
456
+ // @ts-ignore
457
+ let contentLength = -1;
458
+
459
+ // headersTimeout logic
460
+ let headersTimer: any = null;
461
+ if (this.headersTimeout > 0) {
462
+ headersTimer = setTimeout(() => {
463
+ debugLog(`Server: headersTimeout reached for socket, destroying`);
464
+ socket.destroy();
465
+ }, this.headersTimeout);
466
+ }
467
+
468
+ const onData = (data: Buffer) => {
469
+ const handleParsedResult = (result: string) => {
470
+ if (result.startsWith('ERROR:')) {
471
+ if (headersTimer) clearTimeout(headersTimer);
472
+ this.emit('error', new Error(result));
473
+ socket.destroy();
474
+ return;
475
+ }
476
+ const parsed = JSON.parse(result);
477
+
478
+ if (parsed.is_headers) {
479
+ if (headersTimer) {
480
+ clearTimeout(headersTimer);
481
+ headersTimer = null;
482
+ }
483
+
484
+ // Handle CONNECT method (HTTP Tunneling)
485
+ if (parsed.is_connect) {
486
+ const req = new IncomingMessage(socket);
487
+ req.method = parsed.method;
488
+ req.url = parsed.path;
489
+ req.httpVersion = '1.' + parsed.version;
490
+ req.headers = parsed.headers;
491
+
492
+ // Remove our data listener to stop feeding the parser
493
+ // The user is responsible for handling the socket data stream from now on
494
+ socket.removeListener('data', onData);
495
+
496
+ debugLog(`Server: CONNECT request received, emitting 'connect' event`);
497
+
498
+ // TODO: retrieve any remaining body from parser as 'head'
499
+ const head = Buffer.alloc(0);
500
+
501
+ if (this.listenerCount('connect') > 0) {
502
+ this.emit('connect', req, socket, head);
503
+ } else {
504
+ // Default behavior: close connection if no listener
505
+ socket.destroy();
506
+ }
507
+ return;
508
+ }
509
+
510
+ const currentReq = new IncomingMessage(socket);
511
+ currentReq.method = parsed.method;
512
+ currentReq.url = parsed.path;
513
+ currentReq.httpVersion = '1.' + parsed.version;
514
+ currentReq.headers = parsed.headers;
515
+ req = currentReq;
516
+
517
+ const currentRes = new ServerResponse(socket);
518
+ res = currentRes;
519
+
520
+ // Support Keep-Alive: reset state once response is done
521
+ currentRes.on('finish', () => {
522
+ req = null;
523
+ res = null;
524
+ // The parser should already be reset in Rust
525
+ });
526
+
527
+ const upgrade = req.headers['upgrade'];
528
+ if (upgrade && this.listenerCount('upgrade') > 0) {
529
+ debugLog(`Server: Upgrade request received, emitting 'upgrade' event`);
530
+ this.emit('upgrade', req, socket, Buffer.alloc(0));
531
+ return;
532
+ }
533
+
534
+ const expect = req.headers['expect'];
535
+ if (expect && (typeof expect === 'string' && expect.toLowerCase() === '100-continue')) {
536
+ if (this.listenerCount('checkContinue') > 0) {
537
+ this.emit('checkContinue', req, res);
538
+ } else {
539
+ socket.write(Buffer.from('HTTP/1.1 100 Continue\r\n\r\n'));
540
+ this.emit('request', req, res);
541
+ }
542
+ } else {
543
+ debugLog(`Server: Emitting 'request' for ${req.method} ${req.url}`);
544
+ this.emit('request', req, res);
545
+ }
546
+ }
547
+
548
+ if (req && parsed.body && parsed.body.length > 0) {
549
+ req.push(Buffer.from(parsed.body));
550
+ }
551
+
552
+ if (req && parsed.complete) {
553
+ req.complete = true;
554
+ if (parsed.trailers) {
555
+ req.trailers = parsed.trailers;
556
+ }
557
+ req.push(null);
558
+ }
559
+
560
+ // For Keep-Alive, try to parse remaining buffer in case of pipelining
561
+ if (parsed.complete && !req) {
562
+ // This case is handled by the feed loop if multiple messages in data
563
+ }
564
+ };
565
+
566
+ let input: ArrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
567
+ let iterations = 0;
568
+ const maxIterations = 100; // Safety limit
569
+ while (iterations < maxIterations) {
570
+ iterations++;
571
+ const result = parser.feed(input);
572
+ if (!result || result === '' || result.startsWith('ERROR:')) {
573
+ // Empty result (partial) or error - exit loop
574
+ if (result && result.startsWith('ERROR:')) {
575
+ console.log(`[HTTP] Server: Parser error: ${result}`);
576
+ }
577
+ break;
578
+ }
579
+ handleParsedResult(result);
580
+ input = new ArrayBuffer(0); // Continue with empty input to drain Rust buffer
581
+ }
582
+ };
583
+ socket.on('data', onData);
584
+
585
+ // CRITICAL: Ensure server-side socket starts flowing!
586
+ socket.resume();
587
+
588
+ socket.on('close', () => {
589
+ if (headersTimer) clearTimeout(headersTimer);
590
+ this._httpConnections.delete(socket);
591
+ if (req && !req.readableEnded) {
592
+ req.push(null);
593
+ }
594
+ });
595
+
596
+ socket.on('error', (err: Error) => {
597
+ if (req) req.emit('error', err);
598
+ else this.emit('error', err);
599
+ });
600
+ }
601
+
602
+ listen(...args: any[]): this {
603
+ this._netServer.listen(...args);
604
+ return this;
605
+ }
606
+
607
+ close(callback?: (err?: Error) => void): this {
608
+ this._netServer.close(callback);
609
+ return this;
610
+ }
611
+
612
+ // @ts-ignore
613
+ async[Symbol.asyncDispose]() {
614
+ return new Promise<void>((resolve) => {
615
+ this.close(() => resolve());
616
+ });
617
+ }
618
+
619
+ address(): { port: number; family: string; address: string } | null {
620
+ return this._netServer.address();
621
+ }
622
+
623
+ get listening(): boolean {
624
+ return this._netServer.listening;
625
+ }
626
+
627
+ setTimeout(ms: number, callback?: () => void): this {
628
+ this._netServer.setTimeout(ms, callback);
629
+ return this;
630
+ }
631
+ }
632
+
633
+ // ========== Agent ==========
634
+
635
+ export interface AgentOptions {
636
+ keepAlive?: boolean;
637
+ keepAliveMsecs?: number;
638
+ maxSockets?: number;
639
+ maxTotalSockets?: number;
640
+ maxFreeSockets?: number;
641
+ scheduling?: 'fifo' | 'lifo';
642
+ timeout?: number;
643
+ maxCachedSessions?: number;
644
+ }
645
+
646
+ export class Agent extends EventEmitter {
647
+ public maxSockets: number = Infinity;
648
+ public maxTotalSockets: number = Infinity;
649
+ public maxFreeSockets: number = 256;
650
+ public keepAlive: boolean = false;
651
+ public keepAliveMsecs: number = 1000;
652
+ public maxCachedSessions: number = 100;
653
+ public scheduling: 'fifo' | 'lifo' = 'lifo';
654
+
655
+ public requests: Record<string, ClientRequest[]> = {};
656
+ public sockets: Record<string, Socket[]> = {};
657
+ public freeSockets: Record<string, Socket[]> = {};
658
+ private _totalSockets: number = 0;
659
+ public proxy: string | null = null;
660
+
661
+ /**
662
+ * Gets the proxy URL for the given request options.
663
+ * Checks HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables.
664
+ *
665
+ * @param options Request options to determine if proxy should be used
666
+ * @returns Proxy URL or null if no proxy should be used
667
+ */
668
+ protected getProxy(options: RequestOptions): string | null {
669
+ // If explicitly set on agent, use that
670
+ if (this.proxy) return this.proxy;
671
+
672
+ // Check environment variables (React Native may not have process.env)
673
+ const env = typeof process !== 'undefined' && process.env ? process.env : {};
674
+ const isHttps = options.protocol === 'https:';
675
+ const host = options.hostname || options.host || 'localhost';
676
+
677
+ // Check NO_PROXY first
678
+ const noProxy = env.NO_PROXY || env.no_proxy;
679
+ if (noProxy) {
680
+ const noProxyList = noProxy.split(',').map(s => s.trim().toLowerCase());
681
+ const hostLower = host.toLowerCase();
682
+ for (const pattern of noProxyList) {
683
+ if (pattern === '*') return null;
684
+ if (pattern.startsWith('.') && hostLower.endsWith(pattern)) return null;
685
+ if (hostLower === pattern) return null;
686
+ if (hostLower.endsWith('.' + pattern)) return null;
687
+ }
688
+ }
689
+
690
+ // Get proxy URL based on protocol
691
+ const proxyUrl = isHttps
692
+ ? (env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy)
693
+ : (env.HTTP_PROXY || env.http_proxy);
694
+
695
+ return proxyUrl || null;
696
+ }
697
+
698
+ constructor(options?: AgentOptions) {
699
+ super();
700
+ if (options?.maxSockets) this.maxSockets = options.maxSockets;
701
+ if (options?.maxTotalSockets) this.maxTotalSockets = options.maxTotalSockets;
702
+ if (options?.maxFreeSockets) this.maxFreeSockets = options.maxFreeSockets;
703
+ if (options?.keepAlive) this.keepAlive = options.keepAlive;
704
+ if (options?.keepAliveMsecs) this.keepAliveMsecs = options.keepAliveMsecs;
705
+ if (options?.scheduling) this.scheduling = options.scheduling;
706
+ if (options?.maxCachedSessions !== undefined) this.maxCachedSessions = options.maxCachedSessions;
707
+ }
708
+
709
+ public getName(options: RequestOptions): string {
710
+ let name = `${options.host || options.hostname || 'localhost'}:${options.port || (options.protocol === 'https:' ? 443 : 80)}:`;
711
+ if (options.localAddress) name += `${options.localAddress}:`;
712
+ if (options.family) name += `${options.family}:`;
713
+ return name;
714
+ }
715
+
716
+ public addRequest(req: ClientRequest, options: RequestOptions) {
717
+ const name = this.getName(options);
718
+ debugLog(`Agent.addRequest: name=${name}, totalSockets=${this._totalSockets}`);
719
+
720
+ // 1. Check if there's an idle socket in freeSockets
721
+ if (this.freeSockets[name] && this.freeSockets[name].length > 0) {
722
+ const socket = this.scheduling === 'lifo'
723
+ ? this.freeSockets[name].pop()!
724
+ : this.freeSockets[name].shift()!;
725
+
726
+ if (this.freeSockets[name].length === 0) delete this.freeSockets[name];
727
+
728
+ // Re-use socket
729
+ if (!this.sockets[name]) this.sockets[name] = [];
730
+ this.sockets[name].push(socket);
731
+
732
+ req.onSocket(socket);
733
+ return;
734
+ }
735
+
736
+ // 2. Check if we can create a new connection
737
+ const currentSockets = (this.sockets[name]?.length || 0);
738
+ if (currentSockets < this.maxSockets && this._totalSockets < this.maxTotalSockets) {
739
+ if (!this.sockets[name]) this.sockets[name] = [];
740
+
741
+ // Increment total sockets early
742
+ this._totalSockets++;
743
+
744
+ // ClientRequest handles connection but we signal it to proceed
745
+ req.onSocket(null as any);
746
+ return;
747
+ }
748
+
749
+ // 3. Queue the request
750
+ if (!this.requests[name]) this.requests[name] = [];
751
+ this.requests[name].push(req);
752
+ }
753
+
754
+ public createConnection(options: RequestOptions, callback: (err: Error | null, socket: Socket) => void): Socket {
755
+ const name = this.getName(options);
756
+ const isHttps = options.protocol === 'https:';
757
+ const port = options.port || (isHttps ? 443 : 80);
758
+ const host = options.hostname || options.host || 'localhost';
759
+
760
+ debugLog(`Agent.createConnection: name=${name}, isHttps=${isHttps}, host=${host}, port=${port}`);
761
+
762
+ // Build clean connection options - DO NOT pass HTTP path as it will be confused with Unix socket path
763
+ const connectOptions: any = {
764
+ host: host,
765
+ port: port,
766
+ };
767
+ if (isHttps) {
768
+ connectOptions.servername = (options as any).servername || host;
769
+ connectOptions.rejectUnauthorized = options.rejectUnauthorized !== false;
770
+ if ((options as any).ca) connectOptions.ca = (options as any).ca;
771
+ if ((options as any).cert) connectOptions.cert = (options as any).cert;
772
+ if ((options as any).key) connectOptions.key = (options as any).key;
773
+ }
774
+
775
+ const socket = isHttps ? new TLSSocket(connectOptions) : new Socket();
776
+
777
+ // Re-emit keylog events from TLSSockets
778
+ if (isHttps) {
779
+ socket.on('keylog', (line: Buffer) => {
780
+ // @ts-ignore - Agent is an EventEmitter via Node-like inheritance or internal use
781
+ this.emit('keylog', line, socket);
782
+ });
783
+ }
784
+
785
+ let called = false;
786
+ const onConnected = () => {
787
+ if (called) return;
788
+ called = true;
789
+ debugLog(`Agent.createConnection: socket ${isHttps ? 'SECURE_CONNECTED' : 'CONNECTED'} for ${name}`);
790
+ callback(null, socket);
791
+ };
792
+
793
+ if (isHttps) {
794
+ (socket as TLSSocket).on('secureConnect', onConnected);
795
+ } else {
796
+ socket.on('connect', onConnected);
797
+ }
798
+
799
+ socket.on('error', (err) => {
800
+ if (called) return;
801
+ called = true;
802
+ debugLog(`Agent.createConnection: socket ERROR for ${name}: ${err.message}`);
803
+ this._totalSockets--;
804
+ if (this.sockets[name]) {
805
+ const idx = this.sockets[name].indexOf(socket);
806
+ if (idx !== -1) this.sockets[name].splice(idx, 1);
807
+ }
808
+ callback(err, null as any);
809
+ });
810
+
811
+ socket.connect(connectOptions);
812
+
813
+ if (!this.sockets[name]) this.sockets[name] = [];
814
+ this.sockets[name].push(socket);
815
+
816
+ return socket;
817
+ }
818
+
819
+ public releaseSocket(socket: Socket, options: RequestOptions) {
820
+ const name = this.getName(options);
821
+
822
+ // Remove from active sockets
823
+ if (this.sockets[name]) {
824
+ const idx = this.sockets[name].indexOf(socket);
825
+ if (idx !== -1) this.sockets[name].splice(idx, 1);
826
+ if (this.sockets[name].length === 0) delete this.sockets[name];
827
+ }
828
+
829
+ const onClose = () => {
830
+ debugLog(`Agent: socket closed while in pool, removing from ${name}`);
831
+ this._removeSocket(socket, name);
832
+ };
833
+ socket.once('close', onClose);
834
+ socket.once('error', onClose);
835
+ (socket as any)._agentOnClose = onClose;
836
+
837
+ // Check if there are pending requests - ALWAYS reuse if something is waiting
838
+ if (this.requests[name] && this.requests[name].length > 0) {
839
+ const req = this.requests[name].shift()!;
840
+ if (this.requests[name].length === 0) delete this.requests[name];
841
+
842
+ if (!this.sockets[name]) this.sockets[name] = [];
843
+ this.sockets[name].push(socket);
844
+ this.reuseSocket(socket, req);
845
+ return;
846
+ }
847
+
848
+ if (this.keepAlive && this.keepSocketAlive(socket)) {
849
+ // Return to free pool
850
+ if (!this.freeSockets[name]) this.freeSockets[name] = [];
851
+ if (this.freeSockets[name].length < this.maxFreeSockets) {
852
+ this.freeSockets[name].push(socket);
853
+ } else {
854
+ this._totalSockets--;
855
+ socket.end();
856
+ }
857
+ } else {
858
+ this._totalSockets--;
859
+ socket.end();
860
+ }
861
+ }
862
+
863
+ public keepSocketAlive(_socket: Socket): boolean {
864
+ return true;
865
+ }
866
+
867
+ public reuseSocket(socket: Socket, req: ClientRequest): void {
868
+ debugLog(`Agent.reuseSocket: reusing socket for ${req.method} ${req.path}`);
869
+ // Remove agent listeners before reusing
870
+ if ((socket as any)._agentOnClose) {
871
+ socket.removeListener('close', (socket as any)._agentOnClose);
872
+ socket.removeListener('error', (socket as any)._agentOnClose);
873
+ delete (socket as any)._agentOnClose;
874
+ }
875
+ req.onSocket(socket);
876
+ }
877
+
878
+ private _removeSocket(socket: Socket, name: string) {
879
+ if (this.sockets[name]) {
880
+ const idx = this.sockets[name].indexOf(socket);
881
+ if (idx !== -1) {
882
+ this.sockets[name].splice(idx, 1);
883
+ this._totalSockets--;
884
+ }
885
+ }
886
+ if (this.freeSockets[name]) {
887
+ const idx = this.freeSockets[name].indexOf(socket);
888
+ if (idx !== -1) {
889
+ this.freeSockets[name].splice(idx, 1);
890
+ this._totalSockets--;
891
+ }
892
+ }
893
+ }
894
+
895
+ destroy() {
896
+ for (const name in this.sockets) {
897
+ for (const socket of this.sockets[name]) {
898
+ socket.destroy();
899
+ }
900
+ }
901
+ for (const name in this.freeSockets) {
902
+ for (const socket of this.freeSockets[name]) {
903
+ socket.destroy();
904
+ }
905
+ }
906
+ }
907
+ }
908
+
909
+ export const globalAgent = new Agent();
910
+
911
+ // ========== ClientRequest ==========
912
+
913
+ export interface RequestOptions {
914
+ protocol?: string;
915
+ host?: string;
916
+ hostname?: string;
917
+ family?: number;
918
+ port?: number;
919
+ localAddress?: string;
920
+ socketPath?: string;
921
+ method?: string;
922
+ path?: string;
923
+ headers?: Record<string, any>;
924
+ auth?: string;
925
+ agent?: Agent | boolean;
926
+ timeout?: number;
927
+ rejectUnauthorized?: boolean;
928
+ // ...
929
+ }
930
+
931
+ export class ClientRequest extends OutgoingMessage {
932
+ public method: string;
933
+ public path: string;
934
+ public host: string;
935
+ private _res?: IncomingMessage;
936
+ private _options: RequestOptions;
937
+ private _connected: boolean = false;
938
+ private _pendingWrites: Array<{ chunk: any; encoding?: any; callback?: any }> = [];
939
+ private _ended: boolean = false;
940
+ private _expectContinue: boolean = false;
941
+ private _continueReceived: boolean = false;
942
+
943
+ constructor(options: RequestOptions, callback?: (res: IncomingMessage) => void) {
944
+ super();
945
+ this._options = options;
946
+ this.method = options.method || 'GET';
947
+ this.path = options.path || '/';
948
+ this.host = options.hostname || options.host || 'localhost';
949
+
950
+ if (['GET', 'HEAD'].includes(this.method.toUpperCase())) {
951
+ this._hasBody = false;
952
+ }
953
+
954
+ if (options.headers) {
955
+ for (const key in options.headers) {
956
+ this.setHeader(key, options.headers[key]);
957
+ }
958
+ }
959
+
960
+ if (callback) {
961
+ this.once('response', callback);
962
+ }
963
+
964
+ const expect = this.getHeader('expect');
965
+ if (expect && typeof expect === 'string' && expect.toLowerCase() === '100-continue') {
966
+ this._expectContinue = true;
967
+ }
968
+
969
+ if (options.timeout) {
970
+ this.setTimeout(options.timeout);
971
+ }
972
+
973
+ const agent = options.agent === false ? new Agent() : (options.agent instanceof Agent ? options.agent : globalAgent);
974
+
975
+ // Use setImmediate or setTimeout for React Native compatibility
976
+ const nextTick = typeof setImmediate !== 'undefined' ? setImmediate : (fn: () => void) => setTimeout(fn, 0);
977
+ nextTick(() => {
978
+ debugLog(`ClientRequest: nextTick fired for ${this.method} ${this.host}${this.path}`);
979
+ agent.addRequest(this, this._options)
980
+ });
981
+ }
982
+
983
+ /** @internal */
984
+ public onSocket(socket: Socket | null) {
985
+ if (socket) {
986
+ this.socket = socket;
987
+ this._connected = true;
988
+ this.emit('socket', this.socket);
989
+ this._sendRequest();
990
+ this._flushPendingWrites();
991
+ this._attachSocketListeners();
992
+ } else {
993
+ this._connect();
994
+ }
995
+ }
996
+
997
+ private _connect() {
998
+ const agent = this._options.agent === false ? new Agent() : (this._options.agent instanceof Agent ? this._options.agent : globalAgent);
999
+
1000
+ const connectCallback = (err: Error | null, socket: Socket) => {
1001
+ if (err) {
1002
+ debugLog(`ClientRequest._connect: ERROR: ${err.message}`);
1003
+ this.emit('error', err);
1004
+ return;
1005
+ }
1006
+ debugLog(`ClientRequest._connect: Socket connected! socket=${!!socket}, socket._driver=${!!(socket as any)._driver}`);
1007
+ console.log(`[HTTP] _connect: Socket connected!`);
1008
+ this.socket = socket;
1009
+ this._connected = true;
1010
+ this.emit('socket', this.socket);
1011
+ debugLog(`ClientRequest._connect: Calling _sendRequest`);
1012
+ this._sendRequest();
1013
+ this._flushPendingWrites();
1014
+ this._attachSocketListeners();
1015
+ };
1016
+
1017
+ this.socket = agent.createConnection(this._options, connectCallback);
1018
+ }
1019
+
1020
+ private _attachSocketListeners() {
1021
+ if (!this.socket) return;
1022
+
1023
+ const parser = Driver.createHttpParser(1); // 1 = Response mode
1024
+
1025
+ const onData = (data: Buffer) => {
1026
+ const handleParsedResult = (result: string) => {
1027
+ if (result.startsWith('ERROR:')) {
1028
+ this.emit('error', new Error(result));
1029
+ this.socket!.destroy();
1030
+ return;
1031
+ }
1032
+ const parsed = JSON.parse(result);
1033
+ console.log(`[HTTP] _connect: Parser result: ${parsed.is_headers ? 'HEADERS' : 'DATA'}${parsed.complete ? ' (COMPLETE)' : ''}`);
1034
+
1035
+ if (parsed.is_headers) {
1036
+ const status = parsed.status || 0;
1037
+ if (status >= 100 && status < 200 && status !== 101) {
1038
+ const info = {
1039
+ httpVersion: '1.' + parsed.version,
1040
+ httpVersionMajor: 1,
1041
+ httpVersionMinor: parsed.version,
1042
+ statusCode: status,
1043
+ statusMessage: STATUS_CODES[status] || '',
1044
+ headers: parsed.headers,
1045
+ rawHeaders: []
1046
+ };
1047
+ if (status === 100) {
1048
+ this._continueReceived = true;
1049
+ this.emit('continue');
1050
+ this._flushPendingWrites();
1051
+ } else {
1052
+ this.emit('information', info);
1053
+ }
1054
+ return;
1055
+ }
1056
+
1057
+ this._res = new IncomingMessage(this.socket!);
1058
+ this._res.statusCode = status;
1059
+ this._res.httpVersion = '1.' + parsed.version;
1060
+ this._res.headers = parsed.headers;
1061
+
1062
+ if (status === 101) {
1063
+ debugLog(`ClientRequest: 101 Switching Protocols received, detaching parser`);
1064
+ this.socket!.removeListener('data', onData);
1065
+ this.socket!.removeListener('error', onError);
1066
+ this.emit('upgrade', this._res, this.socket!, Buffer.alloc(0));
1067
+ return;
1068
+ }
1069
+
1070
+ // Handle CONNECT method response (HTTP Tunneling)
1071
+ if (this.method.toUpperCase() === 'CONNECT' && status >= 200 && status < 300) {
1072
+ debugLog(`ClientRequest: CONNECT tunnel established (status=${status}), emitting 'connect' event`);
1073
+ this.socket!.removeListener('data', onData);
1074
+ this.socket!.removeListener('error', onError);
1075
+ this.emit('connect', this._res, this.socket!, Buffer.alloc(0));
1076
+ return;
1077
+ }
1078
+
1079
+ this.emit('response', this._res);
1080
+ }
1081
+
1082
+ if (this._res && parsed.body && parsed.body.length > 0) {
1083
+ this._res.push(Buffer.from(parsed.body));
1084
+ }
1085
+
1086
+ if (this._res && parsed.complete) {
1087
+ this._res.complete = true;
1088
+ if (parsed.trailers) {
1089
+ this._res.trailers = parsed.trailers;
1090
+ }
1091
+ this._res.push(null);
1092
+ this._finishResponse();
1093
+ }
1094
+ };
1095
+
1096
+ let input: ArrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
1097
+ let iterations = 0;
1098
+ const maxIterations = 100; // Safety limit
1099
+ while (iterations < maxIterations) {
1100
+ iterations++;
1101
+ const result = parser.feed(input);
1102
+ if (!result || result === '' || result.startsWith('ERROR:')) {
1103
+ // Empty result (partial) or error - exit loop
1104
+ if (result && result.startsWith('ERROR:')) {
1105
+ console.log(`[HTTP] ClientRequest: Parser error: ${result}`);
1106
+ }
1107
+ break;
1108
+ }
1109
+ handleParsedResult(result);
1110
+ input = new ArrayBuffer(0); // Continue with empty input to drain Rust buffer
1111
+ }
1112
+ };
1113
+
1114
+ const onError = (err: Error) => {
1115
+ console.log(`[HTTP] _connect: Socket error: ${err.message}`);
1116
+ this.emit('error', err);
1117
+ this._cleanupSocket();
1118
+ };
1119
+
1120
+ const onClose = () => {
1121
+ console.log(`[HTTP] _connect: Socket closed`);
1122
+ if (this._res && !this._res.readableEnded) this._res.push(null);
1123
+ this.emit('close');
1124
+ this._cleanupSocket();
1125
+ };
1126
+
1127
+ this.socket.on('data', onData);
1128
+ this.socket.on('error', onError);
1129
+ this.socket.on('close', onClose);
1130
+
1131
+ this._socketCleanup = () => {
1132
+ this.socket?.removeListener('data', onData);
1133
+ this.socket?.removeListener('error', onError);
1134
+ this.socket?.removeListener('close', onClose);
1135
+ };
1136
+ }
1137
+
1138
+ private _socketCleanup?: () => void;
1139
+ private _cleanupSocket() {
1140
+ if (this._socketCleanup) this._socketCleanup();
1141
+ this._socketCleanup = undefined;
1142
+ this.socket = null;
1143
+ this._connected = false;
1144
+ }
1145
+
1146
+ private _finishResponse() {
1147
+ // Release socket back to agent
1148
+ const agent = this._options.agent === false ? new Agent() : (this._options.agent instanceof Agent ? this._options.agent : globalAgent);
1149
+ const socket = this.socket;
1150
+ this._cleanupSocket();
1151
+ if (socket) agent.releaseSocket(socket, this._options);
1152
+ }
1153
+
1154
+ private _flushPendingWrites() {
1155
+ if (!this.socket) return;
1156
+ if (!this.headersSent) this._sendRequest();
1157
+
1158
+ // If we are waiting for 100-continue, don't flush yet
1159
+ if (this._expectContinue && !this._continueReceived) {
1160
+ return;
1161
+ }
1162
+
1163
+ const writes = this._pendingWrites;
1164
+ this._pendingWrites = [];
1165
+ for (const pending of writes) {
1166
+ this._write(pending.chunk, pending.encoding, pending.callback);
1167
+ }
1168
+ if (this._ended) {
1169
+ this._finishRequest();
1170
+ }
1171
+ }
1172
+
1173
+ private _finishRequest() {
1174
+ if (!this._ended) return;
1175
+ super.end();
1176
+ }
1177
+
1178
+ private _sendRequest() {
1179
+ debugLog(`ClientRequest._sendRequest: headersSent=${this.headersSent}, socket=${!!this.socket}`);
1180
+ if (this.headersSent) return;
1181
+
1182
+ if (!this.hasHeader('host')) {
1183
+ this.setHeader('Host', this.host);
1184
+ }
1185
+
1186
+ const firstLine = `${this.method} ${this.path} HTTP/1.1`;
1187
+ debugLog(`ClientRequest._sendRequest: sending firstLine=${firstLine}`);
1188
+ this._sendHeaders(firstLine);
1189
+ }
1190
+
1191
+ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void) {
1192
+ if (!this._connected) {
1193
+ this._pendingWrites.push({ chunk, encoding, callback });
1194
+ return;
1195
+ }
1196
+ if (!this.headersSent) this._sendRequest();
1197
+ super._write(chunk, encoding, callback);
1198
+ }
1199
+
1200
+ write(chunk: any, encoding?: any, callback?: any): boolean {
1201
+ if (!this._connected) {
1202
+ this._pendingWrites.push({ chunk, encoding, callback });
1203
+ return true;
1204
+ }
1205
+ if (!this.headersSent) this._sendRequest();
1206
+ return super.write(chunk, encoding, callback);
1207
+ }
1208
+
1209
+ end(chunk?: any, encoding?: any, callback?: any): this {
1210
+ debugLog(`ClientRequest.end() called, connected=${this._connected}, headersSent=${this.headersSent}`);
1211
+ if (chunk) {
1212
+ this.write(chunk, encoding);
1213
+ }
1214
+ this._ended = true;
1215
+
1216
+ // If connected, we can send request and end immediately
1217
+ if (this._connected) {
1218
+ if (!this.headersSent) {
1219
+ this._sendRequest();
1220
+ }
1221
+ // Call super.end only when connected
1222
+ super.end(undefined, undefined, callback);
1223
+ } else {
1224
+ // Socket not connected yet - _flushPendingWrites will handle ending
1225
+ // Store callback if provided
1226
+ if (callback) {
1227
+ this.once('finish', callback);
1228
+ }
1229
+ }
1230
+ return this;
1231
+ }
1232
+
1233
+ public abort(): void {
1234
+ if (this.aborted) return;
1235
+ this.aborted = true;
1236
+ this.emit('abort');
1237
+ this.destroy();
1238
+ }
1239
+
1240
+ public flushHeaders(): void {
1241
+ if (this._connected && !this.headersSent) {
1242
+ this._sendRequest();
1243
+ }
1244
+ }
1245
+ }
1246
+
1247
+ // Overloaded signatures for createServer (matching Node.js)
1248
+ export function createServer(requestListener?: (req: IncomingMessage, res: ServerResponse) => void): Server;
1249
+ export function createServer(options: ServerOptions, requestListener?: (req: IncomingMessage, res: ServerResponse) => void): Server;
1250
+ export function createServer(
1251
+ optionsOrListener?: ServerOptions | ((req: IncomingMessage, res: ServerResponse) => void),
1252
+ requestListener?: (req: IncomingMessage, res: ServerResponse) => void
1253
+ ): Server {
1254
+ return new Server(optionsOrListener as any, requestListener);
1255
+ }
1256
+
1257
+ export function request(
1258
+ urlOrOptions: string | URL | RequestOptions,
1259
+ optionsOrCallback?: RequestOptions | ((res: IncomingMessage) => void),
1260
+ callback?: (res: IncomingMessage) => void
1261
+ ): ClientRequest {
1262
+ let opts: RequestOptions = {};
1263
+ let cb: ((res: IncomingMessage) => void) | undefined = callback;
1264
+
1265
+ if (typeof urlOrOptions === 'string') {
1266
+ const url = new URL(urlOrOptions);
1267
+ opts = {
1268
+ protocol: url.protocol,
1269
+ hostname: url.hostname,
1270
+ path: url.pathname + url.search,
1271
+ port: url.port ? parseInt(url.port) : undefined
1272
+ };
1273
+ } else if (urlOrOptions instanceof URL) {
1274
+ opts = {
1275
+ protocol: urlOrOptions.protocol,
1276
+ hostname: urlOrOptions.hostname,
1277
+ path: urlOrOptions.pathname + urlOrOptions.search,
1278
+ port: urlOrOptions.port ? parseInt(urlOrOptions.port) : undefined
1279
+ };
1280
+ } else {
1281
+ opts = urlOrOptions;
1282
+ }
1283
+
1284
+ // Handle (url, options, callback) or (url, callback) signatures
1285
+ if (typeof optionsOrCallback === 'function') {
1286
+ cb = optionsOrCallback;
1287
+ } else if (optionsOrCallback) {
1288
+ // Merge options
1289
+ opts = { ...opts, ...optionsOrCallback };
1290
+ }
1291
+
1292
+ return new ClientRequest(opts, cb);
1293
+ }
1294
+
1295
+ export function get(
1296
+ urlOrOptions: string | URL | RequestOptions,
1297
+ optionsOrCallback?: RequestOptions | ((res: IncomingMessage) => void),
1298
+ callback?: (res: IncomingMessage) => void
1299
+ ): ClientRequest {
1300
+ const req = request(urlOrOptions, optionsOrCallback, callback);
1301
+ req.end();
1302
+ return req;
1303
+ }