h2lean 0.0.0-stage → 0.1.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 +201 -0
- package/README.md +265 -2
- package/index.js +72 -0
- package/lib/client-session.js +2068 -0
- package/lib/client-stream.js +166 -0
- package/lib/client.js +85 -0
- package/lib/compat.js +849 -0
- package/lib/fast-timer.js +436 -0
- package/lib/frames.js +127 -0
- package/lib/headers.js +463 -0
- package/lib/hpack-data.js +333 -0
- package/lib/hpack.js +1288 -0
- package/lib/pieces.js +134 -0
- package/lib/server.js +272 -0
- package/lib/session.js +2698 -0
- package/lib/stream.js +519 -0
- package/package.json +49 -4
package/lib/compat.js
ADDED
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
// Compat layer: the http1-shaped (req, res) API over an HTTP/2 stream, matching
|
|
2
|
+
// the semantics of node:internal/http2/compat (that source was read through
|
|
3
|
+
// --expose-internals, and the frame sequence was confirmed against a live node
|
|
4
|
+
// server with tools/probe-wire.js).
|
|
5
|
+
//
|
|
6
|
+
// Deliberate differences from node, all of them loud rather than silent:
|
|
7
|
+
// * req.pause()/read() only affect the local Readable. The flow-control window
|
|
8
|
+
// is still replenished, because h2lite releases stream credit as the buffered
|
|
9
|
+
// body is consumed and connection credit in half-window steps, neither of them
|
|
10
|
+
// per read(). A paused request buffers in memory instead of slowing the peer down.
|
|
11
|
+
// * res.cork()/uncork() are absent, not no-ops: h2lite already coalesces every
|
|
12
|
+
// frame produced in one turn of the loop into a single write().
|
|
13
|
+
// * res.write() returning false is honest backpressure, and 'drain' is emitted
|
|
14
|
+
// when the send window reopens.
|
|
15
|
+
// * createPushResponse() throws ERR_H2LITE_NOT_IMPLEMENTED (no server push in
|
|
16
|
+
// v1) instead of failing later with a protocol error.
|
|
17
|
+
// * res 'finish' means the local half of the stream was closed, which for
|
|
18
|
+
// h2lite happens as the frames are queued for the socket; node reports it
|
|
19
|
+
// when the stream closes, which is later. A response the peer cut short
|
|
20
|
+
// reports 'finish' on both stacks.
|
|
21
|
+
// * the order of the teardown events differs: node reports the request's
|
|
22
|
+
// events (its 'end', its 'aborted', its 'close') before the response's
|
|
23
|
+
// ('finish', then 'close'), h2lite reports the response's first. Every
|
|
24
|
+
// event fires on both stacks -- measured against a live node server over
|
|
25
|
+
// the four shapes of bench/micro/probe-teardown-order.js, the sets are equal -- and
|
|
26
|
+
// only their order within the teardown turn differs. One pair is not a
|
|
27
|
+
// fixed difference at all: the request's 'end' against the 'aborted'
|
|
28
|
+
// depends on whether the peer's END_STREAM and its reset arrive in one
|
|
29
|
+
// read or in two, and the two stacks order that pair differently.
|
|
30
|
+
// * the body is pulled, not pushed: nothing is read off the wire until the
|
|
31
|
+
// application consumes it, so an unread upload applies backpressure to the
|
|
32
|
+
// sender (node does the same) instead of buffering here.
|
|
33
|
+
// * with the server option shareHeaders:true, req.headers is frozen and shared
|
|
34
|
+
// between requests whose header block is identical, so req.url = ... and
|
|
35
|
+
// req.method = ... throw instead of rewriting the request.
|
|
36
|
+
// * the compat layer's handlers are not subscriptions on the stream: the stream
|
|
37
|
+
// calls them through one dispatch function (lib/stream.js, emit()), so
|
|
38
|
+
// stream.listenerCount('close') and friends report fewer listeners than they
|
|
39
|
+
// do on node, whose compat layer subscribes to the same events
|
|
40
|
+
// (internal/http2/compat, the stream.on() calls in the request and the
|
|
41
|
+
// response constructor). Everything an application can observe about the
|
|
42
|
+
// events themselves is unchanged: same handlers, same order, same arguments.
|
|
43
|
+
'use strict';
|
|
44
|
+
|
|
45
|
+
const { Readable, Stream } = require('node:stream');
|
|
46
|
+
const { emitNT } = require('./stream');
|
|
47
|
+
const { nullProto, trimName } = require('./headers');
|
|
48
|
+
|
|
49
|
+
function err(code, msg) {
|
|
50
|
+
const e = new Error(msg);
|
|
51
|
+
e.code = code;
|
|
52
|
+
return e;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// node validates a header name before it normalises it. Every accessor that
|
|
56
|
+
// takes a name calls validateString(name, 'name') first (internal/http2/compat.js:
|
|
57
|
+
// setTrailer 581, getHeader 596, hasHeader 618, removeHeader 623, setHeader 639,
|
|
58
|
+
// appendHeader 663), and validateString throws ERR_INVALID_ARG_TYPE for anything
|
|
59
|
+
// that is not a string (internal/validators.js:165). Measured against a live node
|
|
60
|
+
// server (bench/micro/probe-nonname.js): the six accessors refuse a number with
|
|
61
|
+
// The "name" argument must be of type string. Received type number (123)
|
|
62
|
+
// and accept the same name written as a string. Coercing the name instead --
|
|
63
|
+
// which is what this did -- was a silent divergence: the name the application
|
|
64
|
+
// meant and the name that reached the wire were not the same one, and the
|
|
65
|
+
// application was never told.
|
|
66
|
+
//
|
|
67
|
+
// The error is a TypeError with a code, like node's: ERR_INVALID_ARG_TYPE is
|
|
68
|
+
// declared as one (internal/errors.js:1413).
|
|
69
|
+
function checkName(name) {
|
|
70
|
+
if (typeof name !== 'string') {
|
|
71
|
+
const e = new TypeError(
|
|
72
|
+
`The "name" argument must be of type string. Received ${describeType(name)}`);
|
|
73
|
+
e.code = 'ERR_INVALID_ARG_TYPE';
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// node's wording for a value of the wrong type (internal/errors.js,
|
|
79
|
+
// determineSpecificType): the message is what an application reads when it
|
|
80
|
+
// catches the error, so it says what node says.
|
|
81
|
+
function describeType(value) {
|
|
82
|
+
if (value === null) {
|
|
83
|
+
return 'null';
|
|
84
|
+
}
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
return 'undefined';
|
|
87
|
+
}
|
|
88
|
+
switch (typeof value) {
|
|
89
|
+
case 'number':
|
|
90
|
+
if (value === 0) {
|
|
91
|
+
return 1 / value === -Infinity ? 'type number (-0)' : 'type number (0)';
|
|
92
|
+
}
|
|
93
|
+
return `type number (${value})`;
|
|
94
|
+
case 'bigint': return `type bigint (${value}n)`;
|
|
95
|
+
case 'boolean': return `type boolean (${value})`;
|
|
96
|
+
case 'symbol': return `type symbol (${String(value)})`;
|
|
97
|
+
case 'function': return `function ${value.name}`;
|
|
98
|
+
case 'object': {
|
|
99
|
+
const name = value.constructor?.name;
|
|
100
|
+
return typeof name === 'string' && name !== '' ? `an instance of ${name}` : 'an instance of Object';
|
|
101
|
+
}
|
|
102
|
+
default: return `type ${typeof value}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// node lets an application rewrite req.url / req.method. With shareHeaders the
|
|
107
|
+
// headers object belongs to every identical request on the connection, so the
|
|
108
|
+
// rewrite is refused loudly instead of silently corrupting another request.
|
|
109
|
+
function assertWritable(headers) {
|
|
110
|
+
if (Object.isFrozen(headers)) {
|
|
111
|
+
throw err('ERR_H2LITE_FROZEN_HEADERS',
|
|
112
|
+
'req.headers is frozen because the server was created with shareHeaders:true');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- request ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
// Module-level handlers, invoked with `this` bound to the Http2Stream, so no
|
|
119
|
+
// closure is allocated per request. The stream keeps the only reference to the
|
|
120
|
+
// request object, exactly like node keeps it under a symbol.
|
|
121
|
+
function onRequestData(chunk) {
|
|
122
|
+
const req = this._request;
|
|
123
|
+
if (req === undefined) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// a Readable queues the chunk for an unknown amount of time, which is fine:
|
|
127
|
+
// what the session delivers is either a view of a socket read or a buffer built
|
|
128
|
+
// for that frame alone, and neither is recycled underneath us
|
|
129
|
+
if (req.push(chunk)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// The application is not keeping up. node stops replenishing the window here;
|
|
133
|
+
// doing the same costs the sender instead of our heap, so buffer nothing more
|
|
134
|
+
// until _read() asks for it.
|
|
135
|
+
this._pauseConsuming();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function onRequestEnd() {
|
|
139
|
+
const req = this._request;
|
|
140
|
+
if (req === undefined || req._ended) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
req._ended = true;
|
|
144
|
+
req.push(null);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onRequestError(error) {
|
|
148
|
+
const req = this._request;
|
|
149
|
+
if (req !== undefined) {
|
|
150
|
+
req.destroy(error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onRequestAborted() {
|
|
155
|
+
const req = this._request;
|
|
156
|
+
if (req === undefined || req._closed) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
req._aborted = true;
|
|
160
|
+
req.emit('aborted');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function onRequestTrailers(trailers) {
|
|
164
|
+
const req = this._request;
|
|
165
|
+
if (req === undefined) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
req._trailers = trailers;
|
|
169
|
+
req.emit('trailers', trailers);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function onRequestClose() {
|
|
173
|
+
const req = this._request;
|
|
174
|
+
if (req === undefined || req._closed) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
req._closed = true;
|
|
178
|
+
if (!req._ended) {
|
|
179
|
+
req._ended = true;
|
|
180
|
+
req.push(null);
|
|
181
|
+
}
|
|
182
|
+
// node dumps a body nobody read when the stream closes: "If the user didn't
|
|
183
|
+
// interact with incoming data and didn't pipe it, dump it for compatibility
|
|
184
|
+
// with http1" (internal/http2/compat, onStreamCloseRequest). Without it the
|
|
185
|
+
// request would never report 'end' at all: a Readable that nobody reads
|
|
186
|
+
// never ends, and the EOF pushed above only reaches the application once
|
|
187
|
+
// the Readable flows.
|
|
188
|
+
if (!req._didRead) {
|
|
189
|
+
req.resume();
|
|
190
|
+
}
|
|
191
|
+
// 'close' after 'end', so queue it behind the end the push above scheduled.
|
|
192
|
+
// node emits it right here instead, so the two stacks differ in the order of
|
|
193
|
+
// these two: see the difference list at the top of this file.
|
|
194
|
+
process.nextTick(emitCloseNT, req);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function emitCloseNT(target) {
|
|
198
|
+
target.emit('close');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function onRequestTimeout() {
|
|
202
|
+
const req = this._request;
|
|
203
|
+
if (req !== undefined && !req._closed) {
|
|
204
|
+
req.emit('timeout');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The compat layer's handlers, called by the stream (lib/stream.js) when it emits
|
|
209
|
+
// an event, with the stream as `this`. The order is the order the subscriptions had:
|
|
210
|
+
// the request's handler before the response's for the three events both of them
|
|
211
|
+
// answer, and both before the application's own listeners.
|
|
212
|
+
function compatDispatch(name, arg) {
|
|
213
|
+
switch (name) {
|
|
214
|
+
case 'end':
|
|
215
|
+
onRequestEnd.call(this, arg);
|
|
216
|
+
break;
|
|
217
|
+
case 'aborted':
|
|
218
|
+
onRequestAborted.call(this, arg);
|
|
219
|
+
// node's response handler for 'aborted' is a no-op -- "non-op for now"
|
|
220
|
+
// (internal/http2/compat, onStreamAbortedResponse). The response is torn
|
|
221
|
+
// down by the stream's 'close', which reports 'finish' and then 'close';
|
|
222
|
+
// closing it here would report the 'close' without the 'finish' and
|
|
223
|
+
// before the request's own 'close'.
|
|
224
|
+
break;
|
|
225
|
+
case 'trailers':
|
|
226
|
+
onRequestTrailers.call(this, arg);
|
|
227
|
+
break;
|
|
228
|
+
case 'close':
|
|
229
|
+
onRequestClose.call(this, arg);
|
|
230
|
+
onResponseClose.call(this, arg);
|
|
231
|
+
break;
|
|
232
|
+
case 'timeout':
|
|
233
|
+
onRequestTimeout.call(this, arg);
|
|
234
|
+
onResponseTimeout.call(this, arg);
|
|
235
|
+
break;
|
|
236
|
+
case 'drain':
|
|
237
|
+
onResponseDrain.call(this, arg);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class Http2ServerRequest extends Readable {
|
|
243
|
+
constructor(stream, headers, rawHeaders) {
|
|
244
|
+
super({ autoDestroy: false });
|
|
245
|
+
this._stream = stream;
|
|
246
|
+
this._headers = headers;
|
|
247
|
+
this._rawHeaders = rawHeaders === null ? [] : rawHeaders;
|
|
248
|
+
this._trailers = { __proto__: null };
|
|
249
|
+
this._rawTrailers = [];
|
|
250
|
+
this._closed = false;
|
|
251
|
+
this._ended = false;
|
|
252
|
+
this._aborted = false;
|
|
253
|
+
this._didRead = false;
|
|
254
|
+
stream._request = this;
|
|
255
|
+
// No 'data' listener here: see _read(). Attaching it eagerly would buffer
|
|
256
|
+
// every unread upload in the Readable and would emit 'end' for bodies the
|
|
257
|
+
// application never asked for, neither of which node does.
|
|
258
|
+
//
|
|
259
|
+
// The stream's other events are not subscribed to either: the stream calls
|
|
260
|
+
// the compat layer's handlers through one dispatch function, which costs
|
|
261
|
+
// one call per event instead of a subscription per event -- subscriptions
|
|
262
|
+
// are an order of magnitude dearer per request (OPTIMIZATION.md item 5;
|
|
263
|
+
// lib/stream.js, emit()). The 'error' handler stays a
|
|
264
|
+
// subscription, because a stream the application never listens to must not
|
|
265
|
+
// throw on an unhandled 'error' -- the subscription is what keeps that from
|
|
266
|
+
// happening.
|
|
267
|
+
stream._compatDispatch = compatDispatch;
|
|
268
|
+
stream.on('error', onRequestError);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
get aborted() {
|
|
272
|
+
return this._aborted;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get complete() {
|
|
276
|
+
return this._aborted || this.readableEnded || this._closed || this._stream.destroyed;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get stream() {
|
|
280
|
+
return this._stream;
|
|
281
|
+
}
|
|
282
|
+
get headers() {
|
|
283
|
+
return this._headers;
|
|
284
|
+
}
|
|
285
|
+
get rawHeaders() {
|
|
286
|
+
return this._rawHeaders;
|
|
287
|
+
}
|
|
288
|
+
get trailers() {
|
|
289
|
+
return this._trailers;
|
|
290
|
+
}
|
|
291
|
+
get rawTrailers() {
|
|
292
|
+
return this._rawTrailers;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get httpVersionMajor() {
|
|
296
|
+
return 2;
|
|
297
|
+
}
|
|
298
|
+
get httpVersionMinor() {
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
get httpVersion() {
|
|
302
|
+
return '2.0';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// node hands out a Proxy over the stream pretending to be a socket. The real
|
|
306
|
+
// socket is smaller, faster and answers the same questions (remoteAddress).
|
|
307
|
+
get socket() {
|
|
308
|
+
return this._stream.session.socket;
|
|
309
|
+
}
|
|
310
|
+
get connection() {
|
|
311
|
+
return this.socket;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
get method() {
|
|
315
|
+
return this._headers[':method'];
|
|
316
|
+
}
|
|
317
|
+
set method(method) {
|
|
318
|
+
if (typeof method !== 'string') {
|
|
319
|
+
throw err('ERR_INVALID_ARG_TYPE', 'method must be a string');
|
|
320
|
+
}
|
|
321
|
+
if (method.trim() === '') {
|
|
322
|
+
throw err('ERR_INVALID_ARG_VALUE', 'method must not be empty');
|
|
323
|
+
}
|
|
324
|
+
assertWritable(this._headers);
|
|
325
|
+
this._headers[':method'] = method;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
get authority() {
|
|
329
|
+
const auth = this._headers[':authority'];
|
|
330
|
+
if (auth !== undefined) {
|
|
331
|
+
return auth;
|
|
332
|
+
}
|
|
333
|
+
const host = this._headers.host;
|
|
334
|
+
return host === undefined ? undefined : String(host).replace(/:[\d]+$/, '');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
get scheme() {
|
|
338
|
+
return this._headers[':scheme'];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
get url() {
|
|
342
|
+
return this._headers[':path'];
|
|
343
|
+
}
|
|
344
|
+
set url(url) {
|
|
345
|
+
assertWritable(this._headers);
|
|
346
|
+
this._headers[':path'] = url;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
setTimeout(msecs, callback) {
|
|
350
|
+
if (!this._closed) {
|
|
351
|
+
this._stream.setTimeout(msecs, callback);
|
|
352
|
+
}
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// node's pattern, and the reason the compat layer gets real backpressure: the
|
|
357
|
+
// stream only starts delivering when the application asks for data. Until then
|
|
358
|
+
// no credit is released, so the sender is the one that waits.
|
|
359
|
+
_read() {
|
|
360
|
+
if (this._closed || this._ended) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this._didRead = true;
|
|
364
|
+
const stream = this._stream;
|
|
365
|
+
// an 'end' that was deferred because nobody was reading is owed to us by the
|
|
366
|
+
// replay below; anything else means EOF already happened and there is
|
|
367
|
+
// nothing left to fetch
|
|
368
|
+
const owedEnd = stream._endPending;
|
|
369
|
+
if (stream.listenerCount('data') === 0) {
|
|
370
|
+
stream.on('data', onRequestData);
|
|
371
|
+
} else {
|
|
372
|
+
stream._startConsuming();
|
|
373
|
+
}
|
|
374
|
+
if (!owedEnd && stream.state.remoteClose === 1) {
|
|
375
|
+
this._ended = true;
|
|
376
|
+
this.push(null);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---- response -----------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
function onResponseDrain() {
|
|
384
|
+
const res = this._response;
|
|
385
|
+
if (res !== undefined) {
|
|
386
|
+
res.emit('drain');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function onResponseClose() {
|
|
391
|
+
const res = this._response;
|
|
392
|
+
if (res === undefined || res._state.closed) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
res._state.closed = true;
|
|
396
|
+
res.writable = false;
|
|
397
|
+
// node emits 'finish' and 'close' together, when the stream closes
|
|
398
|
+
// (internal/http2/compat, onStreamCloseResponse), so a response the peer cut
|
|
399
|
+
// short still reports 'finish'. h2lite emits 'finish' when the application
|
|
400
|
+
// ends the response, which is the same event one turn earlier; this covers
|
|
401
|
+
// the case where the application never got that far.
|
|
402
|
+
if (!res._state.finishEmitted) {
|
|
403
|
+
res._state.finishEmitted = true;
|
|
404
|
+
res.emit('finish');
|
|
405
|
+
}
|
|
406
|
+
res.emit('close');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function onResponseTimeout() {
|
|
410
|
+
const res = this._response;
|
|
411
|
+
if (res !== undefined && !res._state.closed) {
|
|
412
|
+
res.emit('timeout');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
class Http2ServerResponse extends Stream {
|
|
417
|
+
constructor(stream) {
|
|
418
|
+
super();
|
|
419
|
+
this._state = {
|
|
420
|
+
closed: false,
|
|
421
|
+
ending: false,
|
|
422
|
+
destroyed: false,
|
|
423
|
+
finishEmitted: false,
|
|
424
|
+
sendDate: true,
|
|
425
|
+
statusCode: 200,
|
|
426
|
+
headRequest: stream._requestHeaders
|
|
427
|
+
? stream._requestHeaders[':method'] === 'HEAD'
|
|
428
|
+
: false
|
|
429
|
+
};
|
|
430
|
+
// A fast null-prototype map, not a { __proto__: null } literal: the
|
|
431
|
+
// response's headers are written once and read once per response, and the
|
|
432
|
+
// literal's dictionary mode costs more for both (lib/headers.js, nullProto(),
|
|
433
|
+
// OPTIMIZATION.md item 4). The trailers map stays a literal: it is empty in
|
|
434
|
+
// every response that sends no trailers, and an empty fast map costs more to
|
|
435
|
+
// make than the literal (OPTIMIZATION.md item 4).
|
|
436
|
+
this._headers = nullProto();
|
|
437
|
+
this._trailers = { __proto__: null };
|
|
438
|
+
this._trailerNames = 0;
|
|
439
|
+
this._stream = stream;
|
|
440
|
+
stream._response = this;
|
|
441
|
+
this.writable = true;
|
|
442
|
+
this.req = stream._request;
|
|
443
|
+
// The response's handlers are reached through the stream's dispatch, which
|
|
444
|
+
// the request installed: see compatDispatch(). The request's handler runs
|
|
445
|
+
// first for the events both of them answer, which is the order the two
|
|
446
|
+
// sets of subscriptions had.
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Modules such as finalhandler only test the truthiness of this.
|
|
450
|
+
get _header() {
|
|
451
|
+
return this.headersSent;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
get headersSent() {
|
|
455
|
+
return this._stream.headersSent;
|
|
456
|
+
}
|
|
457
|
+
get stream() {
|
|
458
|
+
return this._stream;
|
|
459
|
+
}
|
|
460
|
+
get socket() {
|
|
461
|
+
return this._state.closed ? undefined : this._stream.session.socket;
|
|
462
|
+
}
|
|
463
|
+
get connection() {
|
|
464
|
+
return this.socket;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
get writableEnded() {
|
|
468
|
+
return this._state.ending;
|
|
469
|
+
}
|
|
470
|
+
get finished() {
|
|
471
|
+
return this._state.ending;
|
|
472
|
+
}
|
|
473
|
+
get writableFinished() {
|
|
474
|
+
return this._stream.state.localClose === 1;
|
|
475
|
+
}
|
|
476
|
+
get writableNeedDrain() {
|
|
477
|
+
return this._stream._queued !== null;
|
|
478
|
+
}
|
|
479
|
+
get writableLength() {
|
|
480
|
+
const q = this._stream._queued;
|
|
481
|
+
if (q === null) {
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
let n = 0;
|
|
485
|
+
for (let i = 0; i < q.length; i++) {
|
|
486
|
+
n += q[i].length;
|
|
487
|
+
}
|
|
488
|
+
return n;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
get statusCode() {
|
|
492
|
+
return this._state.statusCode;
|
|
493
|
+
}
|
|
494
|
+
set statusCode(code) {
|
|
495
|
+
code |= 0;
|
|
496
|
+
if (code >= 100 && code < 200) {
|
|
497
|
+
throw err('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', 'informational status codes cannot be set on statusCode');
|
|
498
|
+
}
|
|
499
|
+
if (code < 100 || code > 599) {
|
|
500
|
+
throw err('ERR_HTTP2_STATUS_INVALID', `invalid status code: ${code}`);
|
|
501
|
+
}
|
|
502
|
+
this._state.statusCode = code;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// HTTP/2 has no reason phrase; node warns and returns ''.
|
|
506
|
+
get statusMessage() {
|
|
507
|
+
process.emitWarning('HTTP/2 does not support a status message', 'Warning');
|
|
508
|
+
return '';
|
|
509
|
+
}
|
|
510
|
+
set statusMessage(msg) {
|
|
511
|
+
process.emitWarning('HTTP/2 does not support a status message', 'Warning');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
get sendDate() {
|
|
515
|
+
return this._state.sendDate;
|
|
516
|
+
}
|
|
517
|
+
set sendDate(bool) {
|
|
518
|
+
this._state.sendDate = Boolean(bool);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
getHeader(name) {
|
|
522
|
+
checkName(name);
|
|
523
|
+
return this._headers[trimName(name)];
|
|
524
|
+
}
|
|
525
|
+
getHeaderNames() {
|
|
526
|
+
return Object.keys(this._headers);
|
|
527
|
+
}
|
|
528
|
+
getHeaders() {
|
|
529
|
+
// The copy the application gets is a fast map as well, for the same
|
|
530
|
+
// reason: it is read once per call and handed to code that reads it.
|
|
531
|
+
const out = nullProto();
|
|
532
|
+
const source = this._headers;
|
|
533
|
+
for (const key of Object.keys(source)) {
|
|
534
|
+
out[key] = source[key];
|
|
535
|
+
}
|
|
536
|
+
return out;
|
|
537
|
+
}
|
|
538
|
+
hasHeader(name) {
|
|
539
|
+
checkName(name);
|
|
540
|
+
return Object.hasOwn(this._headers, trimName(name));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
removeHeader(name) {
|
|
544
|
+
checkName(name);
|
|
545
|
+
const key = trimName(name);
|
|
546
|
+
if (this._stream.headersSent) {
|
|
547
|
+
throw err('ERR_HTTP2_HEADERS_SENT', 'response has already been sent');
|
|
548
|
+
}
|
|
549
|
+
if (key === 'date') {
|
|
550
|
+
this._state.sendDate = false;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
delete this._headers[key];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
setHeader(name, value) {
|
|
557
|
+
checkName(name);
|
|
558
|
+
if (this._stream.headersSent) {
|
|
559
|
+
throw err('ERR_HTTP2_HEADERS_SENT', 'response has already been sent');
|
|
560
|
+
}
|
|
561
|
+
this._setHeader(name, value);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// The private half, the one writeHead() uses. node's kSetHeader does not
|
|
565
|
+
// validate the name either (internal/http2/compat.js), so a name that is not
|
|
566
|
+
// a string fails here the way it fails there: name.trim() is not a function.
|
|
567
|
+
_setHeader(name, value) {
|
|
568
|
+
this._headers[trimName(name)] = value;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
appendHeader(name, value) {
|
|
572
|
+
checkName(name);
|
|
573
|
+
if (this._stream.headersSent) {
|
|
574
|
+
throw err('ERR_HTTP2_HEADERS_SENT', 'response has already been sent');
|
|
575
|
+
}
|
|
576
|
+
const key = trimName(name);
|
|
577
|
+
const current = this._headers[key];
|
|
578
|
+
if (current === undefined) {
|
|
579
|
+
this._headers[key] = value;
|
|
580
|
+
} else if (Array.isArray(current)) {
|
|
581
|
+
current.push(value);
|
|
582
|
+
} else {
|
|
583
|
+
this._headers[key] = [current, value];
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
setTrailer(name, value) {
|
|
588
|
+
checkName(name);
|
|
589
|
+
const key = trimName(name);
|
|
590
|
+
if (this._trailers[key] === undefined) {
|
|
591
|
+
this._trailerNames++;
|
|
592
|
+
}
|
|
593
|
+
this._trailers[key] = value;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
addTrailers(headers) {
|
|
597
|
+
for (const key of Object.keys(headers)) {
|
|
598
|
+
this.setTrailer(key, headers[key]);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
writeHead(statusCode, statusMessage, headers) {
|
|
603
|
+
const state = this._state;
|
|
604
|
+
if (state.closed || this._stream.destroyed || this._stream.closed) {
|
|
605
|
+
return this;
|
|
606
|
+
}
|
|
607
|
+
if (this._stream.headersSent) {
|
|
608
|
+
throw err('ERR_HTTP2_HEADERS_SENT', 'response has already been sent');
|
|
609
|
+
}
|
|
610
|
+
if (typeof statusMessage === 'string') {
|
|
611
|
+
process.emitWarning('HTTP/2 does not support a status message', 'Warning');
|
|
612
|
+
}
|
|
613
|
+
if (headers === undefined && typeof statusMessage === 'object') {
|
|
614
|
+
headers = statusMessage;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (Array.isArray(headers)) {
|
|
618
|
+
if (headers.length && Array.isArray(headers[0])) {
|
|
619
|
+
for (let i = 0; i < headers.length; i++) {
|
|
620
|
+
this._setHeader(headers[i][0], headers[i][1]);
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
if (headers.length % 2 !== 0) {
|
|
624
|
+
throw err('ERR_INVALID_ARG_VALUE', 'headers array must be flat key/value pairs');
|
|
625
|
+
}
|
|
626
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
627
|
+
this._setHeader(headers[i], headers[i + 1]);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
} else if (typeof headers === 'object' && headers !== null) {
|
|
631
|
+
for (const key of Object.keys(headers)) {
|
|
632
|
+
this._setHeader(key, headers[key]);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
state.statusCode = statusCode;
|
|
637
|
+
this._beginSend();
|
|
638
|
+
return this;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
_beginSend() {
|
|
642
|
+
const state = this._state;
|
|
643
|
+
// node writes the status into the same map as the rest, so getHeaderNames()
|
|
644
|
+
// and getHeader(':status') report it after writeHead -- match that exactly
|
|
645
|
+
this._headers[':status'] = state.statusCode;
|
|
646
|
+
const stream = this._stream;
|
|
647
|
+
if (!stream.headersSent) {
|
|
648
|
+
// The map itself is the response's headers, as in node: its [kBeginSend]
|
|
649
|
+
// hands the stream the very map it filled from the application's headers
|
|
650
|
+
// (internal/http2/compat), so node has no second object on that path
|
|
651
|
+
// either. A merged copy was built here until the header path was measured:
|
|
652
|
+
// it bought nothing (OPTIMIZATION.md item 25) -- the map is never frozen
|
|
653
|
+
// and the HPACK block cache -- which is keyed by the identity of a frozen
|
|
654
|
+
// object -- cannot use it. h2lite's own stream.sentHeaders is that map;
|
|
655
|
+
// node's server-side getter
|
|
656
|
+
// returns undefined, because its respond() never sets the field.
|
|
657
|
+
stream.respond(this._headers, {
|
|
658
|
+
// a HEAD response must close the stream after the headers, and an
|
|
659
|
+
// application that called end() before flushing also does
|
|
660
|
+
endStream: state.ending || state.headRequest,
|
|
661
|
+
sendDate: state.sendDate,
|
|
662
|
+
waitForTrailers: true
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return this._headers;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
flushHeaders() {
|
|
669
|
+
const state = this._state;
|
|
670
|
+
if (!state.closed && !this._stream.headersSent) {
|
|
671
|
+
this.writeHead(state.statusCode);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
writeContinue() {
|
|
676
|
+
return this.writeInformation(100);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
writeInformation(statusCode, headers) {
|
|
680
|
+
// node refuses 101 here, in the compat layer, with ERR_HTTP2_STATUS_INVALID -- its own
|
|
681
|
+
// `statusCode === 101` check (internal/http2/compat:917) fires before the value can
|
|
682
|
+
// reach stream.additionalHeaders(), which would have reported ERR_HTTP2_STATUS_101.
|
|
683
|
+
// Both rules are therefore reachable, and which one an application sees depends on the
|
|
684
|
+
// API it called: measured against a live node server, compat writeInformation(101)
|
|
685
|
+
// throws ERR_HTTP2_STATUS_INVALID while stream.additionalHeaders({':status':101})
|
|
686
|
+
// throws ERR_HTTP2_STATUS_101. Nothing else about 101 is a judgement call: the status
|
|
687
|
+
// means "switch protocol", which HTTP/2 forbids outright (RFC 9113 8.5).
|
|
688
|
+
if (typeof statusCode !== 'number' || statusCode < 100 || statusCode > 199
|
|
689
|
+
|| statusCode === 101) {
|
|
690
|
+
throw err('ERR_HTTP2_STATUS_INVALID', `invalid informational status code: ${statusCode}`);
|
|
691
|
+
}
|
|
692
|
+
if (this._stream.headersSent || this._state.closed) {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
const out = { __proto__: null, ':status': statusCode };
|
|
696
|
+
if (headers !== undefined && headers !== null) {
|
|
697
|
+
for (const key of Object.keys(headers)) {
|
|
698
|
+
out[key] = headers[key];
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
this._stream.additionalHeaders(out);
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
write(chunk, encoding, cb) {
|
|
706
|
+
if (typeof encoding === 'function') {
|
|
707
|
+
cb = encoding; encoding = 'utf8';
|
|
708
|
+
}
|
|
709
|
+
const state = this._state;
|
|
710
|
+
let failure;
|
|
711
|
+
if (state.ending) {
|
|
712
|
+
failure = err('ERR_STREAM_WRITE_AFTER_END', 'write after end');
|
|
713
|
+
} else if (state.closed) {
|
|
714
|
+
failure = err('ERR_HTTP2_INVALID_STREAM', 'the stream has been closed');
|
|
715
|
+
} else if (state.destroyed) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
if (failure) {
|
|
719
|
+
if (typeof cb === 'function') {
|
|
720
|
+
process.nextTick(cb, failure);
|
|
721
|
+
}
|
|
722
|
+
this.destroy(failure);
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
if (state.headRequest) {
|
|
726
|
+
if (typeof cb === 'function') {
|
|
727
|
+
process.nextTick(cb);
|
|
728
|
+
}
|
|
729
|
+
return true; // a HEAD response has no body
|
|
730
|
+
}
|
|
731
|
+
if (!this._stream.headersSent) {
|
|
732
|
+
this.writeHead(state.statusCode);
|
|
733
|
+
}
|
|
734
|
+
return this._stream.write(chunk, encoding, cb);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
end(chunk, encoding, cb) {
|
|
738
|
+
const stream = this._stream;
|
|
739
|
+
const state = this._state;
|
|
740
|
+
if (typeof chunk === 'function') {
|
|
741
|
+
cb = chunk; chunk = null;
|
|
742
|
+
} else if (typeof encoding === 'function') {
|
|
743
|
+
cb = encoding; encoding = 'utf8';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (state.closed || state.ending) {
|
|
747
|
+
if (typeof cb === 'function') {
|
|
748
|
+
process.nextTick(cb);
|
|
749
|
+
}
|
|
750
|
+
return this;
|
|
751
|
+
}
|
|
752
|
+
if (chunk !== null && chunk !== undefined && !state.headRequest) {
|
|
753
|
+
this.write(chunk, encoding);
|
|
754
|
+
}
|
|
755
|
+
state.ending = true;
|
|
756
|
+
|
|
757
|
+
if (!stream.headersSent) {
|
|
758
|
+
this.writeHead(state.statusCode);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// one nextTick for the whole completion, and the callback hung off the
|
|
762
|
+
// event rather than scheduled separately -- node does the same. Queued here,
|
|
763
|
+
// before the close below, so 'finish' precedes the teardown events.
|
|
764
|
+
if (typeof cb === 'function') {
|
|
765
|
+
this.once('finish', cb);
|
|
766
|
+
}
|
|
767
|
+
if (!state.closed && !state.finishEmitted) {
|
|
768
|
+
state.finishEmitted = true;
|
|
769
|
+
process.nextTick(emitNT, this, 'finish');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (this._trailerNames > 0) {
|
|
773
|
+
// node sends the trailers from the 'wantTrailers' callback; h2lite closes
|
|
774
|
+
// the local side as soon as the application says so, so the recorded
|
|
775
|
+
// trailers go out here, carrying END_STREAM with them
|
|
776
|
+
stream.sendTrailers(this._trailers);
|
|
777
|
+
} else {
|
|
778
|
+
// The response was opened with waitForTrailers so that addTrailers() could
|
|
779
|
+
// still insert a HEADERS frame. No trailers were added, so the stream must
|
|
780
|
+
// not wait for them: drop the request and close the local half now.
|
|
781
|
+
stream._waitForTrailers = false;
|
|
782
|
+
stream.end();
|
|
783
|
+
}
|
|
784
|
+
return this;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
destroy(error) {
|
|
788
|
+
const state = this._state;
|
|
789
|
+
if (state.destroyed) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
state.destroyed = true;
|
|
793
|
+
this._stream.destroy(error || undefined);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
setTimeout(msecs, callback) {
|
|
797
|
+
if (this._state.closed) {
|
|
798
|
+
return this;
|
|
799
|
+
}
|
|
800
|
+
this._stream.setTimeout(msecs, callback);
|
|
801
|
+
return this;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
createPushResponse(headers, callback) {
|
|
805
|
+
const e = err('ERR_H2LITE_NOT_IMPLEMENTED', 'h2lite v1 does not implement server push');
|
|
806
|
+
if (typeof callback !== 'function') {
|
|
807
|
+
throw e;
|
|
808
|
+
}
|
|
809
|
+
process.nextTick(callback, e);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// node's default 'stream' handler. Called with the server as `this`.
|
|
814
|
+
function onServerStream(stream, headers, options, rawHeaders) {
|
|
815
|
+
const server = this;
|
|
816
|
+
const request = new Http2ServerRequest(stream, headers, rawHeaders);
|
|
817
|
+
stream._requestHeaders = headers;
|
|
818
|
+
const response = new Http2ServerResponse(stream);
|
|
819
|
+
|
|
820
|
+
const method = headers[':method'];
|
|
821
|
+
if (method === 'CONNECT') {
|
|
822
|
+
if (!server.emit('connect', request, response)) {
|
|
823
|
+
response.statusCode = 405;
|
|
824
|
+
response.end();
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (headers.expect !== undefined) {
|
|
830
|
+
if (headers.expect === '100-continue') {
|
|
831
|
+
if (server.listenerCount('checkContinue') > 0) {
|
|
832
|
+
server.emit('checkContinue', request, response);
|
|
833
|
+
} else {
|
|
834
|
+
response.writeContinue();
|
|
835
|
+
server.emit('request', request, response);
|
|
836
|
+
}
|
|
837
|
+
} else if (server.listenerCount('checkExpectation') > 0) {
|
|
838
|
+
server.emit('checkExpectation', request, response);
|
|
839
|
+
} else {
|
|
840
|
+
response.statusCode = 417;
|
|
841
|
+
response.end();
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
server.emit('request', request, response);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
module.exports = { Http2ServerRequest, Http2ServerResponse, onServerStream };
|