react-native-nitro-net 0.2.0 → 0.3.0

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