Skip to content

Commit d7ee9ea

Browse files
ronagRafaelGSS
andcommitted
http: add optimizeEmptyRequests server option
Signed-off-by: RafaelGSS <[email protected]> Co-Authored-By: RafaelGSS <[email protected]>
1 parent 24ded11 commit d7ee9ea

File tree

5 files changed

+103
-4
lines changed

5 files changed

+103
-4
lines changed

doc/api/http.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3554,6 +3554,9 @@ Found'`.
35543554
<!-- YAML
35553555
added: v0.1.13
35563556
changes:
3557+
- version: REPLACEME
3558+
pr-url: https://github.com/nodejs/node/pull/59778
3559+
description: Add optimizeEmptyRequests option.
35573560
- version: REPLACEME
35583561
pr-url: https://github.com/nodejs/node/pull/59824
35593562
description: The `shouldUpgradeCallback` option is now supported.
@@ -3659,6 +3662,12 @@ changes:
36593662
* `rejectNonStandardBodyWrites` {boolean} If set to `true`, an error is thrown
36603663
when writing to an HTTP response which does not have a body.
36613664
**Default:** `false`.
3665+
* `optimizeEmptyRequests` {boolean} If set to `true`, requests without `Content-Length`
3666+
or `Transfer-Encoding` headers (indicating no body) will have their lifecycle events
3667+
immediately emitted. For `HEAD` and `GET` requests, this optimization is applied
3668+
regardless of headers. This improves performance for requests without bodies
3669+
across all HTTP methods.
3670+
**Default:** `false`.
36623671
36633672
* `requestListener` {Function}
36643673

lib/_http_server.js

Lines changed: 30 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,
@@ -453,6 +455,11 @@ function storeHTTPOptions(options) {
453455
validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
454456
this.maxHeaderSize = maxHeaderSize;
455457

458+
const optimizeEmptyRequests = options.optimizeEmptyRequests;
459+
if (optimizeEmptyRequests !== undefined)
460+
validateBoolean(optimizeEmptyRequests, 'options.optimizeEmptyRequests');
461+
this[kOptimizeEmptyRequests] = optimizeEmptyRequests || false;
462+
456463
const insecureHTTPParser = options.insecureHTTPParser;
457464
if (insecureHTTPParser !== undefined)
458465
validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
@@ -1118,6 +1125,29 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
11181125
});
11191126
}
11201127

1128+
// Check if we should optimize empty requests (those without Content-Length or Transfer-Encoding headers)
1129+
const hasBodyHeaders = ('content-length' in req.headers) || ('transfer-encoding' in req.headers);
1130+
const isHeadOrGet = (req.method === 'HEAD' || req.method === 'GET');
1131+
1132+
// Apply optimization for all methods when optimizeEmptyRequests is enabled and there are no body headers
1133+
// For HEAD and GET methods, we optimize regardless of body headers for backward compatibility
1134+
const shouldOptimize = server[kOptimizeEmptyRequests] === true && (isHeadOrGet || !hasBodyHeaders);
1135+
1136+
if (shouldOptimize) {
1137+
// Fast processing where request "has" already emitted all lifecycle events.
1138+
// This avoids a lot of unnecessary overhead otherwise introduced by
1139+
// stream.Readable life cycle rules. The downside is that this will
1140+
// break some servers that read bodies for methods that don't have body headers.
1141+
req._dumped = true;
1142+
req._readableState.ended = true;
1143+
req._readableState.endEmitted = true;
1144+
req._readableState.destroyed = true;
1145+
req._readableState.closed = true;
1146+
req._readableState.closeEmitted = true;
1147+
1148+
req._read();
1149+
}
1150+
11211151
if (socket._httpMessage) {
11221152
// There are already pending outgoing res, append.
11231153
state.outgoing.push(res);

test/parallel/test-http-chunk-extensions-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ const assert = require('assert');
124124
}));
125125

126126
sock.end('' +
127-
'GET / HTTP/1.1\r\n' +
127+
'PUT / HTTP/1.1\r\n' +
128128
`Host: localhost:${port}\r\n` +
129129
'Transfer-Encoding: chunked\r\n\r\n' +
130130
'2;' + 'A'.repeat(10000) + '=bar\r\nAA\r\n' +
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
const server = http.createServer({
10+
optimizeEmptyRequests: true
11+
}, (req, res) => {
12+
reqs++;
13+
assert.strictEqual(req._dumped, true);
14+
assert.strictEqual(req._readableState.ended, true);
15+
assert.strictEqual(req._readableState.endEmitted, true);
16+
assert.strictEqual(req._readableState.destroyed, true);
17+
18+
res.writeHead(200);
19+
res.end('ok');
20+
});
21+
22+
server.listen(0, common.mustCall(async () => {
23+
// GET request without Content-Length (should be optimized)
24+
const getRequest = 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n';
25+
await makeRequest(getRequest);
26+
27+
// HEAD request (should always be optimized regardless of headers)
28+
const headRequest = 'HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n';
29+
await makeRequest(headRequest);
30+
31+
// POST request without body headers (should be optimized)
32+
const postWithoutBodyHeaders = 'POST / HTTP/1.1\r\nHost: localhost\r\n\r\n';
33+
await makeRequest(postWithoutBodyHeaders);
34+
35+
// DELETE request without body headers (should be optimized)
36+
const deleteWithoutBodyHeaders = 'DELETE / HTTP/1.1\r\nHost: localhost\r\n\r\n';
37+
await makeRequest(deleteWithoutBodyHeaders);
38+
server.close();
39+
40+
assert.strictEqual(reqs, 4);
41+
}));
42+
43+
function makeRequest(str) {
44+
return new Promise((resolve) => {
45+
const client = net.connect({ port: server.address().port }, common.mustCall(() => {
46+
client.on('data', common.mustCall());
47+
client.on('end', common.mustCall(() => {
48+
resolve();
49+
}));
50+
client.write(str);
51+
client.end();
52+
}));
53+
});
54+
}

test/parallel/test-http.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,18 @@ const server = http.Server(common.mustCall((req, res) => {
5252
if (expectedRequests.length === 0)
5353
server.close();
5454

55-
req.on('end', () => {
55+
if (req.readableEnded) {
5656
res.writeHead(200, { 'Content-Type': 'text/plain' });
5757
res.write(`The path was ${url.parse(req.url).pathname}`);
5858
res.end();
59-
});
60-
req.resume();
59+
} else {
60+
req.on('end', () => {
61+
res.writeHead(200, { 'Content-Type': 'text/plain' });
62+
res.write(`The path was ${url.parse(req.url).pathname}`);
63+
res.end();
64+
});
65+
req.resume();
66+
}
6167
}, 3));
6268
server.listen(0);
6369

0 commit comments

Comments
 (0)