nlcurl 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -13
- package/dist/cli/args.d.ts +37 -5
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +6 -17
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +25 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts +24 -7
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +24 -12
- package/dist/cli/output.js.map +1 -1
- package/dist/cookies/jar.d.ts +45 -13
- package/dist/cookies/jar.d.ts.map +1 -1
- package/dist/cookies/jar.js +88 -29
- package/dist/cookies/jar.js.map +1 -1
- package/dist/cookies/parser.d.ts +25 -3
- package/dist/cookies/parser.d.ts.map +1 -1
- package/dist/cookies/parser.js +12 -7
- package/dist/cookies/parser.js.map +1 -1
- package/dist/core/client.d.ts +49 -33
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +65 -38
- package/dist/core/client.js.map +1 -1
- package/dist/core/errors.d.ts +94 -6
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +95 -6
- package/dist/core/errors.js.map +1 -1
- package/dist/core/request.d.ts +99 -32
- package/dist/core/request.d.ts.map +1 -1
- package/dist/core/request.js +0 -3
- package/dist/core/request.js.map +1 -1
- package/dist/core/response.d.ts +92 -8
- package/dist/core/response.d.ts.map +1 -1
- package/dist/core/response.js +92 -7
- package/dist/core/response.js.map +1 -1
- package/dist/core/session.d.ts +109 -14
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +143 -49
- package/dist/core/session.js.map +1 -1
- package/dist/fingerprints/akamai.d.ts +11 -11
- package/dist/fingerprints/akamai.d.ts.map +1 -1
- package/dist/fingerprints/akamai.js +10 -14
- package/dist/fingerprints/akamai.js.map +1 -1
- package/dist/fingerprints/database.d.ts +14 -15
- package/dist/fingerprints/database.d.ts.map +1 -1
- package/dist/fingerprints/database.js +14 -19
- package/dist/fingerprints/database.js.map +1 -1
- package/dist/fingerprints/extensions.d.ts +121 -27
- package/dist/fingerprints/extensions.d.ts.map +1 -1
- package/dist/fingerprints/extensions.js +132 -49
- package/dist/fingerprints/extensions.js.map +1 -1
- package/dist/fingerprints/ja3.d.ts +34 -18
- package/dist/fingerprints/ja3.d.ts.map +1 -1
- package/dist/fingerprints/ja3.js +34 -18
- package/dist/fingerprints/ja3.js.map +1 -1
- package/dist/fingerprints/profiles/chrome.d.ts +21 -10
- package/dist/fingerprints/profiles/chrome.d.ts.map +1 -1
- package/dist/fingerprints/profiles/chrome.js +25 -22
- package/dist/fingerprints/profiles/chrome.js.map +1 -1
- package/dist/fingerprints/profiles/edge.d.ts +10 -7
- package/dist/fingerprints/profiles/edge.d.ts.map +1 -1
- package/dist/fingerprints/profiles/edge.js +10 -10
- package/dist/fingerprints/profiles/edge.js.map +1 -1
- package/dist/fingerprints/profiles/firefox.d.ts +11 -3
- package/dist/fingerprints/profiles/firefox.d.ts.map +1 -1
- package/dist/fingerprints/profiles/firefox.js +15 -14
- package/dist/fingerprints/profiles/firefox.js.map +1 -1
- package/dist/fingerprints/profiles/safari.d.ts +14 -3
- package/dist/fingerprints/profiles/safari.d.ts.map +1 -1
- package/dist/fingerprints/profiles/safari.js +16 -13
- package/dist/fingerprints/profiles/safari.js.map +1 -1
- package/dist/fingerprints/profiles/tor.d.ts +8 -7
- package/dist/fingerprints/profiles/tor.d.ts.map +1 -1
- package/dist/fingerprints/profiles/tor.js +8 -14
- package/dist/fingerprints/profiles/tor.js.map +1 -1
- package/dist/fingerprints/types.d.ts +70 -47
- package/dist/fingerprints/types.d.ts.map +1 -1
- package/dist/fingerprints/types.js +0 -7
- package/dist/fingerprints/types.js.map +1 -1
- package/dist/http/h1/client.d.ts +30 -9
- package/dist/http/h1/client.d.ts.map +1 -1
- package/dist/http/h1/client.js +153 -20
- package/dist/http/h1/client.js.map +1 -1
- package/dist/http/h1/encoder.d.ts +9 -6
- package/dist/http/h1/encoder.d.ts.map +1 -1
- package/dist/http/h1/encoder.js +14 -13
- package/dist/http/h1/encoder.js.map +1 -1
- package/dist/http/h1/parser.d.ts +68 -14
- package/dist/http/h1/parser.d.ts.map +1 -1
- package/dist/http/h1/parser.js +92 -37
- package/dist/http/h1/parser.js.map +1 -1
- package/dist/http/h2/client.d.ts +87 -14
- package/dist/http/h2/client.d.ts.map +1 -1
- package/dist/http/h2/client.js +496 -74
- package/dist/http/h2/client.js.map +1 -1
- package/dist/http/h2/frames.d.ts +103 -6
- package/dist/http/h2/frames.d.ts.map +1 -1
- package/dist/http/h2/frames.js +96 -17
- package/dist/http/h2/frames.js.map +1 -1
- package/dist/http/h2/hpack.d.ts +40 -5
- package/dist/http/h2/hpack.d.ts.map +1 -1
- package/dist/http/h2/hpack.js +50 -36
- package/dist/http/h2/hpack.js.map +1 -1
- package/dist/http/negotiator.d.ts +36 -12
- package/dist/http/negotiator.d.ts.map +1 -1
- package/dist/http/negotiator.js +96 -24
- package/dist/http/negotiator.js.map +1 -1
- package/dist/http/pool.d.ts +66 -17
- package/dist/http/pool.d.ts.map +1 -1
- package/dist/http/pool.js +47 -20
- package/dist/http/pool.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -13
- package/dist/index.js.map +1 -1
- package/dist/middleware/interceptor.d.ts +40 -8
- package/dist/middleware/interceptor.d.ts.map +1 -1
- package/dist/middleware/interceptor.js +28 -6
- package/dist/middleware/interceptor.js.map +1 -1
- package/dist/middleware/rate-limiter.d.ts +18 -5
- package/dist/middleware/rate-limiter.d.ts.map +1 -1
- package/dist/middleware/rate-limiter.js +12 -7
- package/dist/middleware/rate-limiter.js.map +1 -1
- package/dist/middleware/retry.d.ts +17 -5
- package/dist/middleware/retry.d.ts.map +1 -1
- package/dist/middleware/retry.js +13 -11
- package/dist/middleware/retry.js.map +1 -1
- package/dist/proxy/http-proxy.d.ts +17 -9
- package/dist/proxy/http-proxy.d.ts.map +1 -1
- package/dist/proxy/http-proxy.js +9 -13
- package/dist/proxy/http-proxy.js.map +1 -1
- package/dist/proxy/socks.d.ts +20 -9
- package/dist/proxy/socks.d.ts.map +1 -1
- package/dist/proxy/socks.js +20 -31
- package/dist/proxy/socks.js.map +1 -1
- package/dist/tls/constants.d.ts +74 -4
- package/dist/tls/constants.d.ts.map +1 -1
- package/dist/tls/constants.js +75 -21
- package/dist/tls/constants.js.map +1 -1
- package/dist/tls/node-engine.d.ts +17 -16
- package/dist/tls/node-engine.d.ts.map +1 -1
- package/dist/tls/node-engine.js +20 -27
- package/dist/tls/node-engine.js.map +1 -1
- package/dist/tls/stealth/client-hello.d.ts +32 -16
- package/dist/tls/stealth/client-hello.d.ts.map +1 -1
- package/dist/tls/stealth/client-hello.js +13 -37
- package/dist/tls/stealth/client-hello.js.map +1 -1
- package/dist/tls/stealth/engine.d.ts +18 -10
- package/dist/tls/stealth/engine.d.ts.map +1 -1
- package/dist/tls/stealth/engine.js +55 -40
- package/dist/tls/stealth/engine.js.map +1 -1
- package/dist/tls/stealth/handshake.d.ts +31 -17
- package/dist/tls/stealth/handshake.d.ts.map +1 -1
- package/dist/tls/stealth/handshake.js +173 -74
- package/dist/tls/stealth/handshake.js.map +1 -1
- package/dist/tls/stealth/key-schedule.d.ts +90 -32
- package/dist/tls/stealth/key-schedule.d.ts.map +1 -1
- package/dist/tls/stealth/key-schedule.js +80 -46
- package/dist/tls/stealth/key-schedule.js.map +1 -1
- package/dist/tls/stealth/record-layer.d.ts +76 -25
- package/dist/tls/stealth/record-layer.d.ts.map +1 -1
- package/dist/tls/stealth/record-layer.js +66 -36
- package/dist/tls/stealth/record-layer.js.map +1 -1
- package/dist/tls/types.d.ts +33 -25
- package/dist/tls/types.d.ts.map +1 -1
- package/dist/tls/types.js +0 -4
- package/dist/tls/types.js.map +1 -1
- package/dist/utils/buffer-reader.d.ts +99 -7
- package/dist/utils/buffer-reader.d.ts.map +1 -1
- package/dist/utils/buffer-reader.js +99 -7
- package/dist/utils/buffer-reader.js.map +1 -1
- package/dist/utils/buffer-writer.d.ts +99 -10
- package/dist/utils/buffer-writer.d.ts.map +1 -1
- package/dist/utils/buffer-writer.js +101 -12
- package/dist/utils/buffer-writer.js.map +1 -1
- package/dist/utils/encoding.d.ts +33 -8
- package/dist/utils/encoding.d.ts.map +1 -1
- package/dist/utils/encoding.js +58 -13
- package/dist/utils/encoding.js.map +1 -1
- package/dist/utils/logger.d.ts +61 -2
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +52 -4
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/url.d.ts +47 -7
- package/dist/utils/url.d.ts.map +1 -1
- package/dist/utils/url.js +47 -7
- package/dist/utils/url.js.map +1 -1
- package/dist/ws/client.d.ts +60 -15
- package/dist/ws/client.d.ts.map +1 -1
- package/dist/ws/client.js +38 -27
- package/dist/ws/client.js.map +1 -1
- package/dist/ws/frame.d.ts +43 -9
- package/dist/ws/frame.d.ts.map +1 -1
- package/dist/ws/frame.js +35 -19
- package/dist/ws/frame.js.map +1 -1
- package/package.json +4 -4
package/dist/http/h2/client.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
* HTTP/2 client.
|
|
3
|
-
*
|
|
4
|
-
* Multiplexed HTTP/2 client that runs over a TLS Duplex stream.
|
|
5
|
-
* Sends the connection preface with browser-profile-matched SETTINGS
|
|
6
|
-
* and WINDOW_UPDATE for Akamai HTTP/2 fingerprint control.
|
|
7
|
-
*/
|
|
1
|
+
import { PassThrough } from 'node:stream';
|
|
8
2
|
import { NLcURLResponse } from '../../core/response.js';
|
|
9
3
|
import { HTTPError, TimeoutError, ProtocolError } from '../../core/errors.js';
|
|
10
|
-
import { decompressBody } from '../../utils/encoding.js';
|
|
4
|
+
import { decompressBody, createDecompressStream } from '../../utils/encoding.js';
|
|
11
5
|
import { HPACKEncoder, HPACKDecoder } from './hpack.js';
|
|
12
|
-
import { readFrame, writeFrame, buildSettingsFrame, buildWindowUpdateFrame, buildHeadersFrame, buildDataFrame, buildPingFrame, buildGoawayFrame, H2_PREFACE, FrameType, Flags, } from './frames.js';
|
|
13
|
-
|
|
6
|
+
import { readFrame, writeFrame, buildSettingsFrame, buildWindowUpdateFrame, buildHeadersFrame, buildDataFrame, buildPingFrame, buildGoawayFrame, buildRstStreamFrame, H2_PREFACE, FrameType, Flags, } from './frames.js';
|
|
7
|
+
const DEFAULT_INITIAL_WINDOW_SIZE = 65535;
|
|
8
|
+
const WINDOW_UPDATE_THRESHOLD_RATIO = 0.5;
|
|
9
|
+
/**
|
|
10
|
+
* Multiplexed HTTP/2 client that operates over an existing duplex stream.
|
|
11
|
+
* Handles all aspects of the HTTP/2 connection lifecycle: connection preface,
|
|
12
|
+
* settings negotiation, flow control (both stream-level and connection-level),
|
|
13
|
+
* HPACK header compression, and stream multiplexing.
|
|
14
|
+
*
|
|
15
|
+
* Create one `H2Client` per underlying TCP/TLS connection; each call to
|
|
16
|
+
* {@link H2Client.request} or {@link H2Client.streamRequest} opens a new HTTP/2 stream.
|
|
17
|
+
*/
|
|
14
18
|
export class H2Client {
|
|
15
19
|
stream;
|
|
16
20
|
encoder;
|
|
@@ -21,7 +25,35 @@ export class H2Client {
|
|
|
21
25
|
streams = new Map();
|
|
22
26
|
readBuffer = Buffer.alloc(0);
|
|
23
27
|
prefaceSent = false;
|
|
24
|
-
|
|
28
|
+
_closed = false;
|
|
29
|
+
_goawayReceived = false;
|
|
30
|
+
connectionRecvWindow = DEFAULT_INITIAL_WINDOW_SIZE;
|
|
31
|
+
initialStreamRecvWindow = DEFAULT_INITIAL_WINDOW_SIZE;
|
|
32
|
+
streamRecvWindows = new Map();
|
|
33
|
+
serverMaxConcurrentStreams = Infinity;
|
|
34
|
+
serverMaxFrameSize = 16384;
|
|
35
|
+
connectionSendWindow = DEFAULT_INITIAL_WINDOW_SIZE;
|
|
36
|
+
initialStreamSendWindow = DEFAULT_INITIAL_WINDOW_SIZE;
|
|
37
|
+
streamSendWindows = new Map();
|
|
38
|
+
pendingSendData = new Map();
|
|
39
|
+
pendingHeaderStreamId = null;
|
|
40
|
+
pendingHeaderBlock = null;
|
|
41
|
+
pendingHeaderFlags = 0;
|
|
42
|
+
/**
|
|
43
|
+
* Optional callback invoked when the underlying connection closes, regardless
|
|
44
|
+
* of the reason (graceful GOAWAY, remote reset, or network error).
|
|
45
|
+
*
|
|
46
|
+
* @type {(() => void) | undefined}
|
|
47
|
+
*/
|
|
48
|
+
onClose;
|
|
49
|
+
/**
|
|
50
|
+
* Creates a new H2Client and begins listening for frames on `stream`.
|
|
51
|
+
*
|
|
52
|
+
* @param {Duplex} stream - Connected transport stream (TLS or plain TCP).
|
|
53
|
+
* @param {H2Profile} [h2Profile] - Browser HTTP/2 fingerprint settings (SETTINGS frame values,
|
|
54
|
+
* window update sizes, and priority frames). Defaults to Chrome-like settings when omitted.
|
|
55
|
+
* @param {Array<[string, string]>} [defaultHeaders=[]] - Profile-level request headers prepended to every request.
|
|
56
|
+
*/
|
|
25
57
|
constructor(stream, h2Profile, defaultHeaders = []) {
|
|
26
58
|
this.stream = stream;
|
|
27
59
|
this.encoder = new HPACKEncoder();
|
|
@@ -29,28 +61,38 @@ export class H2Client {
|
|
|
29
61
|
this.h2Profile = h2Profile;
|
|
30
62
|
this.defaultHeaders = defaultHeaders;
|
|
31
63
|
stream.on('data', (chunk) => this.onData(chunk));
|
|
32
|
-
stream.once('error', (err) => this.
|
|
33
|
-
stream.once('close', () => this.
|
|
64
|
+
stream.once('error', (err) => this.handleError(err));
|
|
65
|
+
stream.once('close', () => this.handleClose());
|
|
34
66
|
}
|
|
35
67
|
/**
|
|
36
|
-
*
|
|
68
|
+
* Returns `true` if the connection has been closed or destroyed and is no
|
|
69
|
+
* longer usable.
|
|
37
70
|
*
|
|
38
|
-
*
|
|
71
|
+
* @returns {boolean} Whether the connection is closed.
|
|
72
|
+
*/
|
|
73
|
+
get isClosed() {
|
|
74
|
+
return this._closed;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Sends the HTTP/2 client connection preface (magic PRI bytes) and the
|
|
78
|
+
* initial SETTINGS frame derived from the browser profile. Idempotent —
|
|
79
|
+
* calling more than once has no effect.
|
|
39
80
|
*/
|
|
40
81
|
sendPreface() {
|
|
41
82
|
if (this.prefaceSent)
|
|
42
83
|
return;
|
|
43
84
|
this.prefaceSent = true;
|
|
44
|
-
// 1. Magic octets
|
|
45
85
|
this.write(H2_PREFACE);
|
|
46
|
-
// 2. SETTINGS frame (ordered per profile)
|
|
47
86
|
if (this.h2Profile) {
|
|
48
87
|
this.write(buildSettingsFrame(this.h2Profile.settings));
|
|
49
|
-
|
|
88
|
+
for (const s of this.h2Profile.settings) {
|
|
89
|
+
if (s.id === 4)
|
|
90
|
+
this.initialStreamRecvWindow = s.value;
|
|
91
|
+
}
|
|
50
92
|
if (this.h2Profile.windowUpdate > 0) {
|
|
93
|
+
this.connectionRecvWindow = DEFAULT_INITIAL_WINDOW_SIZE + this.h2Profile.windowUpdate;
|
|
51
94
|
this.write(buildWindowUpdateFrame(0, this.h2Profile.windowUpdate));
|
|
52
95
|
}
|
|
53
|
-
// 4. PRIORITY frames
|
|
54
96
|
if (this.h2Profile.priorityFrames) {
|
|
55
97
|
for (const pf of this.h2Profile.priorityFrames) {
|
|
56
98
|
const payload = Buffer.allocUnsafe(5);
|
|
@@ -69,21 +111,29 @@ export class H2Client {
|
|
|
69
111
|
}
|
|
70
112
|
}
|
|
71
113
|
else {
|
|
72
|
-
|
|
114
|
+
this.initialStreamRecvWindow = 6291456;
|
|
73
115
|
this.write(buildSettingsFrame([
|
|
74
116
|
{ id: 1, value: 65536 },
|
|
75
117
|
{ id: 2, value: 0 },
|
|
76
118
|
{ id: 4, value: 6291456 },
|
|
77
119
|
{ id: 6, value: 262144 },
|
|
78
120
|
]));
|
|
121
|
+
this.connectionRecvWindow = DEFAULT_INITIAL_WINDOW_SIZE + 15663105;
|
|
79
122
|
this.write(buildWindowUpdateFrame(0, 15663105));
|
|
80
123
|
}
|
|
81
124
|
}
|
|
82
125
|
/**
|
|
83
|
-
*
|
|
126
|
+
* Sends an HTTP/2 request and buffers the entire response body before
|
|
127
|
+
* resolving. This is the standard (non-streaming) request path.
|
|
128
|
+
*
|
|
129
|
+
* @param {NLcURLRequest} req - Request descriptor.
|
|
130
|
+
* @param {Partial<RequestTimings>} [timings={}] - Partial timings object populated with `firstByte`.
|
|
131
|
+
* @returns {Promise<NLcURLResponse>} Resolves with the fully received, decompressed response.
|
|
132
|
+
* @throws {ProtocolError} If the connection is already closed or stream IDs are exhausted.
|
|
133
|
+
* @throws {TimeoutError} If the per-request timeout elapses before a response is received.
|
|
84
134
|
*/
|
|
85
135
|
async request(req, timings = {}) {
|
|
86
|
-
if (this.
|
|
136
|
+
if (this._closed) {
|
|
87
137
|
throw new ProtocolError('HTTP/2 connection is closed');
|
|
88
138
|
}
|
|
89
139
|
if (!this.prefaceSent)
|
|
@@ -92,7 +142,7 @@ export class H2Client {
|
|
|
92
142
|
if (streamId > 0x7fffffff) {
|
|
93
143
|
throw new ProtocolError('HTTP/2 stream ID exhausted; open a new connection');
|
|
94
144
|
}
|
|
95
|
-
this.nextStreamId += 2;
|
|
145
|
+
this.nextStreamId += 2;
|
|
96
146
|
return new Promise((resolve, reject) => {
|
|
97
147
|
const h2stream = {
|
|
98
148
|
id: streamId,
|
|
@@ -107,27 +157,93 @@ export class H2Client {
|
|
|
107
157
|
timings,
|
|
108
158
|
};
|
|
109
159
|
this.streams.set(streamId, h2stream);
|
|
110
|
-
|
|
160
|
+
this.streamRecvWindows.set(streamId, this.initialStreamRecvWindow);
|
|
161
|
+
this.streamSendWindows.set(streamId, this.initialStreamSendWindow);
|
|
162
|
+
const headers = this.buildRequestHeaders(req);
|
|
163
|
+
const headerBlock = this.encoder.encode(headers);
|
|
164
|
+
const hasBody = req.body !== undefined &&
|
|
165
|
+
req.body !== null &&
|
|
166
|
+
req.method !== 'GET' &&
|
|
167
|
+
req.method !== 'HEAD';
|
|
168
|
+
this.write(buildHeadersFrame(streamId, headerBlock, !hasBody, true));
|
|
169
|
+
if (hasBody) {
|
|
170
|
+
const bodyBuf = serializeBody(req.body);
|
|
171
|
+
this.sendDataWithFlowControl(streamId, bodyBuf, true);
|
|
172
|
+
}
|
|
173
|
+
const timeout = req.timeout;
|
|
174
|
+
const timeoutMs = typeof timeout === 'number' ? timeout : (timeout?.total ?? timeout?.response ?? 0);
|
|
175
|
+
if (timeoutMs > 0) {
|
|
176
|
+
h2stream.timer = setTimeout(() => {
|
|
177
|
+
if (this.streams.has(streamId)) {
|
|
178
|
+
this.streams.delete(streamId);
|
|
179
|
+
reject(new TimeoutError('HTTP/2 request timed out', 'response'));
|
|
180
|
+
}
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Sends an HTTP/2 request and returns a streaming response. The returned
|
|
187
|
+
* `NLcURLResponse` resolves once the status and headers are received;
|
|
188
|
+
* the response body is exposed as a `PassThrough` readable stream via
|
|
189
|
+
* `response.body`.
|
|
190
|
+
*
|
|
191
|
+
* @param {NLcURLRequest} req - Request descriptor.
|
|
192
|
+
* @param {Partial<RequestTimings>} [timings={}] - Partial timings object populated with `firstByte`.
|
|
193
|
+
* @returns {Promise<NLcURLResponse>} Resolves once headers are received; body is streamed via `response.body`.
|
|
194
|
+
* @throws {ProtocolError} If the connection is already closed or stream IDs are exhausted.
|
|
195
|
+
* @throws {TimeoutError} If the per-request timeout elapses before the stream begins.
|
|
196
|
+
*/
|
|
197
|
+
async streamRequest(req, timings = {}) {
|
|
198
|
+
if (this._closed) {
|
|
199
|
+
throw new ProtocolError('HTTP/2 connection is closed');
|
|
200
|
+
}
|
|
201
|
+
if (!this.prefaceSent)
|
|
202
|
+
this.sendPreface();
|
|
203
|
+
const streamId = this.nextStreamId;
|
|
204
|
+
if (streamId > 0x7fffffff) {
|
|
205
|
+
throw new ProtocolError('HTTP/2 stream ID exhausted; open a new connection');
|
|
206
|
+
}
|
|
207
|
+
this.nextStreamId += 2;
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const bodyStream = new PassThrough();
|
|
210
|
+
const h2stream = {
|
|
211
|
+
id: streamId,
|
|
212
|
+
request: req,
|
|
213
|
+
responseHeaders: new Map(),
|
|
214
|
+
responseRawHeaders: [],
|
|
215
|
+
status: 0,
|
|
216
|
+
dataChunks: [],
|
|
217
|
+
endStream: false,
|
|
218
|
+
resolve: (_resp) => { },
|
|
219
|
+
reject,
|
|
220
|
+
timings,
|
|
221
|
+
bodyStream,
|
|
222
|
+
};
|
|
223
|
+
this.streams.set(streamId, h2stream);
|
|
224
|
+
this.streamRecvWindows.set(streamId, this.initialStreamRecvWindow);
|
|
225
|
+
this.streamSendWindows.set(streamId, this.initialStreamSendWindow);
|
|
111
226
|
const headers = this.buildRequestHeaders(req);
|
|
112
227
|
const headerBlock = this.encoder.encode(headers);
|
|
113
228
|
const hasBody = req.body !== undefined &&
|
|
114
229
|
req.body !== null &&
|
|
115
230
|
req.method !== 'GET' &&
|
|
116
231
|
req.method !== 'HEAD';
|
|
117
|
-
// Send HEADERS frame
|
|
118
232
|
this.write(buildHeadersFrame(streamId, headerBlock, !hasBody, true));
|
|
119
|
-
// Send body if present
|
|
120
233
|
if (hasBody) {
|
|
121
234
|
const bodyBuf = serializeBody(req.body);
|
|
122
|
-
this.
|
|
235
|
+
this.sendDataWithFlowControl(streamId, bodyBuf, true);
|
|
123
236
|
}
|
|
124
|
-
|
|
237
|
+
h2stream.resolve = (resp) => {
|
|
238
|
+
resolve(resp);
|
|
239
|
+
};
|
|
125
240
|
const timeout = req.timeout;
|
|
126
241
|
const timeoutMs = typeof timeout === 'number' ? timeout : (timeout?.total ?? timeout?.response ?? 0);
|
|
127
242
|
if (timeoutMs > 0) {
|
|
128
243
|
h2stream.timer = setTimeout(() => {
|
|
129
244
|
if (this.streams.has(streamId)) {
|
|
130
245
|
this.streams.delete(streamId);
|
|
246
|
+
bodyStream.destroy(new TimeoutError('HTTP/2 request timed out', 'response'));
|
|
131
247
|
reject(new TimeoutError('HTTP/2 request timed out', 'response'));
|
|
132
248
|
}
|
|
133
249
|
}, timeoutMs);
|
|
@@ -135,17 +251,23 @@ export class H2Client {
|
|
|
135
251
|
});
|
|
136
252
|
}
|
|
137
253
|
/**
|
|
138
|
-
*
|
|
254
|
+
* Initiates a graceful shutdown by sending a GOAWAY frame and ending the
|
|
255
|
+
* underlying stream. In-flight requests will fail with `ProtocolError`.
|
|
139
256
|
*/
|
|
140
257
|
close() {
|
|
141
|
-
if (this.
|
|
258
|
+
if (this._closed)
|
|
142
259
|
return;
|
|
143
|
-
this.
|
|
260
|
+
this._closed = true;
|
|
144
261
|
this.write(buildGoawayFrame(0, 0));
|
|
145
262
|
this.stream.end();
|
|
263
|
+
this.onClose?.();
|
|
146
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Immediately destroys the underlying transport stream, aborting all
|
|
267
|
+
* pending requests with a `ProtocolError`.
|
|
268
|
+
*/
|
|
147
269
|
destroy() {
|
|
148
|
-
this.
|
|
270
|
+
this._closed = true;
|
|
149
271
|
this.stream.destroy();
|
|
150
272
|
for (const [, s] of this.streams) {
|
|
151
273
|
if (s.timer)
|
|
@@ -153,13 +275,15 @@ export class H2Client {
|
|
|
153
275
|
s.reject(new ProtocolError('HTTP/2 connection destroyed'));
|
|
154
276
|
}
|
|
155
277
|
this.streams.clear();
|
|
278
|
+
this.streamRecvWindows.clear();
|
|
279
|
+
this.streamSendWindows.clear();
|
|
280
|
+
this.pendingSendData.clear();
|
|
281
|
+
this.onClose?.();
|
|
156
282
|
}
|
|
157
|
-
// ---- Internal ----
|
|
158
283
|
buildRequestHeaders(req) {
|
|
159
284
|
const url = new URL(req.url);
|
|
160
285
|
const authority = url.port ? `${url.hostname}:${url.port}` : url.hostname;
|
|
161
286
|
const path = url.pathname + url.search;
|
|
162
|
-
// Pseudo headers in profile-defined order
|
|
163
287
|
const order = this.h2Profile?.pseudoHeaderOrder ?? [
|
|
164
288
|
':method',
|
|
165
289
|
':authority',
|
|
@@ -173,36 +297,37 @@ export class H2Client {
|
|
|
173
297
|
':path': path || '/',
|
|
174
298
|
};
|
|
175
299
|
const headers = [];
|
|
176
|
-
// Emit pseudo headers in profile order
|
|
177
300
|
for (const ph of order) {
|
|
178
301
|
const v = pseudoMap[ph];
|
|
179
302
|
if (v !== undefined) {
|
|
180
303
|
headers.push([ph, v]);
|
|
181
304
|
}
|
|
182
305
|
}
|
|
183
|
-
// Default headers
|
|
184
306
|
const seen = new Set(order);
|
|
307
|
+
const reqLower = new Map();
|
|
308
|
+
if (req.headers) {
|
|
309
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
310
|
+
reqLower.set(k.toLowerCase(), v);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
185
313
|
for (const [k, v] of this.defaultHeaders) {
|
|
186
314
|
const lower = k.toLowerCase();
|
|
187
315
|
if (!seen.has(lower)) {
|
|
188
316
|
seen.add(lower);
|
|
189
|
-
headers.push([lower, v]);
|
|
317
|
+
headers.push([lower, reqLower.get(lower) ?? v]);
|
|
318
|
+
reqLower.delete(lower);
|
|
190
319
|
}
|
|
191
320
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (!seen.has(lower)) {
|
|
197
|
-
seen.add(lower);
|
|
198
|
-
headers.push([lower, v]);
|
|
199
|
-
}
|
|
321
|
+
for (const [lower, v] of reqLower) {
|
|
322
|
+
if (!seen.has(lower)) {
|
|
323
|
+
seen.add(lower);
|
|
324
|
+
headers.push([lower, v]);
|
|
200
325
|
}
|
|
201
326
|
}
|
|
202
327
|
return headers;
|
|
203
328
|
}
|
|
204
329
|
write(data) {
|
|
205
|
-
if (!this.
|
|
330
|
+
if (!this._closed) {
|
|
206
331
|
this.stream.write(data);
|
|
207
332
|
}
|
|
208
333
|
}
|
|
@@ -220,10 +345,22 @@ export class H2Client {
|
|
|
220
345
|
}
|
|
221
346
|
}
|
|
222
347
|
handleFrame(frame) {
|
|
348
|
+
if (this.pendingHeaderStreamId !== null && frame.type !== FrameType.CONTINUATION) {
|
|
349
|
+
this._closed = true;
|
|
350
|
+
this.write(buildGoawayFrame(0, 1));
|
|
351
|
+
for (const [, s] of this.streams) {
|
|
352
|
+
if (s.timer)
|
|
353
|
+
clearTimeout(s.timer);
|
|
354
|
+
s.reject(new ProtocolError('Protocol error: expected CONTINUATION frame', 1));
|
|
355
|
+
}
|
|
356
|
+
this.streams.clear();
|
|
357
|
+
this.onClose?.();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
223
360
|
switch (frame.type) {
|
|
224
361
|
case FrameType.SETTINGS:
|
|
225
362
|
if (!(frame.flags & Flags.ACK)) {
|
|
226
|
-
|
|
363
|
+
this.applyServerSettings(frame.payload);
|
|
227
364
|
this.write(buildSettingsFrame([], true));
|
|
228
365
|
}
|
|
229
366
|
break;
|
|
@@ -231,22 +368,56 @@ export class H2Client {
|
|
|
231
368
|
const s = this.streams.get(frame.streamId);
|
|
232
369
|
if (!s)
|
|
233
370
|
break;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
s.
|
|
371
|
+
let headerPayload = frame.payload;
|
|
372
|
+
if (frame.flags & Flags.PADDED) {
|
|
373
|
+
if (headerPayload.length < 1) {
|
|
374
|
+
s.reject(new ProtocolError('HEADERS frame with PADDED flag but no pad length'));
|
|
375
|
+
this.streams.delete(frame.streamId);
|
|
376
|
+
break;
|
|
238
377
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
378
|
+
const padLength = headerPayload[0];
|
|
379
|
+
if (padLength >= headerPayload.length) {
|
|
380
|
+
s.reject(new ProtocolError('HEADERS pad length exceeds payload'));
|
|
381
|
+
this.streams.delete(frame.streamId);
|
|
382
|
+
break;
|
|
243
383
|
}
|
|
244
|
-
|
|
245
|
-
|
|
384
|
+
headerPayload = headerPayload.subarray(1, headerPayload.length - padLength);
|
|
385
|
+
}
|
|
386
|
+
if (frame.flags & Flags.PRIORITY) {
|
|
387
|
+
if (headerPayload.length < 5) {
|
|
388
|
+
s.reject(new ProtocolError('HEADERS frame with PRIORITY but insufficient data'));
|
|
389
|
+
this.streams.delete(frame.streamId);
|
|
390
|
+
break;
|
|
246
391
|
}
|
|
392
|
+
headerPayload = headerPayload.subarray(5);
|
|
247
393
|
}
|
|
248
|
-
if (frame.flags & Flags.
|
|
249
|
-
this.
|
|
394
|
+
if (!(frame.flags & Flags.END_HEADERS)) {
|
|
395
|
+
this.pendingHeaderStreamId = frame.streamId;
|
|
396
|
+
this.pendingHeaderBlock = Buffer.from(headerPayload);
|
|
397
|
+
this.pendingHeaderFlags = frame.flags;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
this.processDecodedHeaders(s, headerPayload, frame.flags);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
case FrameType.CONTINUATION: {
|
|
404
|
+
if (this.pendingHeaderStreamId === null || frame.streamId !== this.pendingHeaderStreamId) {
|
|
405
|
+
this._closed = true;
|
|
406
|
+
this.write(buildGoawayFrame(0, 1));
|
|
407
|
+
this.onClose?.();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.pendingHeaderBlock = Buffer.concat([this.pendingHeaderBlock, frame.payload]);
|
|
411
|
+
if (frame.flags & Flags.END_HEADERS) {
|
|
412
|
+
const s = this.streams.get(this.pendingHeaderStreamId);
|
|
413
|
+
const headerBlock = this.pendingHeaderBlock;
|
|
414
|
+
const flags = this.pendingHeaderFlags;
|
|
415
|
+
this.pendingHeaderStreamId = null;
|
|
416
|
+
this.pendingHeaderBlock = null;
|
|
417
|
+
this.pendingHeaderFlags = 0;
|
|
418
|
+
if (s) {
|
|
419
|
+
this.processDecodedHeaders(s, headerBlock, flags);
|
|
420
|
+
}
|
|
250
421
|
}
|
|
251
422
|
break;
|
|
252
423
|
}
|
|
@@ -254,7 +425,29 @@ export class H2Client {
|
|
|
254
425
|
const s = this.streams.get(frame.streamId);
|
|
255
426
|
if (!s)
|
|
256
427
|
break;
|
|
257
|
-
|
|
428
|
+
let dataPayload = frame.payload;
|
|
429
|
+
if (frame.flags & Flags.PADDED) {
|
|
430
|
+
if (dataPayload.length < 1) {
|
|
431
|
+
s.reject(new ProtocolError('DATA frame with PADDED flag but no pad length'));
|
|
432
|
+
this.streams.delete(frame.streamId);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
const padLength = dataPayload[0];
|
|
436
|
+
if (padLength >= dataPayload.length) {
|
|
437
|
+
s.reject(new ProtocolError('DATA pad length exceeds payload'));
|
|
438
|
+
this.streams.delete(frame.streamId);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
dataPayload = dataPayload.subarray(1, dataPayload.length - padLength);
|
|
442
|
+
}
|
|
443
|
+
const data = Buffer.from(dataPayload);
|
|
444
|
+
if (s.bodyStream) {
|
|
445
|
+
s.bodyStream.write(data);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
s.dataChunks.push(data);
|
|
449
|
+
}
|
|
450
|
+
this.consumeRecvWindow(frame.streamId, frame.payload.length);
|
|
258
451
|
if (frame.flags & Flags.END_STREAM) {
|
|
259
452
|
this.finalizeStream(s);
|
|
260
453
|
}
|
|
@@ -266,8 +459,14 @@ export class H2Client {
|
|
|
266
459
|
if (s.timer)
|
|
267
460
|
clearTimeout(s.timer);
|
|
268
461
|
this.streams.delete(frame.streamId);
|
|
462
|
+
this.streamRecvWindows.delete(frame.streamId);
|
|
463
|
+
this.streamSendWindows.delete(frame.streamId);
|
|
464
|
+
this.pendingSendData.delete(frame.streamId);
|
|
269
465
|
const errorCode = frame.payload.readUInt32BE(0);
|
|
270
|
-
|
|
466
|
+
const err = new ProtocolError(`HTTP/2 stream reset: error code ${errorCode}`, errorCode);
|
|
467
|
+
if (s.bodyStream)
|
|
468
|
+
s.bodyStream.destroy(err);
|
|
469
|
+
s.reject(err);
|
|
271
470
|
}
|
|
272
471
|
break;
|
|
273
472
|
}
|
|
@@ -277,28 +476,124 @@ export class H2Client {
|
|
|
277
476
|
}
|
|
278
477
|
break;
|
|
279
478
|
case FrameType.GOAWAY: {
|
|
280
|
-
this.
|
|
479
|
+
this._closed = true;
|
|
480
|
+
this._goawayReceived = true;
|
|
481
|
+
const lastStreamId = frame.payload.readUInt32BE(0) & 0x7fffffff;
|
|
281
482
|
const errorCode = frame.payload.readUInt32BE(4);
|
|
282
|
-
|
|
283
|
-
|
|
483
|
+
for (const [id, s] of this.streams) {
|
|
484
|
+
if (id > lastStreamId) {
|
|
284
485
|
if (s.timer)
|
|
285
486
|
clearTimeout(s.timer);
|
|
286
|
-
|
|
487
|
+
if (errorCode !== 0) {
|
|
488
|
+
s.reject(new ProtocolError(`HTTP/2 GOAWAY: error code ${errorCode}`, errorCode));
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
s.reject(new ProtocolError('HTTP/2 GOAWAY: graceful shutdown', 0));
|
|
492
|
+
}
|
|
493
|
+
this.streams.delete(id);
|
|
287
494
|
}
|
|
288
|
-
|
|
495
|
+
}
|
|
496
|
+
if (this.streams.size === 0) {
|
|
497
|
+
this.onClose?.();
|
|
289
498
|
}
|
|
290
499
|
break;
|
|
291
500
|
}
|
|
292
|
-
case FrameType.WINDOW_UPDATE:
|
|
293
|
-
|
|
294
|
-
|
|
501
|
+
case FrameType.WINDOW_UPDATE: {
|
|
502
|
+
const increment = frame.payload.readUInt32BE(0) & 0x7fffffff;
|
|
503
|
+
if (increment === 0) {
|
|
504
|
+
if (frame.streamId === 0) {
|
|
505
|
+
this._closed = true;
|
|
506
|
+
this.write(buildGoawayFrame(0, 1));
|
|
507
|
+
this.onClose?.();
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
this.write(buildRstStreamFrame(frame.streamId, 1));
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
if (frame.streamId === 0) {
|
|
515
|
+
this.connectionSendWindow += increment;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
const current = this.streamSendWindows.get(frame.streamId) ?? this.initialStreamSendWindow;
|
|
519
|
+
this.streamSendWindows.set(frame.streamId, current + increment);
|
|
520
|
+
}
|
|
521
|
+
this.flushPendingSendData(frame.streamId);
|
|
295
522
|
break;
|
|
523
|
+
}
|
|
296
524
|
}
|
|
297
525
|
}
|
|
526
|
+
processDecodedHeaders(s, headerBlock, flags) {
|
|
527
|
+
const headers = this.decoder.decode(headerBlock);
|
|
528
|
+
for (const [name, value] of headers) {
|
|
529
|
+
if (name === ':status') {
|
|
530
|
+
s.status = parseInt(value, 10);
|
|
531
|
+
}
|
|
532
|
+
s.responseRawHeaders.push([name, value]);
|
|
533
|
+
const existing = s.responseHeaders.get(name);
|
|
534
|
+
if (existing !== undefined) {
|
|
535
|
+
const sep = name === 'set-cookie' ? '; ' : ', ';
|
|
536
|
+
s.responseHeaders.set(name, existing + sep + value);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
s.responseHeaders.set(name, value);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (flags & Flags.END_STREAM) {
|
|
543
|
+
this.finalizeStream(s);
|
|
544
|
+
}
|
|
545
|
+
else if (s.bodyStream && s.status > 0) {
|
|
546
|
+
this.resolveStreamingResponse(s);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
resolveStreamingResponse(s) {
|
|
550
|
+
const responseHeaders = {};
|
|
551
|
+
for (const [k, v] of s.responseHeaders) {
|
|
552
|
+
if (!k.startsWith(':')) {
|
|
553
|
+
responseHeaders[k] = v;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const encoding = s.responseHeaders.get('content-encoding');
|
|
557
|
+
const decompressor = createDecompressStream(encoding);
|
|
558
|
+
const outputStream = decompressor ? s.bodyStream.pipe(decompressor) : s.bodyStream;
|
|
559
|
+
const response = new NLcURLResponse({
|
|
560
|
+
status: s.status,
|
|
561
|
+
statusText: '',
|
|
562
|
+
headers: responseHeaders,
|
|
563
|
+
rawHeaders: s.responseRawHeaders.filter(([k]) => !k.startsWith(':')),
|
|
564
|
+
rawBody: Buffer.alloc(0),
|
|
565
|
+
body: outputStream,
|
|
566
|
+
httpVersion: 'h2',
|
|
567
|
+
url: s.request.url,
|
|
568
|
+
redirectCount: 0,
|
|
569
|
+
timings: {
|
|
570
|
+
dns: s.timings.dns ?? 0,
|
|
571
|
+
connect: s.timings.connect ?? 0,
|
|
572
|
+
tls: s.timings.tls ?? 0,
|
|
573
|
+
firstByte: s.timings.firstByte ?? 0,
|
|
574
|
+
total: 0,
|
|
575
|
+
},
|
|
576
|
+
request: {
|
|
577
|
+
url: s.request.url,
|
|
578
|
+
method: s.request.method ?? 'GET',
|
|
579
|
+
headers: s.request.headers ?? {},
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
s.resolve(response);
|
|
583
|
+
}
|
|
298
584
|
async finalizeStream(s) {
|
|
299
585
|
if (s.timer)
|
|
300
586
|
clearTimeout(s.timer);
|
|
301
587
|
this.streams.delete(s.id);
|
|
588
|
+
this.streamRecvWindows.delete(s.id);
|
|
589
|
+
if (s.bodyStream) {
|
|
590
|
+
if (s.status > 0) {
|
|
591
|
+
this.resolveStreamingResponse(s);
|
|
592
|
+
}
|
|
593
|
+
s.bodyStream.end();
|
|
594
|
+
this.checkGoawayDrain();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
302
597
|
const rawBody = Buffer.concat(s.dataChunks);
|
|
303
598
|
const encoding = s.responseHeaders.get('content-encoding');
|
|
304
599
|
let body;
|
|
@@ -337,27 +632,154 @@ export class H2Client {
|
|
|
337
632
|
},
|
|
338
633
|
});
|
|
339
634
|
s.resolve(response);
|
|
635
|
+
this.checkGoawayDrain();
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* After a GOAWAY has been received, fires `onClose` once the last remaining
|
|
639
|
+
* stream (with ID ≤ lastStreamId) has been finalized.
|
|
640
|
+
*/
|
|
641
|
+
checkGoawayDrain() {
|
|
642
|
+
if (this._goawayReceived && this.streams.size === 0) {
|
|
643
|
+
this.onClose?.();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
applyServerSettings(payload) {
|
|
647
|
+
for (let i = 0; i + 5 < payload.length; i += 6) {
|
|
648
|
+
const id = payload.readUInt16BE(i);
|
|
649
|
+
const value = payload.readUInt32BE(i + 2);
|
|
650
|
+
switch (id) {
|
|
651
|
+
case 3:
|
|
652
|
+
this.serverMaxConcurrentStreams = value;
|
|
653
|
+
break;
|
|
654
|
+
case 4:
|
|
655
|
+
{
|
|
656
|
+
const recvDelta = value - this.initialStreamRecvWindow;
|
|
657
|
+
this.initialStreamRecvWindow = value;
|
|
658
|
+
for (const [streamId, win] of this.streamRecvWindows) {
|
|
659
|
+
this.streamRecvWindows.set(streamId, win + recvDelta);
|
|
660
|
+
}
|
|
661
|
+
const sendDelta = value - this.initialStreamSendWindow;
|
|
662
|
+
this.initialStreamSendWindow = value;
|
|
663
|
+
for (const [streamId, win] of this.streamSendWindows) {
|
|
664
|
+
this.streamSendWindows.set(streamId, win + sendDelta);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
break;
|
|
668
|
+
case 5:
|
|
669
|
+
this.serverMaxFrameSize = value;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
340
673
|
}
|
|
341
|
-
|
|
674
|
+
consumeRecvWindow(streamId, length) {
|
|
675
|
+
this.connectionRecvWindow -= length;
|
|
676
|
+
const connMax = this.h2Profile
|
|
677
|
+
? DEFAULT_INITIAL_WINDOW_SIZE + (this.h2Profile.windowUpdate || 0)
|
|
678
|
+
: DEFAULT_INITIAL_WINDOW_SIZE + 15663105;
|
|
679
|
+
const connThreshold = Math.floor(connMax * WINDOW_UPDATE_THRESHOLD_RATIO);
|
|
680
|
+
if (this.connectionRecvWindow < connThreshold) {
|
|
681
|
+
const increment = connMax - this.connectionRecvWindow;
|
|
682
|
+
this.connectionRecvWindow += increment;
|
|
683
|
+
this.write(buildWindowUpdateFrame(0, increment));
|
|
684
|
+
}
|
|
685
|
+
const streamWin = this.streamRecvWindows.get(streamId);
|
|
686
|
+
if (streamWin !== undefined) {
|
|
687
|
+
const newWin = streamWin - length;
|
|
688
|
+
const streamThreshold = Math.floor(this.initialStreamRecvWindow * WINDOW_UPDATE_THRESHOLD_RATIO);
|
|
689
|
+
if (newWin < streamThreshold) {
|
|
690
|
+
const increment = this.initialStreamRecvWindow - newWin;
|
|
691
|
+
this.streamRecvWindows.set(streamId, newWin + increment);
|
|
692
|
+
this.write(buildWindowUpdateFrame(streamId, increment));
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
this.streamRecvWindows.set(streamId, newWin);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
handleError(err) {
|
|
342
700
|
for (const [, s] of this.streams) {
|
|
343
701
|
if (s.timer)
|
|
344
702
|
clearTimeout(s.timer);
|
|
345
703
|
s.reject(new HTTPError(err.message, 0));
|
|
346
704
|
}
|
|
347
705
|
this.streams.clear();
|
|
348
|
-
this.
|
|
706
|
+
this.streamRecvWindows.clear();
|
|
707
|
+
this.streamSendWindows.clear();
|
|
708
|
+
this.pendingSendData.clear();
|
|
709
|
+
this._closed = true;
|
|
710
|
+
this.onClose?.();
|
|
349
711
|
}
|
|
350
|
-
|
|
712
|
+
handleClose() {
|
|
351
713
|
for (const [, s] of this.streams) {
|
|
352
714
|
if (s.timer)
|
|
353
715
|
clearTimeout(s.timer);
|
|
354
716
|
s.reject(new HTTPError('HTTP/2 connection closed', 0));
|
|
355
717
|
}
|
|
356
718
|
this.streams.clear();
|
|
357
|
-
this.
|
|
719
|
+
this.streamRecvWindows.clear();
|
|
720
|
+
this.streamSendWindows.clear();
|
|
721
|
+
this.pendingSendData.clear();
|
|
722
|
+
this._closed = true;
|
|
723
|
+
this.onClose?.();
|
|
724
|
+
}
|
|
725
|
+
sendDataWithFlowControl(streamId, data, endStream) {
|
|
726
|
+
let offset = 0;
|
|
727
|
+
while (offset < data.length) {
|
|
728
|
+
const streamWin = this.streamSendWindows.get(streamId) ?? this.initialStreamSendWindow;
|
|
729
|
+
const maxSend = Math.min(this.connectionSendWindow, streamWin, this.serverMaxFrameSize);
|
|
730
|
+
if (maxSend <= 0) {
|
|
731
|
+
const remaining = data.subarray(offset);
|
|
732
|
+
const pending = this.pendingSendData.get(streamId) ?? [];
|
|
733
|
+
pending.push({ data: remaining, endStream, resolve: () => { } });
|
|
734
|
+
this.pendingSendData.set(streamId, pending);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const chunkSize = Math.min(maxSend, data.length - offset);
|
|
738
|
+
const chunk = data.subarray(offset, offset + chunkSize);
|
|
739
|
+
offset += chunkSize;
|
|
740
|
+
const isLast = offset >= data.length && endStream;
|
|
741
|
+
this.write(buildDataFrame(streamId, chunk, isLast));
|
|
742
|
+
this.connectionSendWindow -= chunkSize;
|
|
743
|
+
const currentStreamWin = this.streamSendWindows.get(streamId) ?? this.initialStreamSendWindow;
|
|
744
|
+
this.streamSendWindows.set(streamId, currentStreamWin - chunkSize);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
flushPendingSendData(streamId) {
|
|
748
|
+
const streamIds = streamId === 0
|
|
749
|
+
? [...this.pendingSendData.keys()]
|
|
750
|
+
: [streamId];
|
|
751
|
+
for (const sid of streamIds) {
|
|
752
|
+
const pending = this.pendingSendData.get(sid);
|
|
753
|
+
if (!pending || pending.length === 0)
|
|
754
|
+
continue;
|
|
755
|
+
while (pending.length > 0) {
|
|
756
|
+
const item = pending[0];
|
|
757
|
+
const streamWin = this.streamSendWindows.get(sid) ?? this.initialStreamSendWindow;
|
|
758
|
+
const maxSend = Math.min(this.connectionSendWindow, streamWin, this.serverMaxFrameSize);
|
|
759
|
+
if (maxSend <= 0)
|
|
760
|
+
break;
|
|
761
|
+
const chunkSize = Math.min(maxSend, item.data.length);
|
|
762
|
+
const chunk = item.data.subarray(0, chunkSize);
|
|
763
|
+
const remaining = item.data.subarray(chunkSize);
|
|
764
|
+
const isLast = remaining.length === 0 && item.endStream;
|
|
765
|
+
this.write(buildDataFrame(sid, chunk, isLast));
|
|
766
|
+
this.connectionSendWindow -= chunkSize;
|
|
767
|
+
const currentStreamWin = this.streamSendWindows.get(sid) ?? this.initialStreamSendWindow;
|
|
768
|
+
this.streamSendWindows.set(sid, currentStreamWin - chunkSize);
|
|
769
|
+
if (remaining.length === 0) {
|
|
770
|
+
pending.shift();
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
item.data = remaining;
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (pending.length === 0) {
|
|
778
|
+
this.pendingSendData.delete(sid);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
358
781
|
}
|
|
359
782
|
}
|
|
360
|
-
// ---- Body serialization ----
|
|
361
783
|
function serializeBody(body) {
|
|
362
784
|
if (body === null || body === undefined)
|
|
363
785
|
return Buffer.alloc(0);
|