Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -3537,6 +3537,9 @@ Found'`.
<!-- YAML
added: v0.1.13
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59778
description: Add optimizeEmptyRequests option.
- version:
- v20.1.0
- v18.17.0
Expand Down Expand Up @@ -3632,6 +3635,12 @@ 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.
This option is still under experimental phase.
**Default:** `false`.

* `requestListener` {Function}

Expand Down
9 changes: 9 additions & 0 deletions lib/_http_incoming.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ const onResponseFinishChannel = dc.channel('http.server.response.finish');
const kServerResponse = Symbol('ServerResponse');
const kServerResponseStatistics = Symbol('ServerResponseStatistics');

const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');

const {
hasObserver,
startPerf,
Expand Down Expand Up @@ -452,6 +454,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');
Expand Down Expand Up @@ -1051,6 +1058,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.
Expand Down Expand Up @@ -1102,6 +1113,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);
Expand Down
76 changes: 76 additions & 0 deletions test/parallel/test-http-server-optimize-empty-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'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('end', common.mustCall(() => {
resolve();
}));
client.write(str);
client.end();
}));
});
}
Loading