Skip to content

Commit ba82a41

Browse files
authored
http2: retain header memory in session accounting
Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #63752 Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent 45c7071 commit ba82a41

4 files changed

Lines changed: 96 additions & 11 deletions

File tree

doc/api/http2.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2906,9 +2906,10 @@ changes:
29062906
This is a credit based limit, existing `Http2Stream`s may cause this
29072907
limit to be exceeded, but new `Http2Stream` instances will be rejected
29082908
while this limit is exceeded. The current number of `Http2Stream` sessions,
2909-
the current memory use of the header compression tables, current data
2910-
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
2911-
counted towards the current limit. **Default:** `10`.
2909+
the current memory use of the header compression tables, header blocks
2910+
retained by open streams, current data queued to be sent, and
2911+
unacknowledged `PING` and `SETTINGS` frames are all counted towards the
2912+
current limit. **Default:** `10`.
29122913
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
29132914
This is similar to [`server.maxHeadersCount`][] or
29142915
[`request.maxHeadersCount`][] in the `node:http` module. The minimum value
@@ -3127,9 +3128,10 @@ changes:
31273128
credit based limit, existing `Http2Stream`s may cause this
31283129
limit to be exceeded, but new `Http2Stream` instances will be rejected
31293130
while this limit is exceeded. The current number of `Http2Stream` sessions,
3130-
the current memory use of the header compression tables, current data
3131-
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
3132-
counted towards the current limit. **Default:** `10`.
3131+
the current memory use of the header compression tables, header blocks
3132+
retained by open streams, current data queued to be sent, and
3133+
unacknowledged `PING` and `SETTINGS` frames are all counted towards the
3134+
current limit. **Default:** `10`.
31333135
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
31343136
This is similar to [`server.maxHeadersCount`][] or
31353137
[`request.maxHeadersCount`][] in the `node:http` module. The minimum value
@@ -3307,9 +3309,10 @@ changes:
33073309
This is a credit based limit, existing `Http2Stream`s may cause this
33083310
limit to be exceeded, but new `Http2Stream` instances will be rejected
33093311
while this limit is exceeded. The current number of `Http2Stream` sessions,
3310-
the current memory use of the header compression tables, current data
3311-
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
3312-
counted towards the current limit. **Default:** `10`.
3312+
the current memory use of the header compression tables, header blocks
3313+
retained by open streams, current data queued to be sent, and
3314+
unacknowledged `PING` and `SETTINGS` frames are all counted towards the
3315+
current limit. **Default:** `10`.
33133316
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
33143317
This is similar to [`server.maxHeadersCount`][] or
33153318
[`request.maxHeadersCount`][] in the `node:http` module. The minimum value

src/node_http2.cc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,14 @@ BaseObjectPtr<Http2Stream> Http2Session::RemoveStream(int32_t id) {
902902
stream = FindStream(id);
903903
if (stream) {
904904
streams_.erase(id);
905+
if (stream->current_headers_length_ > 0) {
906+
DecrementCurrentSessionMemory(stream->current_headers_length_);
907+
stream->current_headers_length_ = 0;
908+
}
909+
if (stream->retained_headers_length_ > 0) {
910+
DecrementCurrentSessionMemory(stream->retained_headers_length_);
911+
stream->retained_headers_length_ = 0;
912+
}
905913
DecrementCurrentSessionMemory(sizeof(*stream));
906914
}
907915
return stream;
@@ -1561,7 +1569,10 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
15611569
});
15621570
CHECK_EQ(stream->headers_count(), 0);
15631571

1564-
DecrementCurrentSessionMemory(stream->current_headers_length_);
1572+
// Keep the header block charged against maxSessionMemory while the
1573+
// corresponding JS objects can still keep it alive for the lifetime of
1574+
// the stream.
1575+
stream->retained_headers_length_ += stream->current_headers_length_;
15651576
stream->current_headers_length_ = 0;
15661577

15671578
Local<Value> args[] = {

src/node_http2.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,9 +500,11 @@ class Http2Stream : public AsyncWrap,
500500

501501
// The Current Headers block... As headers are received for this stream,
502502
// they are temporarily stored here until the OnFrameReceived is called
503-
// signalling the end of the HEADERS frame
503+
// signalling the end of the HEADERS frame.
504504
nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS;
505505
uint32_t current_headers_length_ = 0; // total number of octets
506+
// Charged against maxSessionMemory while headers stay alive in JS.
507+
uint64_t retained_headers_length_ = 0;
506508
std::vector<Http2Header> current_headers_;
507509

508510
// This keeps track of the amount of data read from the socket while the
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const Countdown = require('../common/countdown');
8+
const assert = require('assert');
9+
const http2 = require('http2');
10+
11+
const {
12+
NGHTTP2_ENHANCE_YOUR_CALM,
13+
} = http2.constants;
14+
15+
// Regression test: header blocks retained by stalled streams should continue
16+
// to count against maxSessionMemory after they have been handed to JS.
17+
const maxSessionMemory = 1;
18+
const totalRequests = 400;
19+
const cookieCrumbs = 120;
20+
21+
let accepted = 0;
22+
let rejected = 0;
23+
24+
const server = http2.createServer({ maxSessionMemory });
25+
server.on('stream', (stream) => {
26+
accepted++;
27+
stream.on('error', () => {});
28+
stream.respond();
29+
stream.write('x');
30+
});
31+
32+
server.listen(0, common.mustCall(() => {
33+
const client = http2.connect(`http://localhost:${server.address().port}`, {
34+
settings: {
35+
initialWindowSize: 0,
36+
},
37+
});
38+
client.on('error', () => {});
39+
40+
client.on('remoteSettings', common.mustCall(() => {
41+
let destroyed = false;
42+
const countdown = new Countdown(totalRequests, common.mustCall(() => {
43+
assert(rejected > 0);
44+
assert(accepted < totalRequests);
45+
server.close();
46+
}));
47+
48+
for (let i = 0; i < totalRequests; i++) {
49+
const headers = [':path', '/'];
50+
for (let j = 0; j < cookieCrumbs; j++) {
51+
headers.push('cookie', 'a=1');
52+
}
53+
54+
const req = client.request(headers);
55+
req.on('error', () => {});
56+
req.on('close', () => {
57+
if (req.rstCode === NGHTTP2_ENHANCE_YOUR_CALM) {
58+
rejected++;
59+
if (!destroyed) {
60+
destroyed = true;
61+
client.destroy();
62+
}
63+
}
64+
countdown.dec();
65+
});
66+
req.end();
67+
}
68+
}));
69+
}));

0 commit comments

Comments
 (0)