Skip to content

Commit 4395fe1

Browse files
RafaelGSSaduh95
authored andcommitted
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 64fc625 commit 4395fe1

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
constkServerResponse=Symbol('ServerResponse');
108108
constkServerResponseStatistics=Symbol('ServerResponseStatistics');
109109

110+
constkOptimizeEmptyRequests=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+
constoptimizeEmptyRequests=options.optimizeEmptyRequests;
461+
if(optimizeEmptyRequests!==undefined)
462+
validateBoolean(optimizeEmptyRequests,'options.optimizeEmptyRequests');
463+
this[kOptimizeEmptyRequests]=optimizeEmptyRequests||false;
464+
458465
constinsecureHTTPParser=options.insecureHTTPParser;
459466
if(insecureHTTPParser!==undefined)
460467
validateBoolean(insecureHTTPParser,'options.insecureHTTPParser');
@@ -1069,6 +1076,10 @@ function emitCloseNT(self){
10691076
}
10701077
}
10711078

1079+
functionhasBodyHeaders(headers){
1080+
return('content-length'inheaders)||('transfer-encoding'inheaders);
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+
constshouldOptimize=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+
constcommon=require('../common');
4+
constassert=require('assert');
5+
consthttp=require('http');
6+
constnet=require('net');
7+
8+
letreqs=0;
9+
letoptimizedReqs=0;
10+
constserver=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+
constgetRequest='GET / HTTP/1.1\r\nHost: localhost\r\n\r\n';
30+
awaitmakeRequest(getRequest);
31+
32+
// HEAD request (should always be optimized regardless of headers)
33+
constheadRequest='HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n';
34+
awaitmakeRequest(headRequest);
35+
36+
// POST request without body headers (should be optimized)
37+
constpostWithoutBodyHeaders='POST / HTTP/1.1\r\nHost: localhost\r\n\r\n';
38+
awaitmakeRequest(postWithoutBodyHeaders);
39+
40+
// DELETE request without body headers (should be optimized)
41+
constdeleteWithoutBodyHeaders='DELETE / HTTP/1.1\r\nHost: localhost\r\n\r\n';
42+
awaitmakeRequest(deleteWithoutBodyHeaders);
43+
44+
// POST request with Content-Length header (should not be optimized)
45+
constpostWithContentLength='POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n';
46+
awaitmakeRequest(postWithContentLength);
47+
48+
// GET request with Content-Length header (should not be optimized)
49+
constgetWithContentLength='GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n';
50+
awaitmakeRequest(getWithContentLength);
51+
52+
// POST request with Transfer-Encoding header (should not be optimized)
53+
constpostWithTransferEncoding='POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n';
54+
awaitmakeRequest(postWithTransferEncoding);
55+
56+
// GET request with Transfer-Encoding header (should not be optimized)
57+
constgetWithTransferEncoding='GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n';
58+
awaitmakeRequest(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+
functionmakeRequest(str){
67+
returnnewPromise((resolve)=>{
68+
constclient=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)