Skip to content

Commit 270bebd

Browse files
committed
http2: retain header memory in session accounting
1 parent aa444b2 commit 270bebd

4 files changed

Lines changed: 89 additions & 11 deletions

File tree

doc/api/http2.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2874,9 +2874,10 @@ changes:
28742874
This is a credit based limit, existing `Http2Stream`s may cause this
28752875
limit to be exceeded, but new `Http2Stream` instances will be rejected
28762876
while this limit is exceeded. The current number of `Http2Stream` sessions,
2877-
the current memory use of the header compression tables, current data
2878-
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
2879-
counted towards the current limit. **Default:** `10`.
2877+
the current memory use of the header compression tables, header blocks
2878+
retained by open streams, current data queued to be sent, and
2879+
unacknowledged `PING` and `SETTINGS` frames are all counted towards the
2880+
current limit. **Default:** `10`.
28802881
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
28812882
This is similar to [`server.maxHeadersCount`][] or
28822883
[`request.maxHeadersCount`][] in the `node:http` module. The minimum value
@@ -3095,9 +3096,10 @@ changes:
30953096
credit based limit, existing `Http2Stream`s may cause this
30963097
limit to be exceeded, but new `Http2Stream` instances will be rejected
30973098
while this limit is exceeded. The current number of `Http2Stream` sessions,
3098-
the current memory use of the header compression tables, current data
3099-
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
3100-
counted towards the current limit. **Default:** `10`.
3099+
the current memory use of the header compression tables, header blocks
3100+
retained by open streams, current data queued to be sent, and
3101+
unacknowledged `PING` and `SETTINGS` frames are all counted towards the
3102+
current limit. **Default:** `10`.
31013103
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
31023104
This is similar to [`server.maxHeadersCount`][] or
31033105
[`request.maxHeadersCount`][] in the `node:http` module. The minimum value
@@ -3275,9 +3277,10 @@ changes:
32753277
This is a credit based limit, existing `Http2Stream`s may cause this
32763278
limit to be exceeded, but new `Http2Stream` instances will be rejected
32773279
while this limit is exceeded. The current number of `Http2Stream` sessions,
3278-
the current memory use of the header compression tables, current data
3279-
queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
3280-
counted towards the current limit. **Default:** `10`.
3280+
the current memory use of the header compression tables, header blocks
3281+
retained by open streams, current data queued to be sent, and
3282+
unacknowledged `PING` and `SETTINGS` frames are all counted towards the
3283+
current limit. **Default:** `10`.
32813284
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
32823285
This is similar to [`server.maxHeadersCount`][] or
32833286
[`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;
@@ -1548,7 +1556,10 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
15481556
});
15491557
CHECK_EQ(stream->headers_count(), 0);
15501558

1551-
DecrementCurrentSessionMemory(stream->current_headers_length_);
1559+
// Keep the header block charged against maxSessionMemory while the
1560+
// corresponding JS objects can still keep it alive for the lifetime of
1561+
// the stream.
1562+
stream->retained_headers_length_ += stream->current_headers_length_;
15521563
stream->current_headers_length_ = 0;
15531564

15541565
Local<Value> args[] = {

src/node_http2.h

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

491491
// The Current Headers block... As headers are received for this stream,
492492
// they are temporarily stored here until the OnFrameReceived is called
493-
// signalling the end of the HEADERS frame
493+
// signalling the end of the HEADERS frame.
494494
nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS;
495495
uint32_t current_headers_length_ = 0; // total number of octets
496+
// Charged against maxSessionMemory while headers stay alive in JS.
497+
uint64_t retained_headers_length_ = 0;
496498
std::vector<Http2Header> current_headers_;
497499

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

0 commit comments

Comments
 (0)