http: add optimizeEmptyRequests server option

Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
Co-Authored-By: RafaelGSS <rafael.nunu@hotmail.com>
PR-URL: https://github.com/nodejs/node/pull/59778
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Rafael Gonzaga 2025-10-14 09:17:53 -03:00 committed by GitHub
parent 1072295d26
commit 0c35aaf55f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 118 additions and 0 deletions

View File

@ -3555,6 +3555,9 @@ Found'`.
<!-- YAML
added: v0.1.13
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59778
description: Add optimizeEmptyRequests option.
- version: v24.9.0
pr-url: https://github.com/nodejs/node/pull/59824
description: The `shouldUpgradeCallback` option is now supported.
@ -3660,6 +3663,11 @@ changes:
* `rejectNonStandardBodyWrites` {boolean} If set to `true`, an error is thrown
when writing to an HTTP response which does not have a body.
**Default:** `false`.
* `optimizeEmptyRequests` {boolean} If set to `true`, requests without `Content-Length`
or `Transfer-Encoding` headers (indicating no body) will be initialized with an
already-ended body stream, so they will never emit any stream events
(like `'data'` or `'end'`). You can use `req.readableEnded` to detect this case.
**Default:** `false`.
* `requestListener` {Function}

View File

@ -423,6 +423,15 @@ function _addHeaderLineDistinct(field, value, dest) {
}
}
IncomingMessage.prototype._dumpAndCloseReadable = function _dumpAndCloseReadable() {
this._dumped = true;
this._readableState.ended = true;
this._readableState.endEmitted = true;
this._readableState.destroyed = true;
this._readableState.closed = true;
this._readableState.closeEmitted = true;
};
// Call this instead of resume() if we want to just
// dump all the data to /dev/null

View File

@ -107,6 +107,8 @@ const onResponseFinishChannel = dc.channel('http.server.response.finish');
const kServerResponse = Symbol('ServerResponse');
const kServerResponseStatistics = Symbol('ServerResponseStatistics');
const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
const {
hasObserver,
startPerf,
@ -455,6 +457,11 @@ function storeHTTPOptions(options) {
validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
this.maxHeaderSize = maxHeaderSize;
const optimizeEmptyRequests = options.optimizeEmptyRequests;
if (optimizeEmptyRequests !== undefined)
validateBoolean(optimizeEmptyRequests, 'options.optimizeEmptyRequests');
this[kOptimizeEmptyRequests] = optimizeEmptyRequests || false;
const insecureHTTPParser = options.insecureHTTPParser;
if (insecureHTTPParser !== undefined)
validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
@ -1069,6 +1076,10 @@ function emitCloseNT(self) {
}
}
function hasBodyHeaders(headers) {
return ('content-length' in headers) || ('transfer-encoding' in headers);
}
// The following callback is issued after the headers have been read on a
// new message. In this callback we setup the response object and pass it
// to the user.
@ -1120,6 +1131,19 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
});
}
// Check if we should optimize empty requests (those without Content-Length or Transfer-Encoding headers)
const shouldOptimize = server[kOptimizeEmptyRequests] === true && !hasBodyHeaders(req.headers);
if (shouldOptimize) {
// Fast processing where emitting 'data', 'end' and 'close' events is
// skipped and data is dumped.
// This avoids a lot of unnecessary overhead otherwise introduced by
// stream.Readable life cycle rules. The downside is that this will
// break some servers that read bodies for methods that don't have body headers.
req._dumpAndCloseReadable();
req._read();
}
if (socket._httpMessage) {
// There are already pending outgoing res, append.
state.outgoing.push(res);

View File

@ -0,0 +1,77 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
let reqs = 0;
let optimizedReqs = 0;
const server = http.createServer({
optimizeEmptyRequests: true
}, (req, res) => {
reqs++;
if (req._dumped) {
optimizedReqs++;
req.on('data', common.mustNotCall());
req.on('end', common.mustNotCall());
assert.strictEqual(req._dumped, true);
assert.strictEqual(req.readableEnded, true);
assert.strictEqual(req.destroyed, true);
}
res.writeHead(200);
res.end('ok');
});
server.listen(0, common.mustCall(async () => {
// GET request without Content-Length (should be optimized)
const getRequest = 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n';
await makeRequest(getRequest);
// HEAD request (should always be optimized regardless of headers)
const headRequest = 'HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n';
await makeRequest(headRequest);
// POST request without body headers (should be optimized)
const postWithoutBodyHeaders = 'POST / HTTP/1.1\r\nHost: localhost\r\n\r\n';
await makeRequest(postWithoutBodyHeaders);
// DELETE request without body headers (should be optimized)
const deleteWithoutBodyHeaders = 'DELETE / HTTP/1.1\r\nHost: localhost\r\n\r\n';
await makeRequest(deleteWithoutBodyHeaders);
// POST request with Content-Length header (should not be optimized)
const postWithContentLength = 'POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n';
await makeRequest(postWithContentLength);
// GET request with Content-Length header (should not be optimized)
const getWithContentLength = 'GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n';
await makeRequest(getWithContentLength);
// POST request with Transfer-Encoding header (should not be optimized)
const postWithTransferEncoding = 'POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n';
await makeRequest(postWithTransferEncoding);
// GET request with Transfer-Encoding header (should not be optimized)
const getWithTransferEncoding = 'GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n';
await makeRequest(getWithTransferEncoding);
server.close();
assert.strictEqual(reqs, 8, `Expected 8 requests but got ${reqs}`);
assert.strictEqual(optimizedReqs, 4, `Expected 4 optimized requests but got ${optimizedReqs}`);
}));
function makeRequest(str) {
return new Promise((resolve) => {
const client = net.connect({ port: server.address().port }, common.mustCall(() => {
client.on('data', () => {});
client.on('end', common.mustCall(() => {
resolve();
}));
client.write(str);
client.end();
}));
});
}