From 0ab2852470416954d5ce6c82617f1657ea0a3c60 Mon Sep 17 00:00:00 2001 From: Artur K Date: Sat, 11 Sep 2021 14:55:09 +0300 Subject: [PATCH 1/9] http: limit requests per connection --- lib/_http_outgoing.js | 1 + lib/_http_server.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 25fe8fb1ede73f..32d52ccce303dc 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -134,6 +134,7 @@ function OutgoingMessage() { this._header = null; this[kOutHeaders] = null; + this._maxRequestsPerSocket = null; this._keepAliveTimeout = 0; this._onPendingData = nop; diff --git a/lib/_http_server.js b/lib/_http_server.js index bafd615a0fdc60..e66d8e6df73648 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -394,6 +394,7 @@ function Server(options, requestListener) { this.timeout = 0; this.keepAliveTimeout = 5000; this.maxHeadersCount = null; + this.maxRequestsPerSocket = null; this.headersTimeout = 60 * 1000; // 60 seconds this.requestTimeout = 0; } @@ -485,6 +486,7 @@ function connectionListenerInternal(server, socket) { // need to pause TCP socket/HTTP parser, and wait until the data will be // sent to the client. outgoingData: 0, + requestsCount: 0, keepAliveTimeoutSet: false }; state.onData = socketOnData.bind(undefined, @@ -875,6 +877,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { const res = new server[kServerResponse](req); res._keepAliveTimeout = server.keepAliveTimeout; + res._maxRequestsPerSocket = server.maxRequestsPerSocket; res._onPendingData = updateOutgoingData.bind(undefined, socket, state); @@ -903,6 +906,16 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { resOnFinish.bind(undefined, req, res, socket, state, server)); + if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1 + && typeof server.maxRequestsPerSocket === 'number' + && server.maxRequestsPerSocket > ++state.requestsCount) { + res.shouldKeepAlive = false; + res.writeHead(503, { + 'Connection': 'close' + }); + res.end(); + } + if (req.headers.expect !== undefined && (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) { if (RegExpPrototypeTest(continueExpression, req.headers.expect)) { From dc611ee3a6a23f3779fe388a8582322d7df1047d Mon Sep 17 00:00:00 2001 From: Artur K Date: Sat, 11 Sep 2021 16:44:56 +0300 Subject: [PATCH 2/9] http: fix condition --- lib/_http_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/_http_server.js b/lib/_http_server.js index e66d8e6df73648..0c2e8b0dd5edc7 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -908,7 +908,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1 && typeof server.maxRequestsPerSocket === 'number' - && server.maxRequestsPerSocket > ++state.requestsCount) { + && server.maxRequestsPerSocket < ++state.requestsCount) { res.shouldKeepAlive = false; res.writeHead(503, { 'Connection': 'close' From 8f55f74667be144e8e2b63db60231f895c804abe Mon Sep 17 00:00:00 2001 From: Artur K Date: Sat, 11 Sep 2021 17:07:01 +0300 Subject: [PATCH 3/9] http: close on last request --- lib/_http_server.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/_http_server.js b/lib/_http_server.js index 0c2e8b0dd5edc7..355119b8412a86 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -906,14 +906,15 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { resOnFinish.bind(undefined, req, res, socket, state, server)); - if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1 - && typeof server.maxRequestsPerSocket === 'number' - && server.maxRequestsPerSocket < ++state.requestsCount) { - res.shouldKeepAlive = false; - res.writeHead(503, { - 'Connection': 'close' - }); + if (typeof server.maxRequestsPerSocket === 'number' + && (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) { + + if (server.maxRequestsPerSocket < ++state.requestsCount) { + res.writeHead(503); res.end(); + } + + res.shouldKeepAlive = server.maxRequestsPerSocket > state.requestsCount } if (req.headers.expect !== undefined && From 3dd4770562d3ecc724a90e21ec971a2544a4c2a4 Mon Sep 17 00:00:00 2001 From: Artur K Date: Sun, 12 Sep 2021 16:14:22 +0300 Subject: [PATCH 4/9] http: add test, fix write after end --- lib/_http_server.js | 55 +++++---- .../test-http-keep-alive-max-requests.js | 114 ++++++++++++++++++ 2 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 test/parallel/test-http-keep-alive-max-requests.js diff --git a/lib/_http_server.js b/lib/_http_server.js index 355119b8412a86..1dd62942245425 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -906,39 +906,46 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { resOnFinish.bind(undefined, req, res, socket, state, server)); - if (typeof server.maxRequestsPerSocket === 'number' - && (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) { + let handled = false; - if (server.maxRequestsPerSocket < ++state.requestsCount) { - res.writeHead(503); - res.end(); + if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) { + if (typeof server.maxRequestsPerSocket === 'number') { + state.requestsCount++ + res.shouldKeepAlive = server.maxRequestsPerSocket > state.requestsCount } - res.shouldKeepAlive = server.maxRequestsPerSocket > state.requestsCount - } - - if (req.headers.expect !== undefined && - (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) { - if (RegExpPrototypeTest(continueExpression, req.headers.expect)) { - res._expect_continue = true; - - if (server.listenerCount('checkContinue') > 0) { - server.emit('checkContinue', req, res); + if (typeof server.maxRequestsPerSocket === 'number' + && (server.maxRequestsPerSocket < state.requestsCount)) { + handled = true + + res.writeHead(503); + res.end(); + } else if (req.headers.expect !== undefined) { + handled = true + + if (RegExpPrototypeTest(continueExpression, req.headers.expect)) { + res._expect_continue = true; + + if (server.listenerCount('checkContinue') > 0) { + server.emit('checkContinue', req, res); + } else { + res.writeContinue(); + server.emit('request', req, res); + } + } else if (server.listenerCount('checkExpectation') > 0) { + server.emit('checkExpectation', req, res); } else { - res.writeContinue(); - server.emit('request', req, res); + res.writeHead(417); + res.end(); } - } else if (server.listenerCount('checkExpectation') > 0) { - server.emit('checkExpectation', req, res); - } else { - res.writeHead(417); - res.end(); } - } else { - req.on('end', clearRequestTimeout); + } + if(!handled) { + req.on('end', clearRequestTimeout); server.emit('request', req, res); } + return 0; // No special treatment. } diff --git a/test/parallel/test-http-keep-alive-max-requests.js b/test/parallel/test-http-keep-alive-max-requests.js new file mode 100644 index 00000000000000..a06161b94b61b8 --- /dev/null +++ b/test/parallel/test-http-keep-alive-max-requests.js @@ -0,0 +1,114 @@ +'use strict'; + +const net = require('net'); +const http = require('http'); +const assert = require('assert'); +const common = require('../common'); + +const bodySent = 'This is my request'; + +function assertResponse(headers, body, expectClosed) { + if (expectClosed) { + assert.match(headers, /Connection: close\r\n/m); + assert(headers.search(/Keep-Alive: timeout=5, max=3\r\n/m) === -1); + assert.match(body, /Hello World!/m); + } else { + assert.match(headers, /Connection: keep-alive\r\n/m); + assert.match(headers, /Keep-Alive: timeout=5, max=3\r\n/m); + assert.match(body, /Hello World!/m); + } +} + +function writeRequest(socket, withBody) { + if (withBody) { + socket.write('POST / HTTP/1.1\r\n'); + socket.write('Connection: keep-alive\r\n'); + socket.write('Content-Type: text/plain\r\n'); + socket.write(`Content-Length: ${bodySent.length}\r\n\r\n`); + socket.write(`${bodySent}\r\n`); + socket.write('\r\n\r\n') + } else { + socket.write('GET / HTTP/1.1\r\n'); + socket.write('Connection: keep-alive\r\n'); + socket.write('\r\n\r\n') + } +} + +const server = http.createServer(function (req, res) { + let body = '' + req.on('data', (data) => { + body += data + }); + + req.on('end', () => { + if (req.method === 'POST') { + assert(bodySent === body) + } + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write('Hello World!'); + res.end(); + }) +}) + +server.maxRequestsPerSocket = 3; +server.listen(0, common.mustCall((res) => { + const socket = net.createConnection( + { port: server.address().port }, + common.mustCall(() => { + writeRequest(socket) + writeRequest(socket) + + const anotherSocket = net.createConnection( + { port: server.address().port }, + common.mustCall(() => { + writeRequest(anotherSocket) + + let anotherBuffer = '' + let lastWritten = false; + anotherSocket.setEncoding('utf8'); + anotherSocket.on('data', (data) => { + anotherBuffer += data; + + if (anotherBuffer.endsWith('\r\n\r\n')) { + if (lastWritten) { + anotherSocket.end() + } else { + writeRequest(anotherSocket); + lastWritten = true; + } + } + }); + + anotherSocket.on('end', common.mustCall(() => { + const anoterResponses = anotherBuffer.trim().split('\r\n\r\n'); + + assertResponse(anoterResponses[0], anoterResponses[1], false) + assertResponse(anoterResponses[2], anoterResponses[3], false) + + // Add two additional requests to two previous on the first socket + writeRequest(socket, true) + writeRequest(socket, true) + + let buffer = ''; + socket.setEncoding('utf8'); + socket.on('data', (data) => { + buffer += data; + }); + + socket.on('end', common.mustCall(() => { + const responses = buffer.trim().split('\r\n\r\n'); + // We sent more requests than allowed per socket, + // but we get only the allowed number of responses & headers + assert(responses.length === server.maxRequestsPerSocket * 2); + + assertResponse(responses[0], responses[1], false) + assertResponse(responses[2], responses[3], false) + assertResponse(responses[4], responses[5], true) + + server.close(); + })); + })); + })); + }) + ); +})); \ No newline at end of file From 719eba93c23ca8d0fc6540d15e8840d7e3c398e4 Mon Sep 17 00:00:00 2001 From: Artur K Date: Sun, 12 Sep 2021 22:01:05 +0300 Subject: [PATCH 5/9] http: refactor test --- .../test-http-keep-alive-max-requests.js | 117 +++++++++--------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/test/parallel/test-http-keep-alive-max-requests.js b/test/parallel/test-http-keep-alive-max-requests.js index a06161b94b61b8..9ae30480b9da36 100644 --- a/test/parallel/test-http-keep-alive-max-requests.js +++ b/test/parallel/test-http-keep-alive-max-requests.js @@ -4,6 +4,7 @@ const net = require('net'); const http = require('http'); const assert = require('assert'); const common = require('../common'); +const { mustCall } = require('../common'); const bodySent = 'This is my request'; @@ -50,65 +51,63 @@ const server = http.createServer(function (req, res) { }) }) +function initialRequests(socket, numberOfRequests, cb) { + let buffer = ''; + + writeRequest(socket) + + socket.on('data', (data) => { + buffer += data; + + if (buffer.endsWith('\r\n\r\n')) { + if (--numberOfRequests === 0) { + socket.removeAllListeners('data'); + cb(); + } else { + const [headers, body] = buffer.trim().split('\r\n\r\n'); + assertResponse(headers, body) + buffer = ''; + writeRequest(socket, true) + } + } + }); +} + + server.maxRequestsPerSocket = 3; server.listen(0, common.mustCall((res) => { - const socket = net.createConnection( - { port: server.address().port }, - common.mustCall(() => { - writeRequest(socket) - writeRequest(socket) - - const anotherSocket = net.createConnection( - { port: server.address().port }, - common.mustCall(() => { - writeRequest(anotherSocket) - - let anotherBuffer = '' - let lastWritten = false; - anotherSocket.setEncoding('utf8'); - anotherSocket.on('data', (data) => { - anotherBuffer += data; - - if (anotherBuffer.endsWith('\r\n\r\n')) { - if (lastWritten) { - anotherSocket.end() - } else { - writeRequest(anotherSocket); - lastWritten = true; - } - } - }); - - anotherSocket.on('end', common.mustCall(() => { - const anoterResponses = anotherBuffer.trim().split('\r\n\r\n'); - - assertResponse(anoterResponses[0], anoterResponses[1], false) - assertResponse(anoterResponses[2], anoterResponses[3], false) - - // Add two additional requests to two previous on the first socket - writeRequest(socket, true) - writeRequest(socket, true) - - let buffer = ''; - socket.setEncoding('utf8'); - socket.on('data', (data) => { - buffer += data; - }); - - socket.on('end', common.mustCall(() => { - const responses = buffer.trim().split('\r\n\r\n'); - // We sent more requests than allowed per socket, - // but we get only the allowed number of responses & headers - assert(responses.length === server.maxRequestsPerSocket * 2); - - assertResponse(responses[0], responses[1], false) - assertResponse(responses[2], responses[3], false) - assertResponse(responses[4], responses[5], true) - - server.close(); - })); - })); - })); - }) - ); + const socket = new net.Socket(); + const anotherSocket = new net.Socket(); + + socket.on('end', mustCall(() => { + server.close(); + })); + + socket.on('ready', common.mustCall(() => { + // Do two of 3 requests and ensure they still alive + initialRequests(socket, 2, common.mustCall(() => { + anotherSocket.connect({ port: server.address().port }); + })) + })); + + anotherSocket.on('ready', common.mustCall(() => { + // Do another 2 requests with another socket, enusre that this will not affect the first socket + initialRequests(anotherSocket, 2, common.mustCall(() => { + let buffer = ''; + + // Send the rest of the calls to the first socket and see connection is closed + socket.on('data', common.mustCall((data) => { + buffer += data; + + if (buffer.endsWith('\r\n\r\n')) { + const [headers, body] = buffer.trim().split('\r\n\r\n'); + assertResponse(headers, body, true); + } + })); + + writeRequest(socket, true); + })); + })); + + socket.connect({ port: server.address().port }); })); \ No newline at end of file From b1d70f652facd4896849203bf50a6d07432e83b1 Mon Sep 17 00:00:00 2001 From: Artur K Date: Sun, 12 Sep 2021 22:45:21 +0300 Subject: [PATCH 6/9] http: fix 503 on max reached --- lib/_http_outgoing.js | 1 + lib/_http_server.js | 6 +- .../test-http-keep-alive-max-requests.js | 5 +- ...t-http-keep-alive-pipeline-max-requests.js | 86 +++++++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 test/parallel/test-http-keep-alive-pipeline-max-requests.js diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 32d52ccce303dc..ecf46143466d37 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -113,6 +113,7 @@ function OutgoingMessage() { this._last = false; this.chunkedEncoding = false; this.shouldKeepAlive = true; + this.maxRequestsOnConnectionReached = false; this._defaultKeepAlive = true; this.useChunkedEncodingByDefault = true; this.sendDate = false; diff --git a/lib/_http_server.js b/lib/_http_server.js index 1dd62942245425..839c8bd2d93128 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -910,13 +910,13 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) { if (typeof server.maxRequestsPerSocket === 'number') { - state.requestsCount++ - res.shouldKeepAlive = server.maxRequestsPerSocket > state.requestsCount + state.requestsCount++; + res.maxRequestsOnConnectionReached = server.maxRequestsPerSocket <= state.requestsCount } if (typeof server.maxRequestsPerSocket === 'number' && (server.maxRequestsPerSocket < state.requestsCount)) { - handled = true + handled = true; res.writeHead(503); res.end(); diff --git a/test/parallel/test-http-keep-alive-max-requests.js b/test/parallel/test-http-keep-alive-max-requests.js index 9ae30480b9da36..6bacc1a68a7356 100644 --- a/test/parallel/test-http-keep-alive-max-requests.js +++ b/test/parallel/test-http-keep-alive-max-requests.js @@ -4,7 +4,6 @@ const net = require('net'); const http = require('http'); const assert = require('assert'); const common = require('../common'); -const { mustCall } = require('../common'); const bodySent = 'This is my request'; @@ -79,12 +78,12 @@ server.listen(0, common.mustCall((res) => { const socket = new net.Socket(); const anotherSocket = new net.Socket(); - socket.on('end', mustCall(() => { + socket.on('end', common.mustCall(() => { server.close(); })); socket.on('ready', common.mustCall(() => { - // Do two of 3 requests and ensure they still alive + // Do 2 of 3 allowed requests and ensure they still alive initialRequests(socket, 2, common.mustCall(() => { anotherSocket.connect({ port: server.address().port }); })) diff --git a/test/parallel/test-http-keep-alive-pipeline-max-requests.js b/test/parallel/test-http-keep-alive-pipeline-max-requests.js new file mode 100644 index 00000000000000..7f3ba7bf5f22b5 --- /dev/null +++ b/test/parallel/test-http-keep-alive-pipeline-max-requests.js @@ -0,0 +1,86 @@ +'use strict'; + +const net = require('net'); +const http = require('http'); +const assert = require('assert'); +const common = require('../common'); + +const bodySent = 'This is my request'; + +function assertResponse(headers, body, expectClosed) { + if (expectClosed) { + assert.match(headers, /Connection: close\r\n/m); + assert(headers.search(/Keep-Alive: timeout=5, max=3\r\n/m) === -1); + assert.match(body, /Hello World!/m); + } else { + assert.match(headers, /Connection: keep-alive\r\n/m); + assert.match(headers, /Keep-Alive: timeout=5, max=3\r\n/m); + assert.match(body, /Hello World!/m); + } +} + +function writeRequest(socket) { + socket.write('POST / HTTP/1.1\r\n'); + socket.write('Connection: keep-alive\r\n'); + socket.write('Content-Type: text/plain\r\n'); + socket.write(`Content-Length: ${bodySent.length}\r\n\r\n`); + socket.write(`${bodySent}\r\n`); + socket.write('\r\n\r\n') +} + +const server = http.createServer(function (req, res) { + let body = '' + req.on('data', (data) => { + body += data + }); + + req.on('end', () => { + if (req.method === 'POST') { + assert(bodySent === body) + } + + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write('Hello World!'); + res.end(); + }) +}) + +server.maxRequestsPerSocket = 3; + +server.listen(0, common.mustCall((res) => { + const socket = new net.Socket(); + + socket.on('end', common.mustCall(() => { + server.close(); + })); + + socket.on('ready', common.mustCall(() => { + writeRequest(socket); + writeRequest(socket); + writeRequest(socket); + writeRequest(socket); + })); + + let buffer = '' + + socket.on('data', (data) => { + buffer += data; + + const responseParts = buffer.trim().split('\r\n\r\n'); + + if (responseParts.length === 8) { + assertResponse(responseParts[0], responseParts[1]); + assertResponse(responseParts[2], responseParts[3]); + assertResponse(responseParts[4], responseParts[5], true); + + assert.match(responseParts[6], /HTTP\/1.1 503 Service Unavailable/m) + assert.match(responseParts[6], /Connection: close\r\n/m); + assert(responseParts[6].search(/Keep-Alive: timeout=5, max=3\r\n/m) === -1); + assert(responseParts[7].search(/Hello World!/m) === -1); + + socket.end(); + } + }) + + socket.connect({ port: server.address().port }); +})); \ No newline at end of file From 729fd5ab45f55fa20566c3a04ea41c02ec3d147d Mon Sep 17 00:00:00 2001 From: Artur K Date: Mon, 13 Sep 2021 00:26:30 +0300 Subject: [PATCH 7/9] http: fix lint --- lib/_http_server.js | 21 +++++----- .../test-http-keep-alive-max-requests.js | 42 ++++++++++--------- ...t-http-keep-alive-pipeline-max-requests.js | 34 +++++++-------- 3 files changed, 51 insertions(+), 46 deletions(-) diff --git a/lib/_http_server.js b/lib/_http_server.js index 839c8bd2d93128..758bb8c9267e1e 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -911,21 +911,22 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) { if (typeof server.maxRequestsPerSocket === 'number') { state.requestsCount++; - res.maxRequestsOnConnectionReached = server.maxRequestsPerSocket <= state.requestsCount + res.maxRequestsOnConnectionReached = ( + server.maxRequestsPerSocket <= state.requestsCount); } - if (typeof server.maxRequestsPerSocket === 'number' - && (server.maxRequestsPerSocket < state.requestsCount)) { - handled = true; + if (typeof server.maxRequestsPerSocket === 'number' && + (server.maxRequestsPerSocket < state.requestsCount)) { + handled = true; - res.writeHead(503); - res.end(); + res.writeHead(503); + res.end(); } else if (req.headers.expect !== undefined) { - handled = true - + handled = true; + if (RegExpPrototypeTest(continueExpression, req.headers.expect)) { res._expect_continue = true; - + if (server.listenerCount('checkContinue') > 0) { server.emit('checkContinue', req, res); } else { @@ -941,7 +942,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { } } - if(!handled) { + if (!handled) { req.on('end', clearRequestTimeout); server.emit('request', req, res); } diff --git a/test/parallel/test-http-keep-alive-max-requests.js b/test/parallel/test-http-keep-alive-max-requests.js index 6bacc1a68a7356..d1fbc6a7fd0842 100644 --- a/test/parallel/test-http-keep-alive-max-requests.js +++ b/test/parallel/test-http-keep-alive-max-requests.js @@ -1,16 +1,16 @@ 'use strict'; +const common = require('../common'); const net = require('net'); const http = require('http'); const assert = require('assert'); -const common = require('../common'); const bodySent = 'This is my request'; function assertResponse(headers, body, expectClosed) { if (expectClosed) { assert.match(headers, /Connection: close\r\n/m); - assert(headers.search(/Keep-Alive: timeout=5, max=3\r\n/m) === -1); + assert.strictEqual(headers.search(/Keep-Alive: timeout=5, max=3\r\n/m), -1); assert.match(body, /Hello World!/m); } else { assert.match(headers, /Connection: keep-alive\r\n/m); @@ -26,34 +26,34 @@ function writeRequest(socket, withBody) { socket.write('Content-Type: text/plain\r\n'); socket.write(`Content-Length: ${bodySent.length}\r\n\r\n`); socket.write(`${bodySent}\r\n`); - socket.write('\r\n\r\n') + socket.write('\r\n\r\n'); } else { socket.write('GET / HTTP/1.1\r\n'); socket.write('Connection: keep-alive\r\n'); - socket.write('\r\n\r\n') + socket.write('\r\n\r\n'); } } -const server = http.createServer(function (req, res) { - let body = '' +const server = http.createServer((req, res) => { + let body = ''; req.on('data', (data) => { - body += data + body += data; }); req.on('end', () => { if (req.method === 'POST') { - assert(bodySent === body) + assert.strictEqual(bodySent, body); } - res.writeHead(200, {'Content-Type': 'text/plain'}); + res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('Hello World!'); res.end(); - }) -}) + }); +}); function initialRequests(socket, numberOfRequests, cb) { let buffer = ''; - writeRequest(socket) + writeRequest(socket); socket.on('data', (data) => { buffer += data; @@ -64,9 +64,9 @@ function initialRequests(socket, numberOfRequests, cb) { cb(); } else { const [headers, body] = buffer.trim().split('\r\n\r\n'); - assertResponse(headers, body) + assertResponse(headers, body); buffer = ''; - writeRequest(socket, true) + writeRequest(socket, true); } } }); @@ -85,22 +85,26 @@ server.listen(0, common.mustCall((res) => { socket.on('ready', common.mustCall(() => { // Do 2 of 3 allowed requests and ensure they still alive initialRequests(socket, 2, common.mustCall(() => { - anotherSocket.connect({ port: server.address().port }); - })) + anotherSocket.connect({ port: server.address().port }); + })); })); anotherSocket.on('ready', common.mustCall(() => { - // Do another 2 requests with another socket, enusre that this will not affect the first socket + // Do another 2 requests with another socket + // enusre that this will not affect the first socket initialRequests(anotherSocket, 2, common.mustCall(() => { let buffer = ''; - // Send the rest of the calls to the first socket and see connection is closed + // Send the rest of the calls to the first socket + // and see connection is closed socket.on('data', common.mustCall((data) => { buffer += data; if (buffer.endsWith('\r\n\r\n')) { const [headers, body] = buffer.trim().split('\r\n\r\n'); assertResponse(headers, body, true); + anotherSocket.end(); + socket.end(); } })); @@ -109,4 +113,4 @@ server.listen(0, common.mustCall((res) => { })); socket.connect({ port: server.address().port }); -})); \ No newline at end of file +})); diff --git a/test/parallel/test-http-keep-alive-pipeline-max-requests.js b/test/parallel/test-http-keep-alive-pipeline-max-requests.js index 7f3ba7bf5f22b5..0de00b7a04910b 100644 --- a/test/parallel/test-http-keep-alive-pipeline-max-requests.js +++ b/test/parallel/test-http-keep-alive-pipeline-max-requests.js @@ -1,16 +1,16 @@ 'use strict'; +const common = require('../common'); const net = require('net'); const http = require('http'); const assert = require('assert'); -const common = require('../common'); const bodySent = 'This is my request'; function assertResponse(headers, body, expectClosed) { if (expectClosed) { assert.match(headers, /Connection: close\r\n/m); - assert(headers.search(/Keep-Alive: timeout=5, max=3\r\n/m) === -1); + assert.strictEqual(headers.search(/Keep-Alive: timeout=5, max=3\r\n/m), -1); assert.match(body, /Hello World!/m); } else { assert.match(headers, /Connection: keep-alive\r\n/m); @@ -25,25 +25,25 @@ function writeRequest(socket) { socket.write('Content-Type: text/plain\r\n'); socket.write(`Content-Length: ${bodySent.length}\r\n\r\n`); socket.write(`${bodySent}\r\n`); - socket.write('\r\n\r\n') + socket.write('\r\n\r\n'); } -const server = http.createServer(function (req, res) { - let body = '' +const server = http.createServer((req, res) => { + let body = ''; req.on('data', (data) => { - body += data + body += data; }); req.on('end', () => { if (req.method === 'POST') { - assert(bodySent === body) + assert.strictEqual(bodySent, body); } - res.writeHead(200, {'Content-Type': 'text/plain'}); + res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('Hello World!'); res.end(); - }) -}) + }); +}); server.maxRequestsPerSocket = 3; @@ -61,7 +61,7 @@ server.listen(0, common.mustCall((res) => { writeRequest(socket); })); - let buffer = '' + let buffer = ''; socket.on('data', (data) => { buffer += data; @@ -73,14 +73,14 @@ server.listen(0, common.mustCall((res) => { assertResponse(responseParts[2], responseParts[3]); assertResponse(responseParts[4], responseParts[5], true); - assert.match(responseParts[6], /HTTP\/1.1 503 Service Unavailable/m) + assert.match(responseParts[6], /HTTP\/1\.1 503 Service Unavailable/m); assert.match(responseParts[6], /Connection: close\r\n/m); - assert(responseParts[6].search(/Keep-Alive: timeout=5, max=3\r\n/m) === -1); - assert(responseParts[7].search(/Hello World!/m) === -1); - + assert.strictEqual(responseParts[6].search(/Keep-Alive: timeout=5, max=3\r\n/m), -1); + assert.strictEqual(responseParts[7].search(/Hello World!/m), -1); + socket.end(); } - }) + }); socket.connect({ port: server.address().port }); -})); \ No newline at end of file +})); From 063a83cc20c4be55633215a7b7c0d66ce7fb70cf Mon Sep 17 00:00:00 2001 From: Artur K Date: Mon, 13 Sep 2021 00:42:59 +0300 Subject: [PATCH 8/9] http: max requests per socket docs --- doc/api/http.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/api/http.md b/doc/api/http.md index f7484baac2eb1a..24303dfa1a7041 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -1352,6 +1352,22 @@ By default, the Server does not timeout sockets. However, if a callback is assigned to the Server's `'timeout'` event, timeouts must be handled explicitly. +### `server.maxRequestsPerSocket` + + +* {number} Requests per socket. **Default:** null (no limit) + +The maximum number of requests socket can handle +before closing keep alive connection. + +A value of `null` will disable the limit. + +When limit is reach it will set `Connection` header value to `closed`, +but will not actually close the connection, subsequent requests sent +after the limit is reached will get `503 Service Unavailable` as a response. + ### `server.timeout`