h2lean 0.0.0-stage → 0.1.1

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/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 h2lean 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: h2lean 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_H2LEAN_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
+ // h2lean 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'), h2lean 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_H2LEAN_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'. h2lean 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. h2lean'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; h2lean 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_H2LEAN_NOT_IMPLEMENTED', 'h2lean 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 };