tunnelfetch 1.0.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/LICENSE +28 -0
- package/README.md +617 -0
- package/README.zh-CN.md +470 -0
- package/package.json +74 -0
- package/src/client/cookies.js +429 -0
- package/src/client/decode.js +346 -0
- package/src/client/redirect.js +249 -0
- package/src/client.js +704 -0
- package/src/errors.js +181 -0
- package/src/http1/chunked.js +289 -0
- package/src/http1/index.js +10 -0
- package/src/http1/request.js +143 -0
- package/src/http1/response.js +493 -0
- package/src/http2/connection.js +1170 -0
- package/src/http2/constants.js +129 -0
- package/src/http2/frames.js +291 -0
- package/src/http2/hpack.js +420 -0
- package/src/http2/huffman.js +203 -0
- package/src/http2/index.js +21 -0
- package/src/index.js +46 -0
- package/src/pool.js +256 -0
- package/src/proxy/direct.js +62 -0
- package/src/proxy/http-connect.js +206 -0
- package/src/proxy/index.js +197 -0
- package/src/proxy/socks5.js +344 -0
- package/src/tls/aead.js +263 -0
- package/src/tls/connect.js +407 -0
- package/src/tls/constants.js +334 -0
- package/src/tls/extensions.js +376 -0
- package/src/tls/handshake-messages.js +901 -0
- package/src/tls/handshake.js +568 -0
- package/src/tls/handshake12.js +507 -0
- package/src/tls/index.js +44 -0
- package/src/tls/keyschedule.js +473 -0
- package/src/tls/record.js +872 -0
- package/src/tls/tickets.js +145 -0
- package/src/tls/transcript.js +101 -0
- package/src/tls/wire.js +224 -0
- package/src/transport.js +296 -0
- package/src/trust/der.js +551 -0
- package/src/trust/index.js +375 -0
- package/src/trust/name.js +235 -0
- package/src/trust/ocsp.js +759 -0
- package/src/trust/path.js +595 -0
- package/src/trust/roots.js +454 -0
- package/src/trust/x509.js +902 -0
- package/src/util/bytes.js +470 -0
- package/src/util/deadline.js +266 -0
- package/src/warmup-fixture.js +85 -0
- package/src/warmup.js +243 -0
- package/types/client/cookies.d.ts +159 -0
- package/types/client/decode.d.ts +54 -0
- package/types/client/redirect.d.ts +96 -0
- package/types/client.d.ts +323 -0
- package/types/errors.d.ts +141 -0
- package/types/http1/chunked.d.ts +48 -0
- package/types/http1/index.d.ts +3 -0
- package/types/http1/request.d.ts +44 -0
- package/types/http1/response.d.ts +183 -0
- package/types/http2/connection.d.ts +282 -0
- package/types/http2/constants.d.ts +95 -0
- package/types/http2/frames.d.ts +116 -0
- package/types/http2/hpack.d.ts +99 -0
- package/types/http2/huffman.d.ts +21 -0
- package/types/http2/index.d.ts +5 -0
- package/types/index.d.ts +17 -0
- package/types/pool.d.ts +135 -0
- package/types/proxy/direct.d.ts +26 -0
- package/types/proxy/http-connect.d.ts +37 -0
- package/types/proxy/index.d.ts +62 -0
- package/types/proxy/socks5.d.ts +47 -0
- package/types/tls/aead.d.ts +67 -0
- package/types/tls/connect.d.ts +280 -0
- package/types/tls/constants.d.ts +275 -0
- package/types/tls/extensions.d.ts +195 -0
- package/types/tls/handshake-messages.d.ts +430 -0
- package/types/tls/handshake.d.ts +90 -0
- package/types/tls/handshake12.d.ts +35 -0
- package/types/tls/index.d.ts +9 -0
- package/types/tls/keyschedule.d.ts +272 -0
- package/types/tls/record.d.ts +361 -0
- package/types/tls/tickets.d.ts +66 -0
- package/types/tls/transcript.d.ts +52 -0
- package/types/tls/wire.d.ts +106 -0
- package/types/transport.d.ts +222 -0
- package/types/trust/der.d.ts +239 -0
- package/types/trust/index.d.ts +194 -0
- package/types/trust/name.d.ts +33 -0
- package/types/trust/ocsp.d.ts +138 -0
- package/types/trust/path.d.ts +139 -0
- package/types/trust/roots.d.ts +36 -0
- package/types/trust/x509.d.ts +401 -0
- package/types/util/bytes.d.ts +183 -0
- package/types/util/deadline.d.ts +133 -0
- package/types/warmup-fixture.d.ts +11 -0
- package/types/warmup.d.ts +45 -0
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
// The HTTP/2 connection engine (RFC 9113): one TCP+TLS byte duplex, many concurrent streams.
|
|
2
|
+
//
|
|
3
|
+
// CONNECTION SHARING — the decision the pool header warns about. HTTP/1.1 makes reuse safe by
|
|
4
|
+
// EXCLUSIVE CHECKOUT: a connection leaves the pool for one request and returns only when that
|
|
5
|
+
// response's body has reached its framed end, so bytes for one response can never be read as
|
|
6
|
+
// another's. HTTP/2 multiplexes, so exclusive checkout is the wrong model — the connection is
|
|
7
|
+
// never checked out at all. Here the same invariant ("bytes of one response are never delivered
|
|
8
|
+
// as another's") is carried by the stream id instead: exactly ONE reader — the frame loop below —
|
|
9
|
+
// touches the transport, it demultiplexes each DATA frame to the stream its id names, and every
|
|
10
|
+
// stream has its own body queue. A response's bytes reach a caller only through the stream object
|
|
11
|
+
// that owns that id, which is a different object per request. There is no shared byte cursor to
|
|
12
|
+
// desynchronise, so there is nothing to check out. The connection is held in a per-Client registry
|
|
13
|
+
// (see client.js) keyed exactly like the h1 pool, and a single connection serves every concurrent
|
|
14
|
+
// request to that key until it goes away.
|
|
15
|
+
//
|
|
16
|
+
// FLOW CONTROL is the other load-bearing part, and it is wired to CONSUMPTION, not arrival. When a
|
|
17
|
+
// DATA frame lands, its bytes are debited from our receive window immediately (the peer has spent
|
|
18
|
+
// that credit); the window is only reopened — a WINDOW_UPDATE is only sent — as the CONSUMER drains
|
|
19
|
+
// the body. Reopening on arrival instead would defeat backpressure (a fast server + slow reader
|
|
20
|
+
// would buffer without bound); never reopening at all would stall every body larger than the
|
|
21
|
+
// initial window forever, which looks exactly like a hung server. Both failure modes are real and
|
|
22
|
+
// each is guarded by a test.
|
|
23
|
+
|
|
24
|
+
import { Http2Error, codes } from '../errors.js';
|
|
25
|
+
import { ByteReader, ByteWriter, concat } from '../util/bytes.js';
|
|
26
|
+
import {
|
|
27
|
+
CLIENT_CONNECTION_WINDOW,
|
|
28
|
+
CLIENT_CONNECTION_WINDOW_INCREMENT,
|
|
29
|
+
CLIENT_INITIAL_WINDOW_SIZE,
|
|
30
|
+
CLIENT_MAX_CONCURRENT_STREAMS,
|
|
31
|
+
CONNECTION_PREFACE,
|
|
32
|
+
DEFAULT_INITIAL_WINDOW,
|
|
33
|
+
DEFAULT_MAX_FRAME_SIZE,
|
|
34
|
+
FLAG,
|
|
35
|
+
FRAME,
|
|
36
|
+
H2_ERROR,
|
|
37
|
+
H2_ERROR_NAME,
|
|
38
|
+
MAX_ALLOWED_FRAME_SIZE,
|
|
39
|
+
MAX_WINDOW,
|
|
40
|
+
PSEUDO_HEADER_ORDER,
|
|
41
|
+
SETTINGS,
|
|
42
|
+
} from './constants.js';
|
|
43
|
+
import {
|
|
44
|
+
continuationFrame,
|
|
45
|
+
dataFrame,
|
|
46
|
+
goawayFrame,
|
|
47
|
+
headersBlockFragment,
|
|
48
|
+
headersFrame,
|
|
49
|
+
parseGoaway,
|
|
50
|
+
parseRstStream,
|
|
51
|
+
parseSettings,
|
|
52
|
+
parseWindowUpdate,
|
|
53
|
+
pingFrame,
|
|
54
|
+
readFrame,
|
|
55
|
+
rstStreamFrame,
|
|
56
|
+
serializeFrame,
|
|
57
|
+
settingsFrame,
|
|
58
|
+
stripPadding,
|
|
59
|
+
windowUpdateFrame,
|
|
60
|
+
} from './frames.js';
|
|
61
|
+
import { DEFAULT_HEADER_TABLE_SIZE, HpackDecoder, encodeHeaderBlock } from './hpack.js';
|
|
62
|
+
|
|
63
|
+
/** A promise with its settle functions exposed, rejection pre-observed so an unconsumed
|
|
64
|
+
* trailers/completed promise can never crash the isolate. */
|
|
65
|
+
function deferred() {
|
|
66
|
+
let resolve;
|
|
67
|
+
let reject;
|
|
68
|
+
const promise = new Promise((res, rej) => {
|
|
69
|
+
resolve = res;
|
|
70
|
+
reject = rej;
|
|
71
|
+
});
|
|
72
|
+
promise.catch(() => {});
|
|
73
|
+
return { promise, resolve, reject, settled: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** RFC 9113 s8.2.2 / s8.2.3: connection-specific header fields a client must not send in h2, and
|
|
77
|
+
* a server must not send either. Their presence in a response is malformed; we drop them on the
|
|
78
|
+
* request side and reject them on the response side. `te` is allowed only when its value is
|
|
79
|
+
* exactly "trailers", handled separately. */
|
|
80
|
+
const FORBIDDEN_H2_HEADERS = new Set([
|
|
81
|
+
'connection',
|
|
82
|
+
'proxy-connection',
|
|
83
|
+
'keep-alive',
|
|
84
|
+
'transfer-encoding',
|
|
85
|
+
'upgrade',
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @typedef {ReadableStream<Uint8Array> & { completed: Promise<boolean>,
|
|
90
|
+
* trailers: Promise<Headers | null> }} BodyStream
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @typedef {object} Http2ResponseHead
|
|
95
|
+
* @property {number} status
|
|
96
|
+
* @property {string} statusText always '' — HTTP/2 has no reason phrase
|
|
97
|
+
* @property {Headers} headers
|
|
98
|
+
* @property {string[]} setCookie one entry per set-cookie field, kept separate like the h1 path
|
|
99
|
+
* @property {'2'} httpVersion
|
|
100
|
+
* @property {BodyStream} body
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Raised by request() when the connection cannot take the stream but the request PROVABLY was not
|
|
105
|
+
* processed (going away, or refused). It mirrors h1's serverNeverSawIt: only a request the server
|
|
106
|
+
* demonstrably never saw may be re-sent, so client.js can safely open a fresh connection and retry.
|
|
107
|
+
*/
|
|
108
|
+
export class Http2Retryable extends Http2Error {}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @typedef {object} Http2ConnectionOptions
|
|
112
|
+
* @property {import('../transport.js').ConnectionInfo} [info] provenance attached to responses
|
|
113
|
+
* @property {number} [initialWindowSize] our SETTINGS_INITIAL_WINDOW_SIZE (receive window per
|
|
114
|
+
* stream). Defaults to curl's 10 MiB; tests lower it to exercise flow control.
|
|
115
|
+
* @property {number} [connectionWindow] the connection receive window we open with a WINDOW_UPDATE
|
|
116
|
+
* right after SETTINGS. Defaults to curl's 1000 MiB.
|
|
117
|
+
* @property {number} [maxConcurrentStreams] our advertised SETTINGS_MAX_CONCURRENT_STREAMS.
|
|
118
|
+
* @property {number} [maxHeaderTableSize] our advertised SETTINGS_HEADER_TABLE_SIZE.
|
|
119
|
+
* @property {number} [maxHeaderListSize] self-protection cap on a decoded response header list.
|
|
120
|
+
* @property {(err: Error | null) => void} [onClose] called once when the connection dies, so a
|
|
121
|
+
* registry can drop it.
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
export class Http2Connection {
|
|
125
|
+
/**
|
|
126
|
+
* @param {import('./frames.js').ByteDuplex | { readable: ReadableStream<Uint8Array>,
|
|
127
|
+
* writable: WritableStream<Uint8Array> }} duplex plaintext transport (a TLS session's
|
|
128
|
+
* plaintextDuplex, or a raw socket for cleartext h2 in tests)
|
|
129
|
+
* @param {Http2ConnectionOptions} [opts]
|
|
130
|
+
*/
|
|
131
|
+
constructor(duplex, opts = {}) {
|
|
132
|
+
this._reader = new ByteReader(duplex.readable);
|
|
133
|
+
this._writer = new ByteWriter(duplex.writable);
|
|
134
|
+
this._closeTransport = () => duplex.close?.();
|
|
135
|
+
this.info = opts.info ?? null;
|
|
136
|
+
|
|
137
|
+
// Our advertised settings. The defaults ARE the fingerprint (see constants.js); overrides
|
|
138
|
+
// exist for tests, and shifting them shifts what the server sees, so production leaves them.
|
|
139
|
+
this._ourInitialWindow = opts.initialWindowSize ?? CLIENT_INITIAL_WINDOW_SIZE;
|
|
140
|
+
this._ourConnWindow = opts.connectionWindow ?? CLIENT_CONNECTION_WINDOW;
|
|
141
|
+
this._ourMaxConcurrent = opts.maxConcurrentStreams ?? CLIENT_MAX_CONCURRENT_STREAMS;
|
|
142
|
+
this._ourHeaderTableSize = opts.maxHeaderTableSize ?? DEFAULT_HEADER_TABLE_SIZE;
|
|
143
|
+
|
|
144
|
+
// Peer settings, at their protocol defaults until the server's SETTINGS arrives.
|
|
145
|
+
this._peerInitialWindow = DEFAULT_INITIAL_WINDOW;
|
|
146
|
+
this._peerMaxFrameSize = DEFAULT_MAX_FRAME_SIZE;
|
|
147
|
+
this._peerMaxConcurrent = Infinity; // unknown until told; unlimited by default (RFC 9113 s6.5.2)
|
|
148
|
+
this._peerHeaderTableSize = DEFAULT_HEADER_TABLE_SIZE;
|
|
149
|
+
|
|
150
|
+
// Receive-side flow control (what the peer may send us). Connection window jumps to
|
|
151
|
+
// `_ourConnWindow` the moment our opening WINDOW_UPDATE is written.
|
|
152
|
+
this._connRecvWindow = DEFAULT_INITIAL_WINDOW;
|
|
153
|
+
this._connConsumed = 0; // bytes consumed since the last connection WINDOW_UPDATE we sent
|
|
154
|
+
// Send-side flow control (what WE may send the peer), connection level.
|
|
155
|
+
this._connSendWindow = DEFAULT_INITIAL_WINDOW;
|
|
156
|
+
/** @type {Array<() => void>} wakers for senders blocked on the connection window */
|
|
157
|
+
this._connSendWaiters = [];
|
|
158
|
+
|
|
159
|
+
this._decoder = new HpackDecoder({
|
|
160
|
+
maxTableSize: this._ourHeaderTableSize,
|
|
161
|
+
maxHeaderListSize: opts.maxHeaderListSize,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** @type {Map<number, any>} live streams by id */
|
|
165
|
+
this._streams = new Map();
|
|
166
|
+
this._nextStreamId = 1; // client-initiated streams are odd (RFC 9113 s5.1.1)
|
|
167
|
+
this._lastPeerStreamId = 0;
|
|
168
|
+
|
|
169
|
+
// Header-block continuation state: while assembling a HEADERS+CONTINUATION run, no other frame
|
|
170
|
+
// may interleave (RFC 9113 s6.10). Non-null means "the next frame must be CONTINUATION on this
|
|
171
|
+
// stream id".
|
|
172
|
+
this._continuation = null; // { streamId, fragments: Uint8Array[], endStream, kind }
|
|
173
|
+
this._expectFirstSettings = true;
|
|
174
|
+
|
|
175
|
+
this._fatal = null; // set once; rejects every stream and every future request
|
|
176
|
+
this._goaway = null; // { lastStreamId, errorCode } received from the peer
|
|
177
|
+
this._closed = false;
|
|
178
|
+
this._onClose = opts.onClose ?? null;
|
|
179
|
+
this._writeChain = Promise.resolve();
|
|
180
|
+
|
|
181
|
+
// Kick off the preface flight and the read loop. Neither is awaited here: the constructor
|
|
182
|
+
// returns a usable connection and request() serialises behind the preface via the write chain.
|
|
183
|
+
this._sendPreface();
|
|
184
|
+
this._readLoop().catch((err) => this._die(err));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Whether a new request may be dispatched onto this connection right now. */
|
|
188
|
+
canDispatch() {
|
|
189
|
+
return (
|
|
190
|
+
!this._fatal &&
|
|
191
|
+
!this._closed &&
|
|
192
|
+
!this._goaway &&
|
|
193
|
+
this._streams.size < this._peerMaxConcurrent &&
|
|
194
|
+
this._nextStreamId <= 0x7fffffff
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get activeStreams() {
|
|
199
|
+
return this._streams.size;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ------------------------------------------------------------------ preface / writing
|
|
203
|
+
|
|
204
|
+
_sendPreface() {
|
|
205
|
+
// Exactly curl's flight and order: the 24-byte magic, then SETTINGS (ids 3,4,2), then a
|
|
206
|
+
// connection-level WINDOW_UPDATE that raises the receive window to 1000 MiB. See constants.js.
|
|
207
|
+
const settings = settingsFrame([
|
|
208
|
+
[SETTINGS.MAX_CONCURRENT_STREAMS, this._ourMaxConcurrent],
|
|
209
|
+
[SETTINGS.INITIAL_WINDOW_SIZE, this._ourInitialWindow],
|
|
210
|
+
[SETTINGS.ENABLE_PUSH, 0],
|
|
211
|
+
]);
|
|
212
|
+
const inc = this._ourConnWindow - DEFAULT_INITIAL_WINDOW;
|
|
213
|
+
const flight =
|
|
214
|
+
inc > 0
|
|
215
|
+
? concat([CONNECTION_PREFACE, settings, windowUpdateFrame(0, inc)])
|
|
216
|
+
: concat([CONNECTION_PREFACE, settings]);
|
|
217
|
+
this._connRecvWindow = this._ourConnWindow;
|
|
218
|
+
this._write(flight);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Serialise a wire write behind every previous one, so a header block is never split by another
|
|
222
|
+
* frame and the preface always leads. Mirrors the record layer's write discipline. */
|
|
223
|
+
_write(bytes) {
|
|
224
|
+
const task = this._writeChain.then(() => this._writer.write(bytes));
|
|
225
|
+
this._writeChain = task.then(
|
|
226
|
+
() => undefined,
|
|
227
|
+
() => undefined,
|
|
228
|
+
);
|
|
229
|
+
return task;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ------------------------------------------------------------------ request
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Open a stream and send a request. Resolves once the response header block (the first non-1xx
|
|
236
|
+
* HEADERS) has arrived; the body streams after.
|
|
237
|
+
*
|
|
238
|
+
* @param {object} req
|
|
239
|
+
* @param {string} req.method
|
|
240
|
+
* @param {string} req.scheme
|
|
241
|
+
* @param {string} req.authority
|
|
242
|
+
* @param {string} req.path
|
|
243
|
+
* @param {Array<[string, string]>} req.headers already lowercased, connection-specific ones removed
|
|
244
|
+
* @param {Uint8Array | null} [req.body] buffered whole by the caller, so send flow control is simple
|
|
245
|
+
* @param {AbortSignal} [req.signal] aborting it RST_STREAMs the stream and rejects its promises;
|
|
246
|
+
* this is how a per-request deadline tears down exactly one stream without touching the others
|
|
247
|
+
* @returns {Promise<Http2ResponseHead>}
|
|
248
|
+
*/
|
|
249
|
+
request({ method, scheme, authority, path, headers, body, signal }) {
|
|
250
|
+
if (this._fatal) throw this._fatal;
|
|
251
|
+
if (this._closed) throw new Http2Error(codes.HTTP2_PROTOCOL, 'connection is closed');
|
|
252
|
+
if (this._goaway) {
|
|
253
|
+
throw new Http2Retryable(
|
|
254
|
+
codes.HTTP2_GOAWAY,
|
|
255
|
+
'connection is going away; this stream was never opened',
|
|
256
|
+
{ lastStreamId: this._goaway.lastStreamId },
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (this._streams.size >= this._peerMaxConcurrent) {
|
|
260
|
+
throw new Http2Retryable(
|
|
261
|
+
codes.HTTP2_STREAM_STATE,
|
|
262
|
+
`at the peer's SETTINGS_MAX_CONCURRENT_STREAMS (${this._peerMaxConcurrent})`,
|
|
263
|
+
{ limit: this._peerMaxConcurrent },
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const id = this._nextStreamId;
|
|
267
|
+
this._nextStreamId += 2;
|
|
268
|
+
|
|
269
|
+
const stream = this._createStream(id);
|
|
270
|
+
if (signal) {
|
|
271
|
+
if (signal.aborted) {
|
|
272
|
+
this._resetStream(stream, H2_ERROR.CANCEL, signal.reason ?? new Http2Error(codes.HTTP2_PROTOCOL, 'aborted'));
|
|
273
|
+
return stream.head.promise;
|
|
274
|
+
}
|
|
275
|
+
const onAbort = () => {
|
|
276
|
+
if (!stream.closed) {
|
|
277
|
+
this._resetStream(stream, H2_ERROR.CANCEL, signal.reason ?? new Http2Error(codes.HTTP2_PROTOCOL, 'aborted'));
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
281
|
+
}
|
|
282
|
+
const hasBody = body != null && body.byteLength > 0;
|
|
283
|
+
|
|
284
|
+
const fields = buildRequestFields({ method, scheme, authority, path, headers });
|
|
285
|
+
const block = encodeHeaderBlock(fields);
|
|
286
|
+
this._sendHeaderBlock(id, block, !hasBody);
|
|
287
|
+
stream.localEnded = !hasBody;
|
|
288
|
+
|
|
289
|
+
if (hasBody) {
|
|
290
|
+
// Send the body respecting flow control. Not awaited: a large body may block on WINDOW_UPDATE,
|
|
291
|
+
// and blocking request() would stop the caller from ever reading the response head that
|
|
292
|
+
// unblocks it. Errors surface on the stream, which is what the caller is awaiting.
|
|
293
|
+
this._sendBody(id, body, stream).catch((err) => this._failStream(stream, err));
|
|
294
|
+
}
|
|
295
|
+
return stream.head.promise;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_createStream(id) {
|
|
299
|
+
const stream = {
|
|
300
|
+
id,
|
|
301
|
+
// response head
|
|
302
|
+
head: deferred(),
|
|
303
|
+
responseReceived: false,
|
|
304
|
+
// body plumbing: an in-memory queue drained by the body stream's pull, so WINDOW_UPDATE is
|
|
305
|
+
// tied to the consumer, not to arrival.
|
|
306
|
+
/** @type {Uint8Array[]} */
|
|
307
|
+
recvQueue: [],
|
|
308
|
+
recvEnded: false,
|
|
309
|
+
/** @type {Error | null} */
|
|
310
|
+
bodyError: null,
|
|
311
|
+
/** @type {(() => void) | null} */
|
|
312
|
+
pullWaiter: null,
|
|
313
|
+
completed: deferred(),
|
|
314
|
+
trailers: deferred(),
|
|
315
|
+
/** @type {Headers | null} */
|
|
316
|
+
trailerFields: null,
|
|
317
|
+
// flow control
|
|
318
|
+
recvWindow: this._ourInitialWindow,
|
|
319
|
+
recvConsumed: 0,
|
|
320
|
+
sendWindow: this._peerInitialWindow,
|
|
321
|
+
/** @type {Array<() => void>} */
|
|
322
|
+
sendWaiters: [],
|
|
323
|
+
localEnded: false,
|
|
324
|
+
cancelled: false,
|
|
325
|
+
closed: false,
|
|
326
|
+
rstSent: false,
|
|
327
|
+
};
|
|
328
|
+
stream.body = this._makeBodyStream(stream);
|
|
329
|
+
this._streams.set(id, stream);
|
|
330
|
+
return stream;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Send a header block as HEADERS plus CONTINUATION frames if it overflows one frame. The whole
|
|
334
|
+
* run is one write, so no other frame can interleave it (RFC 9113 s6.10). */
|
|
335
|
+
_sendHeaderBlock(streamId, block, endStream) {
|
|
336
|
+
const max = this._peerMaxFrameSize;
|
|
337
|
+
if (block.length <= max) {
|
|
338
|
+
this._write(headersFrame(streamId, block, { endStream, endHeaders: true }));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const frames = [];
|
|
342
|
+
let o = 0;
|
|
343
|
+
const first = block.subarray(0, max);
|
|
344
|
+
frames.push(headersFrame(streamId, first, { endStream, endHeaders: false }));
|
|
345
|
+
o = max;
|
|
346
|
+
while (o < block.length) {
|
|
347
|
+
const chunk = block.subarray(o, Math.min(o + max, block.length));
|
|
348
|
+
o += chunk.length;
|
|
349
|
+
frames.push(continuationFrame(streamId, chunk, o >= block.length));
|
|
350
|
+
}
|
|
351
|
+
this._write(concat(frames));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Send a request body as DATA frames, respecting stream and connection send windows. */
|
|
355
|
+
async _sendBody(streamId, body, stream) {
|
|
356
|
+
let offset = 0;
|
|
357
|
+
while (offset < body.byteLength) {
|
|
358
|
+
if (stream.closed || stream.cancelled) return; // reset or cancelled underneath us
|
|
359
|
+
const room = Math.min(stream.sendWindow, this._connSendWindow);
|
|
360
|
+
if (room <= 0) {
|
|
361
|
+
await this._awaitSendWindow(stream);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const n = Math.min(room, this._peerMaxFrameSize, body.byteLength - offset);
|
|
365
|
+
const slice = body.subarray(offset, offset + n);
|
|
366
|
+
offset += n;
|
|
367
|
+
stream.sendWindow -= n;
|
|
368
|
+
this._connSendWindow -= n;
|
|
369
|
+
const end = offset >= body.byteLength;
|
|
370
|
+
await this._write(dataFrame(streamId, slice, end));
|
|
371
|
+
if (end) stream.localEnded = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Block until either the stream or the connection send window grows (a WINDOW_UPDATE arrives). */
|
|
376
|
+
_awaitSendWindow(stream) {
|
|
377
|
+
if (this._fatal) return Promise.reject(this._fatal);
|
|
378
|
+
return new Promise((resolve) => {
|
|
379
|
+
stream.sendWaiters.push(resolve);
|
|
380
|
+
this._connSendWaiters.push(resolve);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_wakeSendWaiters(stream) {
|
|
385
|
+
const wake = (list) => {
|
|
386
|
+
const waiters = list.splice(0);
|
|
387
|
+
for (const w of waiters) w();
|
|
388
|
+
};
|
|
389
|
+
if (stream) wake(stream.sendWaiters);
|
|
390
|
+
wake(this._connSendWaiters);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ------------------------------------------------------------------ body stream / receive FC
|
|
394
|
+
|
|
395
|
+
_makeBodyStream(stream) {
|
|
396
|
+
const self = this;
|
|
397
|
+
const rs = new ReadableStream(
|
|
398
|
+
{
|
|
399
|
+
// highWaterMark 0: pull only when a consumer actually reads, so delivering a chunk here IS
|
|
400
|
+
// the moment of consumption — which is exactly when the flow-control window may be reopened.
|
|
401
|
+
async pull(controller) {
|
|
402
|
+
for (;;) {
|
|
403
|
+
if (stream.recvQueue.length > 0) {
|
|
404
|
+
const chunk = stream.recvQueue.shift();
|
|
405
|
+
controller.enqueue(chunk);
|
|
406
|
+
self._consumeStream(stream, chunk.byteLength);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (stream.bodyError) {
|
|
410
|
+
controller.error(stream.bodyError);
|
|
411
|
+
self._settleReject(stream.completed, stream.bodyError);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (stream.recvEnded) {
|
|
415
|
+
controller.close();
|
|
416
|
+
self._settleResolve(stream.completed, true);
|
|
417
|
+
self._settleResolve(stream.trailers, stream.trailerFields);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
await new Promise((resolve) => {
|
|
421
|
+
stream.pullWaiter = resolve;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
cancel() {
|
|
426
|
+
// The caller abandoned the body at an unknown position. Tell the peer to stop (RST_STREAM
|
|
427
|
+
// CANCEL) and settle the completion contract as "not finished" (resolve false, not reject).
|
|
428
|
+
stream.cancelled = true;
|
|
429
|
+
self._settleResolve(stream.completed, false);
|
|
430
|
+
self._settleResolve(stream.trailers, null);
|
|
431
|
+
self._sendRst(stream, H2_ERROR.CANCEL);
|
|
432
|
+
self._removeStream(stream);
|
|
433
|
+
return undefined;
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{ highWaterMark: 0 },
|
|
437
|
+
);
|
|
438
|
+
return Object.assign(rs, { completed: stream.completed.promise, trailers: stream.trailers.promise });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
_wakePull(stream) {
|
|
442
|
+
if (stream.pullWaiter) {
|
|
443
|
+
const w = stream.pullWaiter;
|
|
444
|
+
stream.pullWaiter = null;
|
|
445
|
+
w();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Called as the CONSUMER drains `n` bytes: reopen the stream and connection receive windows,
|
|
450
|
+
* batched so a byte-at-a-time consumer does not produce a WINDOW_UPDATE storm. */
|
|
451
|
+
_consumeStream(stream, n) {
|
|
452
|
+
this._replenish(stream, n);
|
|
453
|
+
this._replenishConn(n);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
_replenish(stream, n) {
|
|
457
|
+
stream.recvConsumed += n;
|
|
458
|
+
stream.recvWindow += n;
|
|
459
|
+
const threshold = Math.max(1, this._ourInitialWindow >> 1);
|
|
460
|
+
if (stream.recvConsumed >= threshold && !stream.closed) {
|
|
461
|
+
const inc = stream.recvConsumed;
|
|
462
|
+
stream.recvConsumed = 0;
|
|
463
|
+
this._write(windowUpdateFrame(stream.id, inc));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_replenishConn(n) {
|
|
468
|
+
this._connConsumed += n;
|
|
469
|
+
this._connRecvWindow += n;
|
|
470
|
+
const threshold = Math.max(1, this._ourConnWindow >> 1);
|
|
471
|
+
if (this._connConsumed >= threshold) {
|
|
472
|
+
const inc = this._connConsumed;
|
|
473
|
+
this._connConsumed = 0;
|
|
474
|
+
this._write(windowUpdateFrame(0, inc));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ------------------------------------------------------------------ read loop
|
|
479
|
+
|
|
480
|
+
async _readLoop() {
|
|
481
|
+
for (;;) {
|
|
482
|
+
const frame = await readFrame(this._reader, DEFAULT_MAX_FRAME_SIZE);
|
|
483
|
+
if (frame === null) {
|
|
484
|
+
// Clean transport EOF. Any stream still open ended without END_STREAM — a truncation.
|
|
485
|
+
this._die(
|
|
486
|
+
this._streams.size === 0
|
|
487
|
+
? null
|
|
488
|
+
: new Http2Error(codes.HTTP2_PROTOCOL, 'connection closed with streams still open'),
|
|
489
|
+
);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (this._fatal) return;
|
|
493
|
+
this._dispatchFrame(frame);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
_dispatchFrame(frame) {
|
|
498
|
+
const { type, flags, streamId, payload } = frame;
|
|
499
|
+
|
|
500
|
+
// A header block in progress may be interrupted by nothing but its own CONTINUATION.
|
|
501
|
+
if (this._continuation) {
|
|
502
|
+
if (type !== FRAME.CONTINUATION || streamId !== this._continuation.streamId) {
|
|
503
|
+
this._die(
|
|
504
|
+
new Http2Error(
|
|
505
|
+
codes.HTTP2_PROTOCOL,
|
|
506
|
+
`expected CONTINUATION on stream ${this._continuation.streamId}, got frame type ` +
|
|
507
|
+
`${type} on stream ${streamId}`,
|
|
508
|
+
{ expectedStream: this._continuation.streamId, gotType: type, gotStream: streamId },
|
|
509
|
+
),
|
|
510
|
+
);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this._onContinuation(flags, payload);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// The very first frame from the server must be a SETTINGS frame (RFC 9113 s3.4).
|
|
518
|
+
if (this._expectFirstSettings) {
|
|
519
|
+
if (type !== FRAME.SETTINGS) {
|
|
520
|
+
this._die(
|
|
521
|
+
new Http2Error(
|
|
522
|
+
codes.HTTP2_PROTOCOL,
|
|
523
|
+
`first frame from the server was type ${type}, expected SETTINGS`,
|
|
524
|
+
{ type },
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this._expectFirstSettings = false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
switch (type) {
|
|
533
|
+
case FRAME.SETTINGS:
|
|
534
|
+
this._onSettings(flags, streamId, payload);
|
|
535
|
+
break;
|
|
536
|
+
case FRAME.HEADERS:
|
|
537
|
+
this._onHeaders(flags, streamId, payload);
|
|
538
|
+
break;
|
|
539
|
+
case FRAME.DATA:
|
|
540
|
+
this._onData(flags, streamId, payload);
|
|
541
|
+
break;
|
|
542
|
+
case FRAME.WINDOW_UPDATE:
|
|
543
|
+
this._onWindowUpdate(streamId, payload);
|
|
544
|
+
break;
|
|
545
|
+
case FRAME.RST_STREAM:
|
|
546
|
+
this._onRstStream(streamId, payload);
|
|
547
|
+
break;
|
|
548
|
+
case FRAME.PING:
|
|
549
|
+
this._onPing(flags, streamId, payload);
|
|
550
|
+
break;
|
|
551
|
+
case FRAME.GOAWAY:
|
|
552
|
+
this._onGoaway(payload);
|
|
553
|
+
break;
|
|
554
|
+
case FRAME.PUSH_PROMISE:
|
|
555
|
+
// We advertised SETTINGS_ENABLE_PUSH = 0, so a PUSH_PROMISE is a protocol violation, not a
|
|
556
|
+
// resource to accept (RFC 9113 s8.4). Fail the whole connection closed.
|
|
557
|
+
this._die(
|
|
558
|
+
new Http2Error(
|
|
559
|
+
codes.HTTP2_PUSH_UNEXPECTED,
|
|
560
|
+
'server sent PUSH_PROMISE despite SETTINGS_ENABLE_PUSH = 0',
|
|
561
|
+
),
|
|
562
|
+
);
|
|
563
|
+
break;
|
|
564
|
+
case FRAME.PRIORITY:
|
|
565
|
+
// The priority scheme is deprecated (RFC 9113 s5.3.2). A well-formed PRIORITY frame is
|
|
566
|
+
// accepted and ignored; a mis-sized one is still a stream-level FRAME_SIZE_ERROR.
|
|
567
|
+
if (payload.length !== 5) {
|
|
568
|
+
this._die(
|
|
569
|
+
new Http2Error(codes.HTTP2_FRAME_SIZE, `PRIORITY payload is ${payload.length} bytes, must be 5`),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
case FRAME.CONTINUATION:
|
|
574
|
+
// A CONTINUATION with no HEADERS in progress has nothing to continue.
|
|
575
|
+
this._die(
|
|
576
|
+
new Http2Error(codes.HTTP2_PROTOCOL, 'CONTINUATION frame with no open header block'),
|
|
577
|
+
);
|
|
578
|
+
break;
|
|
579
|
+
default:
|
|
580
|
+
// Unknown frame types MUST be ignored (RFC 9113 s5.5) — this is deliberate and NOT a
|
|
581
|
+
// relaxation of the fail-closed rule: an unknown frame is fully length-delimited, so
|
|
582
|
+
// skipping it is unambiguous, and real servers (ALTSVC, ORIGIN, GREASE) send extension
|
|
583
|
+
// frames that a client refusing them could never reach. The one place they are refused is
|
|
584
|
+
// mid-header-block above, where s6.10 makes any interleaved frame a connection error.
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
_onSettings(flags, streamId, payload) {
|
|
590
|
+
if (streamId !== 0) {
|
|
591
|
+
this._die(new Http2Error(codes.HTTP2_PROTOCOL, 'SETTINGS on a non-zero stream'));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (flags & FLAG.ACK) {
|
|
595
|
+
if (payload.length !== 0) {
|
|
596
|
+
this._die(new Http2Error(codes.HTTP2_FRAME_SIZE, 'SETTINGS ACK must have an empty payload'));
|
|
597
|
+
}
|
|
598
|
+
return; // our SETTINGS were acknowledged; nothing to apply
|
|
599
|
+
}
|
|
600
|
+
let entries;
|
|
601
|
+
try {
|
|
602
|
+
entries = parseSettings(payload);
|
|
603
|
+
} catch (err) {
|
|
604
|
+
this._die(err);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
for (const [id, value] of entries) {
|
|
608
|
+
switch (id) {
|
|
609
|
+
case SETTINGS.INITIAL_WINDOW_SIZE: {
|
|
610
|
+
if (value > MAX_WINDOW) {
|
|
611
|
+
this._die(
|
|
612
|
+
new Http2Error(
|
|
613
|
+
codes.HTTP2_FLOW_CONTROL,
|
|
614
|
+
`SETTINGS_INITIAL_WINDOW_SIZE ${value} exceeds ${MAX_WINDOW}`,
|
|
615
|
+
{ value },
|
|
616
|
+
),
|
|
617
|
+
);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// A change retroactively adjusts every open stream's SEND window by the delta
|
|
621
|
+
// (RFC 9113 s6.9.2).
|
|
622
|
+
const delta = value - this._peerInitialWindow;
|
|
623
|
+
this._peerInitialWindow = value;
|
|
624
|
+
for (const stream of this._streams.values()) {
|
|
625
|
+
stream.sendWindow += delta;
|
|
626
|
+
if (stream.sendWindow > 0) this._wakeSendWaiters(stream);
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
case SETTINGS.MAX_FRAME_SIZE:
|
|
631
|
+
if (value < DEFAULT_MAX_FRAME_SIZE || value > MAX_ALLOWED_FRAME_SIZE) {
|
|
632
|
+
this._die(
|
|
633
|
+
new Http2Error(codes.HTTP2_PROTOCOL, `illegal SETTINGS_MAX_FRAME_SIZE ${value}`, { value }),
|
|
634
|
+
);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
this._peerMaxFrameSize = value;
|
|
638
|
+
break;
|
|
639
|
+
case SETTINGS.MAX_CONCURRENT_STREAMS:
|
|
640
|
+
this._peerMaxConcurrent = value;
|
|
641
|
+
break;
|
|
642
|
+
case SETTINGS.HEADER_TABLE_SIZE:
|
|
643
|
+
this._peerHeaderTableSize = value;
|
|
644
|
+
break;
|
|
645
|
+
case SETTINGS.ENABLE_PUSH:
|
|
646
|
+
if (value !== 0 && value !== 1) {
|
|
647
|
+
this._die(new Http2Error(codes.HTTP2_PROTOCOL, `illegal SETTINGS_ENABLE_PUSH ${value}`));
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
break;
|
|
651
|
+
default:
|
|
652
|
+
break; // unknown settings are ignored (RFC 9113 s6.5.2)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Acknowledge, as required (RFC 9113 s6.5.3).
|
|
656
|
+
this._write(settingsFrame([], true));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_onHeaders(flags, streamId, payload) {
|
|
660
|
+
if (streamId === 0 || (streamId & 1) === 0) {
|
|
661
|
+
// A response arrives on the odd, client-initiated stream we opened; 0 and even ids are wrong.
|
|
662
|
+
this._die(new Http2Error(codes.HTTP2_PROTOCOL, `HEADERS on invalid stream ${streamId}`));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
let fragment;
|
|
666
|
+
try {
|
|
667
|
+
fragment = headersBlockFragment(payload, flags);
|
|
668
|
+
} catch (err) {
|
|
669
|
+
this._die(err);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const endStream = (flags & FLAG.END_STREAM) !== 0;
|
|
673
|
+
if (flags & FLAG.END_HEADERS) {
|
|
674
|
+
this._completeHeaderBlock(streamId, fragment, endStream);
|
|
675
|
+
} else {
|
|
676
|
+
this._continuation = { streamId, fragments: [fragment.slice()], endStream };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
_onContinuation(flags, payload) {
|
|
681
|
+
this._continuation.fragments.push(payload.slice());
|
|
682
|
+
if (flags & FLAG.END_HEADERS) {
|
|
683
|
+
const { streamId, fragments, endStream } = this._continuation;
|
|
684
|
+
this._continuation = null;
|
|
685
|
+
this._completeHeaderBlock(streamId, concat(fragments), endStream);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** A full header block has been assembled: HPACK-decode it (connection-fatal on failure, since
|
|
690
|
+
* HPACK state is shared) and route it to the stream as a response head or as trailers. */
|
|
691
|
+
_completeHeaderBlock(streamId, block, endStream) {
|
|
692
|
+
let pairs;
|
|
693
|
+
try {
|
|
694
|
+
pairs = this._decoder.decode(block);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
// An HPACK error corrupts the shared decoder for every stream, so it is a connection error
|
|
697
|
+
// of type COMPRESSION_ERROR (RFC 9113 s4.3), never a stream-local one.
|
|
698
|
+
this._die(err);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const stream = this._streams.get(streamId);
|
|
702
|
+
if (!stream) {
|
|
703
|
+
// HEADERS for a stream we have closed. It cost us HPACK work (already done, so the table is
|
|
704
|
+
// consistent) but we owe it nothing else; ignore it rather than tear the connection down.
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (!stream.responseReceived) {
|
|
708
|
+
this._deliverResponseHead(stream, pairs, endStream);
|
|
709
|
+
} else {
|
|
710
|
+
this._deliverTrailers(stream, pairs, endStream);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
_deliverResponseHead(stream, pairs, endStream) {
|
|
715
|
+
let head;
|
|
716
|
+
try {
|
|
717
|
+
head = parseResponseHeaders(pairs);
|
|
718
|
+
} catch (err) {
|
|
719
|
+
// A malformed response is a STREAM error: reset this stream, leave the connection and its
|
|
720
|
+
// other streams untouched (RFC 9113 s8.1.1).
|
|
721
|
+
this._resetStream(stream, H2_ERROR.PROTOCOL_ERROR, err);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (head.status >= 100 && head.status <= 199) {
|
|
725
|
+
// Interim (1xx) response. It never carries a body and is not the real response; wait for the
|
|
726
|
+
// final HEADERS. An interim response with END_STREAM is malformed.
|
|
727
|
+
if (endStream) {
|
|
728
|
+
this._resetStream(
|
|
729
|
+
stream,
|
|
730
|
+
H2_ERROR.PROTOCOL_ERROR,
|
|
731
|
+
new Http2Error(codes.HTTP2_HEADER, `interim ${head.status} response ended the stream`),
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
stream.responseReceived = true;
|
|
737
|
+
if (endStream) {
|
|
738
|
+
// No body and no trailers: the completion contract is satisfiable now, exactly like the h1
|
|
739
|
+
// "complete at creation" case, so a caller that never reads the (empty) body still lets the
|
|
740
|
+
// deadline dispose.
|
|
741
|
+
stream.recvEnded = true;
|
|
742
|
+
this._settleResolve(stream.completed, true);
|
|
743
|
+
this._settleResolve(stream.trailers, null);
|
|
744
|
+
this._wakePull(stream);
|
|
745
|
+
}
|
|
746
|
+
this._settleResolve(stream.head, {
|
|
747
|
+
status: head.status,
|
|
748
|
+
statusText: '',
|
|
749
|
+
headers: head.headers,
|
|
750
|
+
setCookie: head.setCookie,
|
|
751
|
+
httpVersion: '2',
|
|
752
|
+
body: stream.body,
|
|
753
|
+
});
|
|
754
|
+
if (endStream) this._maybeCloseStream(stream);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
_deliverTrailers(stream, pairs, endStream) {
|
|
758
|
+
if (!endStream) {
|
|
759
|
+
// A second header block that is not the end can only be trailers, and trailers END the
|
|
760
|
+
// stream by definition (RFC 9113 s8.1).
|
|
761
|
+
this._resetStream(
|
|
762
|
+
stream,
|
|
763
|
+
H2_ERROR.PROTOCOL_ERROR,
|
|
764
|
+
new Http2Error(codes.HTTP2_TRAILER, 'trailing HEADERS without END_STREAM'),
|
|
765
|
+
);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
stream.trailerFields = parseTrailers(pairs);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
this._resetStream(stream, H2_ERROR.PROTOCOL_ERROR, err);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
stream.recvEnded = true;
|
|
775
|
+
this._wakePull(stream);
|
|
776
|
+
this._maybeCloseStream(stream);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
_onData(flags, streamId, payload) {
|
|
780
|
+
const stream = this._streams.get(streamId);
|
|
781
|
+
// Flow control is accounted at the connection level for EVERY DATA frame, even one for a
|
|
782
|
+
// stream we have already closed — the peer spent connection window to send it, and not
|
|
783
|
+
// crediting it back would slowly starve the connection (RFC 9113 s6.9.1).
|
|
784
|
+
const flowLen = payload.byteLength;
|
|
785
|
+
this._connRecvWindow -= flowLen;
|
|
786
|
+
if (this._connRecvWindow < 0) {
|
|
787
|
+
this._die(
|
|
788
|
+
new Http2Error(codes.HTTP2_FLOW_CONTROL, 'peer overran the connection flow-control window'),
|
|
789
|
+
);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (!stream || stream.closed) {
|
|
793
|
+
// No consumer will ever drain these bytes, so credit the whole frame back now.
|
|
794
|
+
this._replenishConn(flowLen);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (!stream.responseReceived) {
|
|
798
|
+
this._resetStream(
|
|
799
|
+
stream,
|
|
800
|
+
H2_ERROR.PROTOCOL_ERROR,
|
|
801
|
+
new Http2Error(codes.HTTP2_STREAM_STATE, 'DATA before response HEADERS'),
|
|
802
|
+
);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
let data;
|
|
806
|
+
try {
|
|
807
|
+
data = flags & FLAG.PADDED ? stripPadding(payload).data : payload;
|
|
808
|
+
} catch (err) {
|
|
809
|
+
this._die(err);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
stream.recvWindow -= flowLen;
|
|
813
|
+
if (stream.recvWindow < 0) {
|
|
814
|
+
this._resetStream(
|
|
815
|
+
stream,
|
|
816
|
+
H2_ERROR.FLOW_CONTROL_ERROR,
|
|
817
|
+
new Http2Error(codes.HTTP2_FLOW_CONTROL, 'peer overran the stream window'),
|
|
818
|
+
);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
// Padding (and the pad-length byte) is discarded, so it is consumed immediately for
|
|
822
|
+
// flow-control purposes; only the payload's data defers to the consumer.
|
|
823
|
+
const overhead = flowLen - data.byteLength;
|
|
824
|
+
if (overhead > 0) this._replenishConn(overhead);
|
|
825
|
+
// The stream window was debited by flowLen; credit the overhead back on the stream too.
|
|
826
|
+
if (overhead > 0) this._replenish(stream, overhead);
|
|
827
|
+
if (data.byteLength > 0) {
|
|
828
|
+
stream.recvQueue.push(data.slice());
|
|
829
|
+
this._wakePull(stream);
|
|
830
|
+
}
|
|
831
|
+
if (flags & FLAG.END_STREAM) {
|
|
832
|
+
stream.recvEnded = true;
|
|
833
|
+
this._wakePull(stream);
|
|
834
|
+
this._maybeCloseStream(stream);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
_onWindowUpdate(streamId, payload) {
|
|
839
|
+
let inc;
|
|
840
|
+
try {
|
|
841
|
+
inc = parseWindowUpdate(payload);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
this._die(err);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (inc === 0) {
|
|
847
|
+
// A zero increment is a PROTOCOL_ERROR — connection-level when on stream 0, else stream-level.
|
|
848
|
+
if (streamId === 0) {
|
|
849
|
+
this._die(new Http2Error(codes.HTTP2_PROTOCOL, 'connection WINDOW_UPDATE with a zero increment'));
|
|
850
|
+
} else {
|
|
851
|
+
const stream = this._streams.get(streamId);
|
|
852
|
+
if (stream) {
|
|
853
|
+
this._resetStream(
|
|
854
|
+
stream,
|
|
855
|
+
H2_ERROR.PROTOCOL_ERROR,
|
|
856
|
+
new Http2Error(codes.HTTP2_PROTOCOL, 'zero WINDOW_UPDATE increment'),
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (streamId === 0) {
|
|
863
|
+
this._connSendWindow += inc;
|
|
864
|
+
if (this._connSendWindow > MAX_WINDOW) {
|
|
865
|
+
this._die(new Http2Error(codes.HTTP2_FLOW_CONTROL, 'connection send window exceeded 2^31-1'));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
this._wakeSendWaiters(null);
|
|
869
|
+
} else {
|
|
870
|
+
const stream = this._streams.get(streamId);
|
|
871
|
+
if (!stream) return; // WINDOW_UPDATE for a closed stream is harmless and ignored
|
|
872
|
+
stream.sendWindow += inc;
|
|
873
|
+
if (stream.sendWindow > MAX_WINDOW) {
|
|
874
|
+
this._resetStream(
|
|
875
|
+
stream,
|
|
876
|
+
H2_ERROR.FLOW_CONTROL_ERROR,
|
|
877
|
+
new Http2Error(codes.HTTP2_FLOW_CONTROL, 'stream send window exceeded 2^31-1'),
|
|
878
|
+
);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
this._wakeSendWaiters(stream);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
_onRstStream(streamId, payload) {
|
|
886
|
+
let errorCode;
|
|
887
|
+
try {
|
|
888
|
+
errorCode = parseRstStream(payload);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
this._die(err);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (streamId === 0) {
|
|
894
|
+
this._die(new Http2Error(codes.HTTP2_PROTOCOL, 'RST_STREAM on stream 0'));
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const stream = this._streams.get(streamId);
|
|
898
|
+
if (!stream) return;
|
|
899
|
+
const name = H2_ERROR_NAME[errorCode] ?? `0x${errorCode.toString(16)}`;
|
|
900
|
+
// REFUSED_STREAM specifically means the server did not process the request (RFC 9113 s8.7), so
|
|
901
|
+
// it is safe to retry on a fresh connection — the same "server never saw it" guarantee the h1
|
|
902
|
+
// path relies on. Everything else is reported as-is.
|
|
903
|
+
const retryable = errorCode === H2_ERROR.REFUSED_STREAM && !stream.responseReceived;
|
|
904
|
+
const err = retryable
|
|
905
|
+
? new Http2Retryable(codes.HTTP2_STREAM_CLOSED, 'server refused the stream (REFUSED_STREAM)', { errorCode })
|
|
906
|
+
: new Http2Error(codes.HTTP2_STREAM_CLOSED, `server reset the stream: ${name}`, { errorCode });
|
|
907
|
+
this._failStream(stream, err);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
_onPing(flags, streamId, payload) {
|
|
911
|
+
if (streamId !== 0) {
|
|
912
|
+
this._die(new Http2Error(codes.HTTP2_PROTOCOL, 'PING on a non-zero stream'));
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (payload.length !== 8) {
|
|
916
|
+
this._die(new Http2Error(codes.HTTP2_FRAME_SIZE, `PING payload is ${payload.length} bytes, must be 8`));
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (flags & FLAG.ACK) return; // a reply to a PING we never send; ignore
|
|
920
|
+
this._write(pingFrame(payload.slice(), true)); // echo the opaque data (RFC 9113 s6.7)
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
_onGoaway(payload) {
|
|
924
|
+
let g;
|
|
925
|
+
try {
|
|
926
|
+
g = parseGoaway(payload);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
this._die(err);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
this._goaway = { lastStreamId: g.lastStreamId, errorCode: g.errorCode };
|
|
932
|
+
// Streams the server never committed to (id > lastStreamId) provably were not processed and
|
|
933
|
+
// may be retried elsewhere; streams within the promise keep running until they finish or the
|
|
934
|
+
// transport dies.
|
|
935
|
+
for (const stream of this._streams.values()) {
|
|
936
|
+
if (stream.id > g.lastStreamId && !stream.responseReceived) {
|
|
937
|
+
const name = H2_ERROR_NAME[g.errorCode] ?? `0x${g.errorCode.toString(16)}`;
|
|
938
|
+
this._failStream(
|
|
939
|
+
stream,
|
|
940
|
+
new Http2Retryable(codes.HTTP2_GOAWAY, `stream not processed before GOAWAY (${name})`, {
|
|
941
|
+
errorCode: g.errorCode,
|
|
942
|
+
}),
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (this._onClose) this._onClose(null); // stop new dispatch; existing streams finish
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ------------------------------------------------------------------ stream teardown
|
|
950
|
+
//
|
|
951
|
+
// Three composable, idempotent primitives so every teardown path is exact about two independent
|
|
952
|
+
// questions: does the peer need an RST_STREAM (only when WE abandon a stream it still believes
|
|
953
|
+
// is live), and do the caller's promises reject or resolve. Conflating them is how a stream ends
|
|
954
|
+
// up both RST'd in response to the peer's own RST (illegal) and left un-rejected.
|
|
955
|
+
|
|
956
|
+
/** Forget a stream: drop it from the table and wake anything blocked on it. */
|
|
957
|
+
_removeStream(stream) {
|
|
958
|
+
if (stream.closed) return;
|
|
959
|
+
stream.closed = true;
|
|
960
|
+
this._streams.delete(stream.id);
|
|
961
|
+
this._wakePull(stream);
|
|
962
|
+
this._wakeSendWaiters(stream);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Reject the caller's promises for a stream. Idempotent via the deferreds' settled flags. */
|
|
966
|
+
_rejectStream(stream, err) {
|
|
967
|
+
if (!stream.bodyError) stream.bodyError = err;
|
|
968
|
+
this._settleReject(stream.head, err);
|
|
969
|
+
this._settleReject(stream.completed, err);
|
|
970
|
+
this._settleReject(stream.trailers, err);
|
|
971
|
+
this._wakePull(stream);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** Send RST_STREAM once, telling the peer to stop spending bandwidth on a stream we gave up on. */
|
|
975
|
+
_sendRst(stream, errorCode) {
|
|
976
|
+
if (stream.rstSent) return;
|
|
977
|
+
stream.rstSent = true;
|
|
978
|
+
if (!this._fatal && !this._closed) this._write(rstStreamFrame(stream.id, errorCode));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** A stream finished cleanly once both halves ended: our request fully sent, END_STREAM received. */
|
|
982
|
+
_maybeCloseStream(stream) {
|
|
983
|
+
if (stream.recvEnded && stream.localEnded) this._removeStream(stream);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** A failure originating with the PEER (its RST_STREAM, its GOAWAY, a connection death): reject
|
|
987
|
+
* the caller, never RST back — RFC 9113 s5.4.2 forbids answering a reset with a reset. */
|
|
988
|
+
_failStream(stream, err) {
|
|
989
|
+
this._removeStream(stream);
|
|
990
|
+
this._rejectStream(stream, err);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** A failure originating with US (malformed response, our timeout, flow-control overrun we caught):
|
|
994
|
+
* tell the peer with RST_STREAM, then reject the caller. */
|
|
995
|
+
_resetStream(stream, errorCode, err) {
|
|
996
|
+
this._sendRst(stream, errorCode);
|
|
997
|
+
this._removeStream(stream);
|
|
998
|
+
if (err) this._rejectStream(stream, err);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ------------------------------------------------------------------ connection teardown
|
|
1002
|
+
|
|
1003
|
+
_die(err) {
|
|
1004
|
+
if (this._fatal) return;
|
|
1005
|
+
this._fatal = err ?? new Http2Error(codes.HTTP2_PROTOCOL, 'connection closed');
|
|
1006
|
+
// A GOAWAY on the way out is a courtesy so the peer knows the last stream we handled; failures
|
|
1007
|
+
// are ignored because the transport may already be unusable.
|
|
1008
|
+
if (!this._closed) {
|
|
1009
|
+
this._closed = true;
|
|
1010
|
+
this._write(goawayFrame(this._lastPeerStreamId, err ? H2_ERROR.PROTOCOL_ERROR : H2_ERROR.NO_ERROR)).catch(
|
|
1011
|
+
() => {},
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
for (const stream of [...this._streams.values()]) {
|
|
1015
|
+
this._failStream(stream, this._fatal);
|
|
1016
|
+
}
|
|
1017
|
+
void this._writer.close?.();
|
|
1018
|
+
try {
|
|
1019
|
+
void this._reader.cancel?.(this._fatal);
|
|
1020
|
+
} catch {
|
|
1021
|
+
/* already gone */
|
|
1022
|
+
}
|
|
1023
|
+
void Promise.resolve(this._closeTransport()).catch(() => {});
|
|
1024
|
+
if (this._onClose) {
|
|
1025
|
+
const cb = this._onClose;
|
|
1026
|
+
this._onClose = null;
|
|
1027
|
+
cb(err ?? null);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Graceful shutdown: GOAWAY(NO_ERROR), then close the transport. Any live stream is failed.
|
|
1033
|
+
*
|
|
1034
|
+
* The GOAWAY and the writer close are BEST-EFFORT and are not awaited: a peer that has stopped
|
|
1035
|
+
* reading applies backpressure that never clears, and awaiting a courtesy frame into a full
|
|
1036
|
+
* buffer would hang close() forever — the exact trap the record layer avoids with its grace
|
|
1037
|
+
* window. Only the transport close is awaited, because that is what actually releases the socket.
|
|
1038
|
+
*/
|
|
1039
|
+
async close() {
|
|
1040
|
+
if (this._closed) return;
|
|
1041
|
+
this._closed = true;
|
|
1042
|
+
this._write(goawayFrame(this._lastPeerStreamId, H2_ERROR.NO_ERROR)).catch(() => {});
|
|
1043
|
+
const err = new Http2Error(codes.HTTP2_PROTOCOL, 'connection closed by client');
|
|
1044
|
+
if (!this._fatal) this._fatal = err;
|
|
1045
|
+
for (const stream of [...this._streams.values()]) this._failStream(stream, err);
|
|
1046
|
+
void this._writer.close?.().catch?.(() => {});
|
|
1047
|
+
try {
|
|
1048
|
+
void this._reader.cancel?.(err);
|
|
1049
|
+
} catch {
|
|
1050
|
+
/* already gone */
|
|
1051
|
+
}
|
|
1052
|
+
await Promise.resolve(this._closeTransport()).catch(() => {});
|
|
1053
|
+
if (this._onClose) {
|
|
1054
|
+
const cb = this._onClose;
|
|
1055
|
+
this._onClose = null;
|
|
1056
|
+
cb(null);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ------------------------------------------------------------------ settle helpers
|
|
1061
|
+
|
|
1062
|
+
_settleResolve(d, value) {
|
|
1063
|
+
if (d.settled) return;
|
|
1064
|
+
d.settled = true;
|
|
1065
|
+
d.resolve(value);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
_settleReject(d, err) {
|
|
1069
|
+
if (d.settled) return;
|
|
1070
|
+
d.settled = true;
|
|
1071
|
+
d.reject(err);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ---------------------------------------------------------------------- header building / parsing
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Build the ordered HPACK field list for a request, pseudo-headers first in curl's order
|
|
1079
|
+
* (:method, :scheme, :authority, :path). :path is emitted "without indexing" and the rest
|
|
1080
|
+
* "incremental", matching the captured curl encoding.
|
|
1081
|
+
*
|
|
1082
|
+
* @param {{ method: string, scheme: string, authority: string, path: string,
|
|
1083
|
+
* headers: Array<[string, string]> }} req
|
|
1084
|
+
* @returns {import('./hpack.js').HpackField[]}
|
|
1085
|
+
*/
|
|
1086
|
+
export function buildRequestFields({ method, scheme, authority, path, headers }) {
|
|
1087
|
+
const pseudo = { ':method': method, ':scheme': scheme, ':authority': authority, ':path': path };
|
|
1088
|
+
const fields = [];
|
|
1089
|
+
for (const name of PSEUDO_HEADER_ORDER) {
|
|
1090
|
+
fields.push({ name, value: pseudo[name], indexing: name === ':path' ? 'without' : 'incremental' });
|
|
1091
|
+
}
|
|
1092
|
+
for (const [name, value] of headers) {
|
|
1093
|
+
fields.push({ name, value, indexing: 'incremental' });
|
|
1094
|
+
}
|
|
1095
|
+
return fields;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/** Validate and split a decoded RESPONSE header list into status + regular headers + set-cookie. */
|
|
1099
|
+
function parseResponseHeaders(pairs) {
|
|
1100
|
+
let status = null;
|
|
1101
|
+
let sawRegular = false;
|
|
1102
|
+
const headers = new Headers();
|
|
1103
|
+
const setCookie = [];
|
|
1104
|
+
for (const [name, value] of pairs) {
|
|
1105
|
+
if (name.length === 0) {
|
|
1106
|
+
throw new Http2Error(codes.HTTP2_HEADER, 'empty header field name');
|
|
1107
|
+
}
|
|
1108
|
+
if (name[0] === ':') {
|
|
1109
|
+
if (sawRegular) {
|
|
1110
|
+
throw new Http2Error(codes.HTTP2_HEADER, `pseudo-header ${name} appeared after a regular header`);
|
|
1111
|
+
}
|
|
1112
|
+
if (name !== ':status') {
|
|
1113
|
+
throw new Http2Error(codes.HTTP2_HEADER, `unknown response pseudo-header ${name}`, { name });
|
|
1114
|
+
}
|
|
1115
|
+
if (status !== null) {
|
|
1116
|
+
throw new Http2Error(codes.HTTP2_HEADER, 'duplicate :status pseudo-header');
|
|
1117
|
+
}
|
|
1118
|
+
if (!/^[0-9]{3}$/.test(value)) {
|
|
1119
|
+
throw new Http2Error(codes.HTTP2_HEADER, `:status ${JSON.stringify(value)} is not three digits`, { value });
|
|
1120
|
+
}
|
|
1121
|
+
status = Number(value);
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
assertHeaderNameGrammar(name);
|
|
1125
|
+
if (FORBIDDEN_H2_HEADERS.has(name)) {
|
|
1126
|
+
throw new Http2Error(codes.HTTP2_HEADER, `connection-specific header ${name} is forbidden in HTTP/2`, { name });
|
|
1127
|
+
}
|
|
1128
|
+
if (name === 'te' && value.toLowerCase() !== 'trailers') {
|
|
1129
|
+
throw new Http2Error(codes.HTTP2_HEADER, 'the only legal te value in HTTP/2 is "trailers"');
|
|
1130
|
+
}
|
|
1131
|
+
sawRegular = true;
|
|
1132
|
+
if (name === 'set-cookie') setCookie.push(value);
|
|
1133
|
+
headers.append(name, value);
|
|
1134
|
+
}
|
|
1135
|
+
if (status === null) {
|
|
1136
|
+
throw new Http2Error(codes.HTTP2_HEADER, 'response has no :status pseudo-header');
|
|
1137
|
+
}
|
|
1138
|
+
return { status, headers, setCookie };
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** Validate decoded trailers: regular field lines only, never a pseudo-header (RFC 9113 s8.1). */
|
|
1142
|
+
function parseTrailers(pairs) {
|
|
1143
|
+
const trailers = new Headers();
|
|
1144
|
+
for (const [name, value] of pairs) {
|
|
1145
|
+
if (name.length === 0 || name[0] === ':') {
|
|
1146
|
+
throw new Http2Error(codes.HTTP2_TRAILER, `trailer field ${JSON.stringify(name)} is not a regular header`);
|
|
1147
|
+
}
|
|
1148
|
+
assertHeaderNameGrammar(name);
|
|
1149
|
+
if (FORBIDDEN_H2_HEADERS.has(name)) {
|
|
1150
|
+
throw new Http2Error(codes.HTTP2_TRAILER, `connection-specific header ${name} is forbidden in a trailer`);
|
|
1151
|
+
}
|
|
1152
|
+
trailers.append(name, value);
|
|
1153
|
+
}
|
|
1154
|
+
return trailers;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** RFC 9113 s8.2.1: field names are lowercase; an uppercase letter is malformed. Also reject the
|
|
1158
|
+
* bytes the field-name grammar forbids, so a decoded name can never split a downstream parser. */
|
|
1159
|
+
function assertHeaderNameGrammar(name) {
|
|
1160
|
+
for (let i = 0; i < name.length; i++) {
|
|
1161
|
+
const c = name.charCodeAt(i);
|
|
1162
|
+
if (c >= 0x41 && c <= 0x5a) {
|
|
1163
|
+
throw new Http2Error(codes.HTTP2_HEADER, `header name ${JSON.stringify(name)} contains an uppercase letter`, { name });
|
|
1164
|
+
}
|
|
1165
|
+
// Control bytes, space, and the HTTP/2 forbidden separators have no place in a field name.
|
|
1166
|
+
if (c <= 0x20 || c === 0x7f || c === 0x3a /* ':' mid-name */) {
|
|
1167
|
+
throw new Http2Error(codes.HTTP2_HEADER, `header name ${JSON.stringify(name)} contains an illegal byte`, { name });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|