From 32192b36a16d18b94569a6299dfd3460447dc4a7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 29 May 2025 22:41:38 -0700 Subject: [PATCH] Add test-http and implement basic Agent queue --- src/js/node/_http_agent.ts | 23 ++++++ src/js/node/_http_client.ts | 39 +++++++--- test/js/node/parallel/test-http.js | 114 +++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 test/js/node/parallel/test-http.js diff --git a/src/js/node/_http_agent.ts b/src/js/node/_http_agent.ts index c7e3c441ec..824e8dd830 100644 --- a/src/js/node/_http_agent.ts +++ b/src/js/node/_http_agent.ts @@ -72,6 +72,10 @@ function Agent(options = kEmptyObject) { this.totalSocketCount = 0; this.defaultPort = options.defaultPort || 80; this.protocol = options.protocol || "http:"; + + // Minimal queue implementation used for Node.js tests. + this._queue = []; + this._active = 0; } $toClass(Agent, "Agent", EventEmitter); @@ -142,6 +146,25 @@ Agent.prototype.destroy = function () { $debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.destroy is a no-op"); }; +// Queue management helpers for limited sockets +Agent.prototype._enqueue = function (req, start) { + if (this._active < (this.maxSockets || Infinity)) { + this._active++; + start(); + } else { + this._queue.push([req, start]); + } +}; + +Agent.prototype._requestFinished = function () { + if (this._active > 0) this._active--; + const next = this._queue.shift(); + if (next) { + this._active++; + next[1](); + } +}; + var globalAgent = new Agent(); const http_agent_exports = { diff --git a/src/js/node/_http_client.ts b/src/js/node/_http_client.ts index 7402d1d428..b9cc02a6b0 100644 --- a/src/js/node/_http_client.ts +++ b/src/js/node/_http_client.ts @@ -248,6 +248,7 @@ function ClientRequest(input, options, cb) { process.nextTick(emitAbortNextTick, this); this[abortedSymbol] = true; } + finishAgentRequest(); }; let fetching = false; @@ -390,6 +391,7 @@ function ClientRequest(input, options, cb) { })); setIsNextIncomingMessageHTTPS(prevIsHTTPS); res.req = this; + res.on("end", finishAgentRequest); let timer; res.setTimeout = (msecs, callback) => { if (timer) { @@ -463,6 +465,7 @@ function ClientRequest(input, options, cb) { } catch (_err) { void _err; } + finishAgentRequest(); }) .finally(() => { if (!keepOpen) { @@ -540,6 +543,13 @@ function ClientRequest(input, options, cb) { let onEnd = () => {}; let handleResponse: (() => void) | undefined = () => {}; + let finishedAgent = false; + const finishAgentRequest = () => { + if (!finishedAgent) { + finishedAgent = true; + this[kAgent]?._requestFinished?.(); + } + }; const send = () => { this.finished = true; @@ -548,16 +558,25 @@ function ClientRequest(input, options, cb) { var body = this[kBodyChunks] && this[kBodyChunks].length > 1 ? new Blob(this[kBodyChunks]) : this[kBodyChunks]?.[0]; - try { - startFetch(body); - onEnd = () => { - handleResponse?.(); - }; - } catch (err) { - if (!!$debug) globalReportError(err); - this.emit("error", err); - } finally { - process.nextTick(maybeEmitFinish.bind(this)); + const doStart = () => { + try { + startFetch(body); + onEnd = () => { + handleResponse?.(); + }; + } catch (err) { + if (!!$debug) globalReportError(err); + this.emit("error", err); + finishAgentRequest(); + } finally { + process.nextTick(maybeEmitFinish.bind(this)); + } + }; + + if (this[kAgent]?._enqueue) { + this[kAgent]._enqueue(this, doStart); + } else { + doStart(); } }; diff --git a/test/js/node/parallel/test-http.js b/test/js/node/parallel/test-http.js new file mode 100644 index 0000000000..1829b57b37 --- /dev/null +++ b/test/js/node/parallel/test-http.js @@ -0,0 +1,114 @@ +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const url = require('url'); + +const expectedRequests = ['/hello', '/there', '/world']; + +const server = http.Server(common.mustCall((req, res) => { + assert.strictEqual(expectedRequests.shift(), req.url); + + switch (req.url) { + case '/hello': + assert.strictEqual(req.method, 'GET'); + assert.strictEqual(req.headers.accept, '*/*'); + assert.strictEqual(req.headers.foo, 'bar'); + assert.strictEqual(req.headers.cookie, 'foo=bar; bar=baz; baz=quux'); + break; + case '/there': + assert.strictEqual(req.method, 'PUT'); + assert.strictEqual(req.headers.cookie, 'node=awesome; ta=da'); + break; + case '/world': + assert.strictEqual(req.method, 'POST'); + assert.strictEqual(req.headers.cookie, 'abc=123; def=456; ghi=789'); + break; + default: + assert(false, `Unexpected request for ${req.url}`); + } + + if (expectedRequests.length === 0) + server.close(); + + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write(`The path was ${url.parse(req.url).pathname}`); + res.end(); + }); + req.resume(); +}, 3)); +server.listen(0); + +server.on('listening', () => { + const agent = new http.Agent({ port: server.address().port, maxSockets: 1 }); + const req = http.get({ + port: server.address().port, + path: '/hello', + headers: { + Accept: '*/*', + Foo: 'bar', + Cookie: [ 'foo=bar', 'bar=baz', 'baz=quux' ] + }, + agent: agent + }, common.mustCall((res) => { + const cookieHeaders = req._header.match(/^Cookie: .+$/img); + assert.deepStrictEqual(cookieHeaders, + ['Cookie: foo=bar; bar=baz; baz=quux']); + assert.strictEqual(res.statusCode, 200); + let body = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { body += chunk; }); + res.on('end', common.mustCall(() => { + assert.strictEqual(body, 'The path was /hello'); + })); + })); + + setTimeout(common.mustCall(() => { + const req = http.request({ + port: server.address().port, + method: 'PUT', + path: '/there', + agent: agent + }, common.mustCall((res) => { + const cookieHeaders = req._header.match(/^Cookie: .+$/img); + assert.deepStrictEqual(cookieHeaders, ['Cookie: node=awesome; ta=da']); + assert.strictEqual(res.statusCode, 200); + let body = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { body += chunk; }); + res.on('end', common.mustCall(() => { + assert.strictEqual(body, 'The path was /there'); + })); + })); + req.setHeader('Cookie', ['node=awesome', 'ta=da']); + req.end(); + }), 1); + + setTimeout(common.mustCall(() => { + const req = http.request({ + port: server.address().port, + method: 'POST', + path: '/world', + headers: [ ['Cookie', 'abc=123'], + ['Cookie', 'def=456'], + ['Cookie', 'ghi=789'], + ['Host', 'example.com'], + ], + agent: agent + }, common.mustCall((res) => { + const cookieHeaders = req._header.match(/^Cookie: .+$/img); + assert.deepStrictEqual(cookieHeaders, + ['Cookie: abc=123', + 'Cookie: def=456', + 'Cookie: ghi=789']); + assert.strictEqual(res.statusCode, 200); + let body = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { body += chunk; }); + res.on('end', common.mustCall(() => { + assert.strictEqual(body, 'The path was /world'); + })); + })); + req.end(); + }), 2); +});