Skip to content

Commit 0c35aaf

Browse files
authored
http: add optimizeEmptyRequests server option
Signed-off-by: RafaelGSS <[email protected]> Co-Authored-By: RafaelGSS <[email protected]> PR-URL: #59778 Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Tim Perry <[email protected]> Reviewed-By: Gerhard Stöbich <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]>
1 parent 1072295 commit 0c35aaf

File tree

4 files changed

+118
-0
lines changed

4 files changed

+118
-0
lines changed

doc/api/http.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3555,6 +3555,9 @@ Found'`.
35553555
<!-- YAML
35563556
added: v0.1.13
35573557
changes:
3558+
- version: REPLACEME
3559+
pr-url: https://github.com/nodejs/node/pull/59778
3560+
description: Add optimizeEmptyRequests option.
35583561
- version: v24.9.0
35593562
pr-url: https://github.com/nodejs/node/pull/59824
35603563
description: The `shouldUpgradeCallback` option is now supported.
@@ -3660,6 +3663,11 @@ changes:
36603663
* `rejectNonStandardBodyWrites` {boolean} If set to `true`, an error is thrown
36613664
when writing to an HTTP response which does not have a body.
36623665
**Default:** `false`.
3666+
* `optimizeEmptyRequests` {boolean} If set to `true`, requests without `Content-Length`
3667+
or `Transfer-Encoding` headers (indicating no body) will be initialized with an
3668+
already-ended body stream, so they will never emit any stream events
3669+
(like `'data'` or `'end'`). You can use `req.readableEnded` to detect this case.
3670+
**Default:** `false`.
36633671
36643672
* `requestListener` {Function}
36653673

lib/_http_incoming.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,15 @@ function _addHeaderLineDistinct(field, value, dest) {
423423
}
424424
}
425425

426+
IncomingMessage.prototype._dumpAndCloseReadable = function _dumpAndCloseReadable() {
427+
this._dumped = true;
428+
this._readableState.ended = true;
429+
this._readableState.endEmitted = true;
430+
this._readableState.destroyed = true;
431+
this._readableState.closed = true;
432+
this._readableState.closeEmitted = true;
433+
};
434+
426435

427436
// Call this instead of resume() if we want to just
428437
// dump all the data to /dev/null

lib/_http_server.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ const onResponseFinishChannel = dc.channel('http.server.response.finish');
107107
const kServerResponse = Symbol('ServerResponse');
108108
const kServerResponseStatistics = Symbol('ServerResponseStatistics');
109109

110+
const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
111+
110112
const {
111113
hasObserver,
112114
startPerf,
@@ -455,6 +457,11 @@ function storeHTTPOptions(options) {
455457
validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
456458
this.maxHeaderSize = maxHeaderSize;
457459

460+
const optimizeEmptyRequests = options.optimizeEmptyRequests;
461+
if (optimizeEmptyRequests !== undefined)
462+
validateBoolean(optimizeEmptyRequests, 'options.optimizeEmptyRequests');
463+
this[kOptimizeEmptyRequests] = optimizeEmptyRequests || false;
464+
458465
const insecureHTTPParser = options.insecureHTTPParser;
459466
if (insecureHTTPParser !== undefined)
460467
validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
@@ -1069,6 +1076,10 @@ function emitCloseNT(self) {
10691076
}
10701077
}
10711078

1079+
function hasBodyHeaders(headers) {
1080+
return ('content-length' in headers) || ('transfer-encoding' in headers);
1081+
}
1082+
10721083
// The following callback is issued after the headers have been read on a
10731084
// new message. In this callback we setup the response object and pass it
10741085
// to the user.
@@ -1120,6 +1131,19 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
11201131
});
11211132
}
11221133

1134+
// Check if we should optimize empty requests (those without Content-Length or Transfer-Encoding headers)
1135+
const shouldOptimize = server[kOptimizeEmptyRequests] === true && !hasBodyHeaders(req.headers);
1136+
1137+
if (shouldOptimize) {
1138+
// Fast processing where emitting 'data', 'end' and 'close' events is
1139+
// skipped and data is dumped.
1140+
// This avoids a lot of unnecessary overhead otherwise introduced by
1141+
// stream.Readable life cycle rules. The downside is that this will
1142+
// break some servers that read bodies for methods that don't have body headers.
1143+
req._dumpAndCloseReadable();
1144+
req._read();
1145+
}
1146+
11231147
if (socket._httpMessage) {
11241148
// There are already pending outgoing res, append.
11251149
state.outgoing.push(res);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
const net = require('net');
7+
8+
let reqs = 0;
9+
let optimizedReqs = 0;
10+
const server = http.createServer({
11+
optimizeEmptyRequests: true
12+
}, (req, res) => {
13+
reqs++;
14+
if (req._dumped) {
15+
optimizedReqs++;
16+
req.on('data', common.mustNotCall());
17+
req.on('end', common.mustNotCall());
18+
19+
assert.strictEqual(req._dumped, true);
20+
assert.strictEqual(req.readableEnded, true);
21+
assert.strictEqual(req.destroyed, true);
22+
}
23+
res.writeHead(200);
24+
res.end('ok');
25+
});
26+
27+
server.listen(0, common.mustCall(async () => {
28+
// GET request without Content-Length (should be optimized)
29+
const getRequest = 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n';
30+
await makeRequest(getRequest);
31+
32+
// HEAD request (should always be optimized regardless of headers)
33+
const headRequest = 'HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n';
34+
await makeRequest(headRequest);
35+
36+
// POST request without body headers (should be optimized)
37+
const postWithoutBodyHeaders = 'POST / HTTP/1.1\r\nHost: localhost\r\n\r\n';
38+
await makeRequest(postWithoutBodyHeaders);
39+
40+
// DELETE request without body headers (should be optimized)
41+
const deleteWithoutBodyHeaders = 'DELETE / HTTP/1.1\r\nHost: localhost\r\n\r\n';
42+
await makeRequest(deleteWithoutBodyHeaders);
43+
44+
// POST request with Content-Length header (should not be optimized)
45+
const postWithContentLength = 'POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n';
46+
await makeRequest(postWithContentLength);
47+
48+
// GET request with Content-Length header (should not be optimized)
49+
const getWithContentLength = 'GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n';
50+
await makeRequest(getWithContentLength);
51+
52+
// POST request with Transfer-Encoding header (should not be optimized)
53+
const postWithTransferEncoding = 'POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n';
54+
await makeRequest(postWithTransferEncoding);
55+
56+
// GET request with Transfer-Encoding header (should not be optimized)
57+
const getWithTransferEncoding = 'GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n';
58+
await makeRequest(getWithTransferEncoding);
59+
60+
server.close();
61+
62+
assert.strictEqual(reqs, 8, `Expected 8 requests but got ${reqs}`);
63+
assert.strictEqual(optimizedReqs, 4, `Expected 4 optimized requests but got ${optimizedReqs}`);
64+
}));
65+
66+
function makeRequest(str) {
67+
return new Promise((resolve) => {
68+
const client = net.connect({ port: server.address().port }, common.mustCall(() => {
69+
client.on('data', () => {});
70+
client.on('end', common.mustCall(() => {
71+
resolve();
72+
}));
73+
client.write(str);
74+
client.end();
75+
}));
76+
});
77+
}

0 commit comments

Comments
 (0)