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