h2lean 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  <div align="center">
2
2
 
3
- <img src="assets/banner.svg" alt="h2lite banner" width="720">
3
+ <img src="assets/banner.svg" alt="h2lean banner" width="720">
4
4
 
5
5
  ### The pure-JavaScript HTTP/2 stack for Node.js
6
6
 
7
7
  **Drop-in replacement for `node:http2` — without the nghttp2 C layer.**
8
8
 
9
- [![npm](https://img.shields.io/badge/npm-v0.1.0-cb3837)](https://www.npmjs.com/package/h2lite)
9
+ [![npm](https://img.shields.io/badge/npm-v0.1.0-cb3837)](https://www.npmjs.com/package/h2lean)
10
10
  [![node](https://img.shields.io/badge/node-%E2%89%A520-339933)](https://nodejs.org)
11
11
  [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](package.json)
12
12
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
@@ -19,7 +19,7 @@ Up to **8× less CPU per request** and **6.8× more requests per second** than `
19
19
 
20
20
  ## 📑 Table of Contents
21
21
 
22
- - ⚡ [Why h2lite?](#-why-h2lite)
22
+ - ⚡ [Why h2lean?](#-why-h2lean)
23
23
  - 🏎️ [Performance](#️-performance)
24
24
  - 🔌 [Compatibility — Drop-in Replacement](#-compatibility--drop-in-replacement)
25
25
  - 📦 [Installation](#-installation)
@@ -37,18 +37,18 @@ Up to **8× less CPU per request** and **6.8× more requests per second** than `
37
37
  - 🤝 [Contributing](#-contributing)
38
38
  - ⚖️ [License](#️-license)
39
39
 
40
- ## ⚡ Why h2lite?
40
+ ## ⚡ Why h2lean?
41
41
 
42
42
  Node's native `node:http2` (stable since v10.10.0) has a fundamental architectural constraint: every HTTP/2 frame crosses the JS ↔ C++ boundary between V8 and bundled nghttp2. Frame dispatch, HPACK encoding/decoding, and flow-control bookkeeping all pay for that crossing — on every single frame of every single request.
43
43
 
44
- **h2lite removes the crossing entirely.** The entire protocol — frame parsing, HPACK (RFC 7541), flow control, connection state machines — lives in pure JavaScript, tuned item by item against measured data ([OPTIMIZATION.md](./OPTIMIZATION.md), 35 numbered techniques). Zero native code, zero runtime dependencies.
44
+ **h2lean removes the crossing entirely.** The entire protocol — frame parsing, HPACK (RFC 7541), flow control, connection state machines — lives in pure JavaScript, tuned item by item against measured data ([OPTIMIZATION.md](./OPTIMIZATION.md), 35 numbered techniques). Zero native code, zero runtime dependencies.
45
45
 
46
46
  The result is a library that keeps `node:http2`'s exact public surface while spending dramatically less CPU per request. Perfect for:
47
47
 
48
48
  - 🧩 **Microservices** — header-heavy service-to-service traffic is where the gap is widest
49
49
  - 📡 **gRPC servers** — many small streams, all header-bound
50
50
  - 🤖 **AI backends** — long-lived streams, high connection counts, every CPU cycle matters
51
- - 🔁 **Behind a reverse proxy** — pair h2lite with an HTTP/2-terminating edge and stop paying for HTTP/1.1
51
+ - 🔁 **Behind a reverse proxy** — pair h2lean with an HTTP/2-terminating edge and stop paying for HTTP/1.1
52
52
 
53
53
  ## 🏎️ Performance
54
54
 
@@ -61,14 +61,14 @@ Every number below is `node`'s own per-request CPU time (μs/req, read inside th
61
61
  | node:http (HTTP/1.1) | 9.11 | 111,816 | 1.21× |
62
62
  | uWebSockets C++ (HTTP/1.1) | 4.87 | 203,283 | 2.27× |
63
63
  | node:http2 (stream API) | 7.30 | 171,835 | 1.52× |
64
- | **h2lite (stream API)** | **1.38** | **765,149** | **8.01×** |
65
- | **h2lite (compat API)** | **2.85** | **367,275** | **3.88×** |
64
+ | **h2lean (stream API)** | **1.38** | **765,149** | **8.01×** |
65
+ | **h2lean (compat API)** | **2.85** | **367,275** | **3.88×** |
66
66
 
67
- **Server, with response body:** 16 kB — **2.50× less CPU** than `node:http2` ([PERFORMANCE.md](./PERFORMANCE.md)). 256 kB — parity, as expected: the work is body-bound, and h2lite does not win by cutting corners.
67
+ **Server, with response body:** 16 kB — **2.50× less CPU** than `node:http2` ([PERFORMANCE.md](./PERFORMANCE.md)). 256 kB — parity, as expected: the work is body-bound, and h2lean does not win by cutting corners.
68
68
 
69
- **Client, same-server paired measurement** (`h2lite.connect()` vs `node:http2.connect()`, driven against nghttpd):
69
+ **Client, same-server paired measurement** (`h2lean.connect()` vs `node:http2.connect()`, driven against nghttpd):
70
70
 
71
- | Workload | node client μs/req | h2lite client μs/req | Advantage |
71
+ | Workload | node client μs/req | h2lean client μs/req | Advantage |
72
72
  |---|--:|--:|---|
73
73
  | Headers only | 12.08 | **3.61** | 3.35× |
74
74
  | 16 kB | 19.66 | **11.73** | 1.68× |
@@ -78,11 +78,11 @@ Every number below is `node`'s own per-request CPU time (μs/req, read inside th
78
78
 
79
79
  ## 🔌 Compatibility — Drop-in Replacement
80
80
 
81
- Compatibility target is the **public surface** of `node:http2`: names, argument shapes, event orders, wire-visible frame sequences, `rstCode`/`goawayCode` numbers — verified differentially. The test suite runs the **same application scripts** against both `node:http2` and h2lite and compares the transcripts ([test/diff-harness.js](./test/diff-harness.js)); [h2spec](https://github.com/summerwind/h2spec) conformance is 146/146 — the same verdict node's own HTTP/2 receives.
81
+ Compatibility target is the **public surface** of `node:http2`: names, argument shapes, event orders, wire-visible frame sequences, `rstCode`/`goawayCode` numbers — verified differentially. The test suite runs the **same application scripts** against both `node:http2` and h2lean and compares the transcripts ([test/diff-harness.js](./test/diff-harness.js)); [h2spec](https://github.com/summerwind/h2spec) conformance is 146/146 — the same verdict node's own HTTP/2 receives.
82
82
 
83
83
  ```diff
84
84
  - const http2 = require('node:http2');
85
- + const http2 = require('h2lite');
85
+ + const http2 = require('h2lean');
86
86
 
87
87
  const server = http2.createServer((req, res) => {
88
88
  res.end('hello world');
@@ -97,7 +97,7 @@ Where behavior deliberately differs from node, it is **documented, measured, and
97
97
  ## 📦 Installation
98
98
 
99
99
  ```bash
100
- npm install h2lite
100
+ npm install h2lean
101
101
  ```
102
102
 
103
103
  Requires Node.js **≥ 20** (verified against v26.12.1). Zero runtime dependencies — `lib/` imports nothing but `node:` builtins.
@@ -109,12 +109,12 @@ Requires Node.js **≥ 20** (verified against v26.12.1). Zero runtime dependenci
109
109
  The compat API feels like HTTP/1.1 but speaks HTTP/2 — this is what replaces `node:http2` in an existing application:
110
110
 
111
111
  ```javascript
112
- const http2 = require('h2lite');
112
+ const http2 = require('h2lean');
113
113
 
114
114
  const server = http2.createServer((req, res) => {
115
115
  // req is an Http2ServerRequest, res is an Http2ServerResponse
116
116
  res.writeHead(200, { 'content-type': 'text/plain' });
117
- res.end('hello from h2lite');
117
+ res.end('hello from h2lean');
118
118
  });
119
119
 
120
120
  server.listen(8080);
@@ -125,7 +125,7 @@ server.listen(8080);
125
125
  Maximum throughput: skip the compat objects entirely and work with streams and header maps:
126
126
 
127
127
  ```javascript
128
- const http2 = require('h2lite');
128
+ const http2 = require('h2lean');
129
129
 
130
130
  const server = http2.createServer();
131
131
 
@@ -140,7 +140,7 @@ server.listen(8081);
140
140
  ### Secure server (h2s / TLS)
141
141
 
142
142
  ```javascript
143
- const http2 = require('h2lite');
143
+ const http2 = require('h2lean');
144
144
 
145
145
  const server = http2.createSecureServer({
146
146
  key: require('fs').readFileSync('key.pem'),
@@ -160,7 +160,7 @@ TLS decisions stay delegated to `node:tls` — ALPN is pinned to `h2` exactly th
160
160
  ### Client — `connect()`
161
161
 
162
162
  ```javascript
163
- const http2 = require('h2lite');
163
+ const http2 = require('h2lean');
164
164
 
165
165
  const session = http2.connect('http://localhost:8080');
166
166
 
@@ -179,20 +179,20 @@ Works over `https://…` too — the client speaks both h2c and h2s.
179
179
 
180
180
  ### Fastify and other frameworks
181
181
 
182
- Any framework that lets you hand it a server instance takes h2lite in two lines. Fastify's [`serverFactory`](https://fastify.dev/docs/latest/Reference/Server/#serverfactory):
182
+ Any framework that lets you hand it a server instance takes h2lean in two lines. Fastify's [`serverFactory`](https://fastify.dev/docs/latest/Reference/Server/#serverfactory):
183
183
 
184
184
  ```javascript
185
185
  const Fastify = require('fastify');
186
- const h2lite = require('h2lite');
186
+ const h2lean = require('h2lean');
187
187
 
188
188
  const fastify = Fastify({
189
189
  http2: true,
190
190
  serverFactory: (handler) => {
191
- return h2lite.createServer((req, res) => handler(req, res));
191
+ return h2lean.createServer((req, res) => handler(req, res));
192
192
  }
193
193
  });
194
194
 
195
- fastify.get('/', async () => ({ hello: 'from h2lite under Fastify' }));
195
+ fastify.get('/', async () => ({ hello: 'from h2lean under Fastify' }));
196
196
  fastify.listen({ port: 8080 });
197
197
  ```
198
198
 
@@ -203,16 +203,16 @@ If a framework requires `node:http2` internally and offers no injection point, p
203
203
  ```javascript
204
204
  // patch.js — require this first: node -r ./patch.js app.js
205
205
  const nodeHttp2 = require('node:http2');
206
- const h2lite = require('h2lite');
206
+ const h2lean = require('h2lean');
207
207
 
208
- nodeHttp2.createServer = h2lite.createServer;
209
- nodeHttp2.createSecureServer = h2lite.createSecureServer;
210
- nodeHttp2.connect = h2lite.connect;
208
+ nodeHttp2.createServer = h2lean.createServer;
209
+ nodeHttp2.createSecureServer = h2lean.createSecureServer;
210
+ nodeHttp2.connect = h2lean.connect;
211
211
  ```
212
212
 
213
213
  ## 🛠️ Advanced Tuning
214
214
 
215
- - ♻️ **Freeze reusable header objects.** `Object.freeze()` on a header set enables h2lite's per-connection response-block cache — the HPACK bytes are encoded once and copied afterwards. Measured **~25% CPU saved** on a headers-only workload ([PERFORMANCE.md](./PERFORMANCE.md)):
215
+ - ♻️ **Freeze reusable header objects.** `Object.freeze()` on a header set enables h2lean's per-connection response-block cache — the HPACK bytes are encoded once and copied afterwards. Measured **~25% CPU saved** on a headers-only workload ([PERFORMANCE.md](./PERFORMANCE.md)):
216
216
 
217
217
  ```javascript
218
218
  const HEADERS = Object.freeze({ ':status': 200, 'content-type': 'application/json' });
@@ -225,18 +225,18 @@ nodeHttp2.connect = h2lite.connect;
225
225
 
226
226
  ## 🔒 Security
227
227
 
228
- h2lite terminates **untrusted bytes from the internet** and turns them into objects your application trusts — the threat model is treated as the primary design constraint ([SECURITY.md](./SECURITY.md)):
228
+ h2lean terminates **untrusted bytes from the internet** and turns them into objects your application trusts — the threat model is treated as the primary design constraint ([SECURITY.md](./SECURITY.md)):
229
229
 
230
- - 🛡️ **Every denial-of-service limit matches nghttp2** (the engine inside node) or was measured against a live node server — a peer's budget against node is its budget against h2lite: SETTINGS-entry cap (CVE-2020-11080), CONTINUATION cap (CVE-2024-28182), rapid-reset budget (CVE-2023-44487), outbound staging, parked-stream and history bounds.
231
- - 🚫 **Request-smuggling defense by construction** (RFC 9113 §10.3): field names must be lower-case tokens, values may not carry NUL/CR/LF, connection-specific fields are refused in both directions — all refused at encode time, *before a single octet reaches the wire*. h2lite is stricter than node here, deliberately and measurably.
230
+ - 🛡️ **Every denial-of-service limit matches nghttp2** (the engine inside node) or was measured against a live node server — a peer's budget against node is its budget against h2lean: SETTINGS-entry cap (CVE-2020-11080), CONTINUATION cap (CVE-2024-28182), rapid-reset budget (CVE-2023-44487), outbound staging, parked-stream and history bounds.
231
+ - 🚫 **Request-smuggling defense by construction** (RFC 9113 §10.3): field names must be lower-case tokens, values may not carry NUL/CR/LF, connection-specific fields are refused in both directions — all refused at encode time, *before a single octet reaches the wire*. h2lean is stricter than node here, deliberately and measurably.
232
232
  - ✅ **Verified, not assumed:**
233
- - **300+ differential tests** — application scripts run against both `node:http2` and h2lite, transcripts compared;
233
+ - **300+ differential tests** — application scripts run against both `node:http2` and h2lean, transcripts compared;
234
234
  - **h2spec 146/146** — full RFC 9113/7541 conformance, same verdict as node's own HTTP/2 (`npm run spec`);
235
235
  - **mutation-tested guards** — every defensive limit is rewritten in place and proven to fail the suite ([tools/mutate-guards.js](./tools/mutate-guards.js)).
236
236
 
237
237
  ## ⚠️ Not Implemented — Said Loudly
238
238
 
239
- Two APIs are absent, and both fail **loudly** with `ERR_H2LITE_NOT_IMPLEMENTED` — never a silent wrong answer:
239
+ Two APIs are absent, and both fail **loudly** with `ERR_H2LEAN_NOT_IMPLEMENTED` — never a silent wrong answer:
240
240
 
241
241
  - **Server push** — removed from browsers in 2022 and considered obsolete; the safer replacement is [Early Hints (103)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/103).
242
242
  - **`performServerHandshake`** — no known use case outside node's own tests.
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // h2lite -- a pure-JS HTTP/2 library with a node:http2-shaped API.
1
+ // h2lean -- a pure-JS HTTP/2 library with a node:http2-shaped API.
2
2
  //
3
3
  // Servers: h2c and h2s (prior-knowledge, ALPN 'h2'), the `stream` and compat
4
4
  // (`request`) APIs. Client: `connect()` + `session.request()` over h2c and h2s
@@ -561,7 +561,7 @@ class ClientHttp2Session extends EventEmitter {
561
561
  // is up (internal/http2/core.js setupHandle -> nextTick(emit, 'connect')) and
562
562
  // submits the request then, so the request goes out in the first flight. Measured
563
563
  // against a raw server that delays its SETTINGS by 200 ms: node's request HEADERS
564
- // arrive at 5 ms, while h2lite's used to arrive at 225 ms -- after the SETTINGS it
564
+ // arrive at 5 ms, while h2lean's used to arrive at 225 ms -- after the SETTINGS it
565
565
  // was waiting for -- which is a round trip on every session's first request
566
566
  // (tools/probe-client-first.js).
567
567
  //
@@ -1795,7 +1795,7 @@ class ClientHttp2Session extends EventEmitter {
1795
1795
  // state.pendingStreams.forEach((stream) => stream.destroy(cancel))) so an
1796
1796
  // application can tell "my request was never sent" from "the connection broke
1797
1797
  // under it". Measured on a TLS handshake failure: node answers a queued request
1798
- // with stream error ERR_HTTP2_STREAM_CANCEL before the session error, h2lite used
1798
+ // with stream error ERR_HTTP2_STREAM_CANCEL before the session error, h2lean used
1799
1799
  // to leave it silent.
1800
1800
  if (this._queuedReq !== null && this._queuedReq.length > 0) {
1801
1801
  for (const r of this._queuedReq) {
@@ -4,7 +4,7 @@
4
4
  // `internal/http2/core.js`. Two shapes are load-bearing for a drop-in:
5
5
  // * it is a `stream.Duplex` with `allowHalfOpen: true`, so ending the request
6
6
  // body (writable) does not close the response (readable). node's `Http2Stream`
7
- // extends Duplex (core.js:2128); h2lite's SERVER stream is an EventEmitter
7
+ // extends Duplex (core.js:2128); h2lean's SERVER stream is an EventEmitter
8
8
  // (AGENTS.md 10) but a client stream the application `.pipe()`s and reads
9
9
  // `:status` off has to be a real Duplex or the common
10
10
  // `req.pipe(server).pipe(res)` idiom breaks.
package/lib/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // connect(): the client front door. Mirrors node's `connect(authority, options,
2
2
  // listener)` in internal/http2/core.js -- same argument juggling, same
3
3
  // authority/protocol/port derivation, same `createConnection` override -- so that
4
- // `require('h2lite').connect(url)` is a drop-in for `require('node:http2').connect(url)`.
4
+ // `require('h2lean').connect(url)` is a drop-in for `require('node:http2').connect(url)`.
5
5
  //
6
6
  // Both transports are supported: h2c over `net`, and h2s over `tls.connect` the
7
7
  // way core.js initializeTLSOptions builds it -- the ALPN list is forced to ['h2']
@@ -19,8 +19,8 @@ const tls = require('node:tls');
19
19
  const { ClientHttp2Session } = require('./client-session');
20
20
 
21
21
  function notImplemented(what) {
22
- const err = new Error(`h2lite: not implemented: ${what}`);
23
- err.code = 'ERR_H2LITE_NOT_IMPLEMENTED';
22
+ const err = new Error(`h2lean: not implemented: ${what}`);
23
+ err.code = 'ERR_H2LEAN_NOT_IMPLEMENTED';
24
24
  return err;
25
25
  }
26
26
 
package/lib/compat.js CHANGED
@@ -5,22 +5,22 @@
5
5
  //
6
6
  // Deliberate differences from node, all of them loud rather than silent:
7
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
8
+ // is still replenished, because h2lean releases stream credit as the buffered
9
9
  // body is consumed and connection credit in half-window steps, neither of them
10
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
11
+ // * res.cork()/uncork() are absent, not no-ops: h2lean already coalesces every
12
12
  // frame produced in one turn of the loop into a single write().
13
13
  // * res.write() returning false is honest backpressure, and 'drain' is emitted
14
14
  // when the send window reopens.
15
- // * createPushResponse() throws ERR_H2LITE_NOT_IMPLEMENTED (no server push in
15
+ // * createPushResponse() throws ERR_H2LEAN_NOT_IMPLEMENTED (no server push in
16
16
  // v1) instead of failing later with a protocol error.
17
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
18
+ // h2lean happens as the frames are queued for the socket; node reports it
19
19
  // when the stream closes, which is later. A response the peer cut short
20
20
  // reports 'finish' on both stacks.
21
21
  // * the order of the teardown events differs: node reports the request's
22
22
  // events (its 'end', its 'aborted', its 'close') before the response's
23
- // ('finish', then 'close'), h2lite reports the response's first. Every
23
+ // ('finish', then 'close'), h2lean reports the response's first. Every
24
24
  // event fires on both stacks -- measured against a live node server over
25
25
  // the four shapes of bench/micro/probe-teardown-order.js, the sets are equal -- and
26
26
  // only their order within the teardown turn differs. One pair is not a
@@ -108,7 +108,7 @@ function describeType(value) {
108
108
  // rewrite is refused loudly instead of silently corrupting another request.
109
109
  function assertWritable(headers) {
110
110
  if (Object.isFrozen(headers)) {
111
- throw err('ERR_H2LITE_FROZEN_HEADERS',
111
+ throw err('ERR_H2LEAN_FROZEN_HEADERS',
112
112
  'req.headers is frozen because the server was created with shareHeaders:true');
113
113
  }
114
114
  }
@@ -396,7 +396,7 @@ function onResponseClose() {
396
396
  res.writable = false;
397
397
  // node emits 'finish' and 'close' together, when the stream closes
398
398
  // (internal/http2/compat, onStreamCloseResponse), so a response the peer cut
399
- // short still reports 'finish'. h2lite emits 'finish' when the application
399
+ // short still reports 'finish'. h2lean emits 'finish' when the application
400
400
  // ends the response, which is the same event one turn earlier; this covers
401
401
  // the case where the application never got that far.
402
402
  if (!res._state.finishEmitted) {
@@ -651,7 +651,7 @@ class Http2ServerResponse extends Stream {
651
651
  // either. A merged copy was built here until the header path was measured:
652
652
  // it bought nothing (OPTIMIZATION.md item 25) -- the map is never frozen
653
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;
654
+ // object -- cannot use it. h2lean's own stream.sentHeaders is that map;
655
655
  // node's server-side getter
656
656
  // returns undefined, because its respond() never sets the field.
657
657
  stream.respond(this._headers, {
@@ -770,7 +770,7 @@ class Http2ServerResponse extends Stream {
770
770
  }
771
771
 
772
772
  if (this._trailerNames > 0) {
773
- // node sends the trailers from the 'wantTrailers' callback; h2lite closes
773
+ // node sends the trailers from the 'wantTrailers' callback; h2lean closes
774
774
  // the local side as soon as the application says so, so the recorded
775
775
  // trailers go out here, carrying END_STREAM with them
776
776
  stream.sendTrailers(this._trailers);
@@ -802,7 +802,7 @@ class Http2ServerResponse extends Stream {
802
802
  }
803
803
 
804
804
  createPushResponse(headers, callback) {
805
- const e = err('ERR_H2LITE_NOT_IMPLEMENTED', 'h2lite v1 does not implement server push');
805
+ const e = err('ERR_H2LEAN_NOT_IMPLEMENTED', 'h2lean v1 does not implement server push');
806
806
  if (typeof callback !== 'function') {
807
807
  throw e;
808
808
  }
package/lib/fast-timer.js CHANGED
@@ -4,10 +4,10 @@
4
4
  //
5
5
  // This file is deliberately a byte-for-byte copy of that source, with no local
6
6
  // changes, so that it can be lifted out into a standalone npm package and have
7
- // h2lite require it instead. Any deviation here would make that extraction a
7
+ // h2lean require it instead. Any deviation here would make that extraction a
8
8
  // merge rather than a move, and would break the upstream tests that come with it.
9
9
  //
10
- // What h2lite uses it for: one shared 499 ms clock for all idle/keep-alive
10
+ // What h2lean uses it for: one shared 499 ms clock for all idle/keep-alive
11
11
  // timeouts, instead of one libuv timer handle per connection. See
12
12
  // Http2Session.setTimeout() in lib/session.js.
13
13
 
package/lib/headers.js CHANGED
@@ -160,7 +160,7 @@ function nameCacheSize() {
160
160
  // A field whose value carries one is ignored (NGHTTP2_ERR_IGN_HTTP_HEADER) and the
161
161
  // stream lives -- the request is served, without the field.
162
162
  //
163
- // The OUTBOUND check is h2lite's own, and stricter than node's: node does not look at
163
+ // The OUTBOUND check is h2lean's own, and stricter than node's: node does not look at
164
164
  // an outbound value at all and puts a CR on the wire (bench/micro/probe-out-value-matrix.js --
165
165
  // every one of the fifteen characters tried is accepted, and the CR arrives in the
166
166
  // HEADERS frame). Refusing here is the mitigation the CVE-2020-8287 class needs: the
package/lib/hpack.js CHANGED
@@ -1,4 +1,4 @@
1
- // HPACK codec (RFC 7541) for h2lite.
1
+ // HPACK codec (RFC 7541) for h2lean.
2
2
  //
3
3
  // Deliberate choices, all CPU-driven:
4
4
  // * field names are interned, so a repeated name costs no new string and the
package/lib/server.js CHANGED
@@ -6,10 +6,10 @@
6
6
  // documentation:
7
7
  // * node's Http2Server has close, setTimeout and updateSettings -- and NOTHING else: no
8
8
  // sessionTimeout, no closeIdleSessions, no sessions getter, no settings property. node keeps
9
- // the settings updateSettings merges in a private options object; h2lite keeps them in the
9
+ // the settings updateSettings merges in a private options object; h2lean keeps them in the
10
10
  // public `settings` field it also builds sessions from, which is a third name node does not
11
11
  // have. The extras implemented here are additive, not substitutions:
12
- // server.sessions -- the live session set, kept because h2lite tracks sessions
12
+ // server.sessions -- the live session set, kept because h2lean tracks sessions
13
13
  // in it and node keeps the same set under a symbol;
14
14
  // server.closeIdleSessions() -- documented by node for years and gone in 26; the
15
15
  // keep-alive policy it served is session.setTimeout() plus
@@ -26,14 +26,30 @@
26
26
  // * closing a connection is not the same MOMENT on the two stacks, and one event depends on it.
27
27
  // node's session survives close() until its graceful close completes (internal/http2/core.js:
28
28
  // handle.ongracefulclosecomplete -> kMaybeDestroy), so it can still read -- and report -- the
29
- // peer's answer to the GOAWAY we just sent. h2lite destroys the session as soon as that GOAWAY
29
+ // peer's answer to the GOAWAY we just sent. h2lean destroys the session as soon as that GOAWAY
30
30
  // is written (lib/session.js: close() -> _maybeFinishClose), but its socket stays readable
31
31
  // until the write is flushed, so the answer is read there too. The two therefore disagree
32
32
  // about ONE event -- the 'goaway' the peer's answer produces -- and only inside a window:
33
33
  // measured with tools/probe-goaway-echo.js, node's outcome moves with the load (40 of 40
34
- // rounds on an idle machine, 26 of 40 and 29 of 30 in two loaded runs) while h2lite reports
34
+ // rounds on an idle machine, 26 of 40 and 29 of 30 in two loaded runs) while h2lean reports
35
35
  // the echo in every round of every run. test/event-order.test.js drops the echo outside its
36
36
  // 'goaway' scenario for that reason.
37
+ // * allowHTTP1 + http1Options (both createSecureServer options, per node's docs): when the
38
+ // fallback is live, node serves HTTP/1.1 from the SAME object -- _http_server's private
39
+ // storeHTTPOptions() writes the HTTP/1 options into private fields of the Http2SecureServer
40
+ // and http's _connectionListener is bound to it -- so one emitter and one listener list
41
+ // carry both protocols. Those symbols are not reachable from node:http, so the fallback
42
+ // here is an embedded http.Server that never listens, fed the sockets the ALPN verdict
43
+ // hands it, with a newListener mirror (node's own setupCompat hook) carrying the
44
+ // application's request/upgrade/clientError/... listeners onto it. Three written-down
45
+ // consequences of that model: a removeListener does not unmirror (node has no such seam);
46
+ // the embedded server's own idle-connection bookkeeping never starts, because its
47
+ // 'listening' never fires, so the handed-off sockets are tracked here and close() shuts
48
+ // the idle keep-alive ones down while letting a response in flight finish (node waits
49
+ // through net.Server's own bookkeeping); and an unknownProtocol socket -- no ALPN, no
50
+ // allowHTTP1 -- is answered with node's 403 text instead of being destroyed silently,
51
+ // which is what node does (core.js connectionListener) and what this file used to claim
52
+ // wrong: no ALPN is not h2 prior knowledge, and it never became a session on node.
37
53
  'use strict';
38
54
 
39
55
  const net = require('node:net');
@@ -41,17 +57,17 @@ const tls = require('node:tls');
41
57
  const { Http2Session, OUR_DEFAULTS } = require('./session');
42
58
  const { onServerStream, Http2ServerRequest, Http2ServerResponse } = require('./compat');
43
59
 
44
- const kCompat = Symbol('h2lite.compatListener');
60
+ const kCompat = Symbol('h2lean.compatListener');
45
61
  // The transport's own close/on, captured before the shared API overrides them:
46
62
  // net.Server and tls.Server are unrelated base classes and JS has only single
47
63
  // inheritance, so the shared behaviour is mixed onto the prototype the same way
48
64
  // node's own core does it, with the overridden methods kept reachable by symbol.
49
- const kTransportClose = Symbol('h2lite.transportClose');
50
- const kTransportOn = Symbol('h2lite.transportOn');
65
+ const kTransportClose = Symbol('h2lean.transportClose');
66
+ const kTransportOn = Symbol('h2lean.transportOn');
51
67
 
52
68
  function notImplemented(what) {
53
- const err = new Error(`h2lite v1 does not implement ${what}`);
54
- err.code = 'ERR_H2LITE_NOT_IMPLEMENTED';
69
+ const err = new Error(`h2lean v1 does not implement ${what}`);
70
+ err.code = 'ERR_H2LEAN_NOT_IMPLEMENTED';
55
71
  return err;
56
72
  }
57
73
 
@@ -76,6 +92,21 @@ function initHttp2Server(server, options) {
76
92
  };
77
93
  server._sessions = new Set();
78
94
  server[kCompat] = null;
95
+ // node validates this as uint32 and defaults it to 10000 (initializeOptions, core.js);
96
+ // it bounds how long an unknown-protocol socket may sit after the 403 text.
97
+ if (opts.unknownProtocolTimeout !== undefined &&
98
+ (!Number.isInteger(opts.unknownProtocolTimeout) || opts.unknownProtocolTimeout < 0)) {
99
+ const err = new RangeError(`Invalid unknownProtocolTimeout: ${opts.unknownProtocolTimeout}`);
100
+ err.code = 'ERR_OUT_OF_RANGE';
101
+ throw err;
102
+ }
103
+ server._sessionOptions.unknownProtocolTimeout = opts.unknownProtocolTimeout ?? 10000;
104
+ // allowHTTP1 has nothing to negotiate over a plain socket (no ALPN, so the branch
105
+ // that would consume it is unreachable -- the same reason node's cleartext server
106
+ // ignores it), and http1Options is accepted here only so that the option bag a
107
+ // caller builds for node keeps working verbatim. The live HTTP/1 machinery is
108
+ // built, when allowHTTP1 is set, in initHttp1Fallback below.
109
+ server._http1 = null;
79
110
  }
80
111
 
81
112
  // Turns an accepted socket -- plain or already decrypted -- into a session.
@@ -132,7 +163,13 @@ function applyHttp2ServerApi(proto) {
132
163
  // followed by closeAllSessions(this) (internal/http2/core): the listening socket goes away
133
164
  // right away and its callback is net.Server's own. Waiting for the session set to empty first
134
165
  // was ours, and it made close(cb) a promise that a session which cannot drain never keeps --
135
- // 'close' then depended on every connection finishing.
166
+ // 'close' then depended on every connection finishing. When the HTTP/1 fallback is live,
167
+ // node also runs httpServerPreClose() (closeIdleConnections + the checker interval); the
168
+ // public-API equivalent here shuts the idle keep-alive sockets down and lets the active
169
+ // responses finish, see closeHttp1().
170
+ if (this._http1) {
171
+ closeHttp1(this);
172
+ }
136
173
  proto[kTransportClose].call(this, cb);
137
174
  for (const session of [...this._sessions]) {
138
175
  session.close();
@@ -204,28 +241,145 @@ class Http2Server extends net.Server {
204
241
  }
205
242
  }
206
243
 
207
- // node's h2s server insists on h2 and overwrites whatever ALPNProtocols the
208
- // application passed: a peer offering only http/1.1 is refused by the handshake
209
- // itself, with the TLS alert no_application_protocol, rather than after it. The
210
- // list is therefore fixed here too, and the ALPN check in secureConnection below
211
- // stays as the guard for contexts installed later through setSecureContext().
244
+ // node's h2s server fixes the ALPN list the way initializeTLSOptions (core.js) does:
245
+ // ['h2'] -- with 'http/1.1' added when allowHTTP1 asks for the fallback -- and an
246
+ // options.ALPNCallback keeps the application's list out of reach. A peer offering only
247
+ // http/1.1 without allowHTTP1 is therefore still refused by the handshake itself, with
248
+ // the TLS alert no_application_protocol, rather than after it. The ALPN check in
249
+ // secureConnection below stays as the guard for contexts installed later through
250
+ // setSecureContext().
212
251
  function alpnOptions(options) {
213
252
  const opts = { ...(options || {}) };
214
- opts.ALPNProtocols = ['h2'];
253
+ if (!opts.ALPNCallback) {
254
+ opts.ALPNProtocols = ['h2'];
255
+ if (opts.allowHTTP1 === true) {
256
+ opts.ALPNProtocols.push('http/1.1');
257
+ }
258
+ }
215
259
  return opts;
216
260
  }
217
261
 
262
+ // The events an HTTP/1 connection produces that an application registers on the h2
263
+ // server. node serves both protocols from ONE emitter (http's _connectionListener is
264
+ // bound to the Http2SecureServer itself), so a listener sees both kinds of request;
265
+ // the mirror below is what makes one 'request' handler keep working here, where the
266
+ // fallback lives on an embedded server instead.
267
+ const HTTP1_MIRROR = new Set([
268
+ 'request', 'checkContinue', 'checkExpectation', 'upgrade', 'connect', 'clientError'
269
+ ]);
270
+
271
+ // node's fallback is a mixin: _http_server's private storeHTTPOptions() writes the
272
+ // HTTP/1 options into private fields of the Http2SecureServer itself, so there is no
273
+ // second server object and no second listener list. Those symbols are not reachable
274
+ // from node:http, so the fallback here is a real http.Server that never listens and
275
+ // receives the sockets the ALPN verdict hands it. Consequences written down in the
276
+ // header comment: the mirror is one-way (removeListener does not unmirror), and the
277
+ // embedded server's own connection bookkeeping never starts because 'listening' never
278
+ // fires -- the handed-off sockets are tracked here instead.
279
+ function initHttp1Fallback(server, opts) {
280
+ const http = require('node:http');
281
+ // DEP0202 (doc/api/deprecations.md): the top-level Http1IncomingMessage and
282
+ // Http1ServerResponse names migrate into http1Options.
283
+ const http1Options = { ...(opts.http1Options || {}) };
284
+ if (opts.Http1IncomingMessage !== undefined && http1Options.IncomingMessage === undefined) {
285
+ http1Options.IncomingMessage = opts.Http1IncomingMessage;
286
+ }
287
+ if (opts.Http1ServerResponse !== undefined && http1Options.ServerResponse === undefined) {
288
+ http1Options.ServerResponse = opts.Http1ServerResponse;
289
+ }
290
+ server._http1 = new http.Server(http1Options);
291
+ server._http1Sockets = new Set();
292
+ server._http1Active = 0;
293
+ server._http1Closing = false;
294
+ server.on('newListener', (event, listener) => {
295
+ if (HTTP1_MIRROR.has(event)) {
296
+ server._http1.on(event, listener);
297
+ }
298
+ });
299
+ // Per-socket activity, so close() can tell an idle keep-alive socket (destroy it,
300
+ // the way closeIdleConnections does on node) from one with a response in flight.
301
+ server._http1.on('request', (req, res) => {
302
+ server._http1Active++;
303
+ req.socket._h2leanActive = true;
304
+ res.once('finish', () => {
305
+ server._http1Active--;
306
+ req.socket._h2leanActive = false;
307
+ if (server._http1Closing && server._http1Active === 0) {
308
+ for (const socket of server._http1Sockets) {
309
+ socket.destroy();
310
+ }
311
+ }
312
+ });
313
+ });
314
+ }
315
+
316
+ function handOffHttp1(server, socket) {
317
+ server._http1Sockets.add(socket);
318
+ socket.once('close', () => server._http1Sockets.delete(socket));
319
+ server._http1.emit('connection', socket);
320
+ }
321
+
322
+ // The public-API equivalent of node's httpServerPreClose(): the idle keep-alive
323
+ // sockets go away right away, a response in flight is allowed to finish, and when the
324
+ // last one finishes the sockets it was talking on go too. node's mixin waits through
325
+ // net.Server's own connection bookkeeping instead; that bookkeeping is exactly what
326
+ // the embedded-server model cannot reach.
327
+ function closeHttp1(server) {
328
+ server._http1Closing = true;
329
+ for (const socket of [...server._http1Sockets]) {
330
+ if (!socket._h2leanActive) {
331
+ socket.destroy();
332
+ }
333
+ }
334
+ }
335
+
336
+ function unknownProtocol(server, socket) {
337
+ if (server.emit('unknownProtocol', socket)) {
338
+ return;
339
+ }
340
+ // core.js connectionListener: with no listener, the socket is told in a format it
341
+ // might understand why it got nothing, and an unref'd timer bounds how long a peer
342
+ // that never closes can hold it.
343
+ socket.end('HTTP/1.0 403 Forbidden\r\n' +
344
+ 'Content-Type: text/plain\r\n\r\n' +
345
+ 'Missing ALPN Protocol, expected `h2` to be available.\n' +
346
+ 'If this is a HTTP request: The server was not ' +
347
+ 'configured with the `allowHTTP1` option or a ' +
348
+ 'listener for the `unknownProtocol` event.\n');
349
+ const timer = setTimeout(() => {
350
+ if (!socket.destroyed) {
351
+ socket.destroy();
352
+ }
353
+ }, server._sessionOptions.unknownProtocolTimeout);
354
+ timer.unref();
355
+ socket.once('close', () => clearTimeout(timer));
356
+ }
357
+
218
358
  class Http2SecureServer extends tls.Server {
219
359
  constructor(options) {
220
- super(alpnOptions(options));
221
- initHttp2Server(this, options);
360
+ const opts = options || {};
361
+ super(alpnOptions(opts));
362
+ initHttp2Server(this, opts);
363
+ if (opts.allowHTTP1 === true) {
364
+ initHttp1Fallback(this, opts);
365
+ }
222
366
  this.on('secureConnection', (socket) => {
223
- // node's rule: 'h2' negotiated, or no ALPN at all -- which is h2 with prior
224
- // knowledge carried over a TLS connection. Anything else is dropped without
225
- // ever becoming a session.
367
+ // node's connectionListener (core.js): 'h2' negotiated becomes a session;
368
+ // 'http/1.1' -- or no ALPN at all, which is NOT h2 prior knowledge, whatever an
369
+ // older comment here claimed -- goes to the HTTP/1 fallback when allowHTTP1 is
370
+ // set, and otherwise to 'unknownProtocol', with the 403 text when nobody
371
+ // listens. Measured on node 26 in this session: a no-ALPN client speaking
372
+ // HTTP/1.1 is served by the fallback under allowHTTP1 and receives the 403
373
+ // without it.
226
374
  const alpn = socket.alpnProtocol;
227
- if (alpn === 'h2' || alpn === false) {
375
+ if (alpn === 'h2') {
228
376
  attachSession(this, socket);
377
+ } else if (alpn === 'http/1.1' || alpn === false) {
378
+ if (this._http1) {
379
+ handOffHttp1(this, socket);
380
+ } else {
381
+ unknownProtocol(this, socket);
382
+ }
229
383
  } else {
230
384
  socket.destroy();
231
385
  }
package/lib/session.js CHANGED
@@ -84,7 +84,7 @@ const MAX_SHAPES = 128;
84
84
  // v26.12.1 with tools/probe-inbound-limits.js: a 4-field block whose names and values total 46
85
85
  // octets is REFUSED under a limit of 48 (46 + 4*32 = 174), and for a 5-field block the line
86
86
  // between served and refused falls between 87 and 93 decoded octets (250 - 5*32 = 90). The
87
- // plain sum of the field octets is what h2lite compared, and it is 128 entry octets away from
87
+ // plain sum of the field octets is what h2lean compared, and it is 128 entry octets away from
88
88
  // node's verdict for a four-field request: three of the measured blocks were served that node
89
89
  // refuses. test/inbound-limits.test.js pins both sides of the line.
90
90
  const ENTRY_OVERHEAD = 32;
@@ -139,7 +139,7 @@ const OPT_NO_END_STREAM = { endStream: false };
139
139
  // How long the teardown waits for the goodbye to reach the peer before the socket is
140
140
  // taken away anyway. The FIN is queued behind whatever we wrote, so a peer that stopped
141
141
  // reading would hold it forever -- node does not have that problem because its data sits
142
- // inside nghttp2 until the window allows it, while h2lite hands it to the socket. The
142
+ // inside nghttp2 until the window allows it, while h2lean hands it to the socket. The
143
143
  // wait itself is what makes the difference between a closed connection and a reset
144
144
  // (h2spec's http2/7), so it gets a bound of its own: one second is three orders of
145
145
  // magnitude longer than the flush takes when the peer is reading.
@@ -1146,7 +1146,7 @@ class Http2Session extends EventEmitter {
1146
1146
  // RFC 9113 5.5: "Implementations MUST discard frames that have unknown or unsupported
1147
1147
  // types." Measured on a live node server with tools/probe-inbound-limits.js: type 10
1148
1148
  // (ALTSVC, RFC 7807), type 99 and type 0xff with a 4 KiB payload all leave the
1149
- // connection up and the next request served, and h2lite answers the same. Answering a
1149
+ // connection up and the next request served, and h2lean answers the same. Answering a
1150
1150
  // fatal error here would break every extension frame a peer has not told us about.
1151
1151
  // What is NOT ignored is a frame that interrupts a field block -- 5.5 forbids those
1152
1152
  // outright, and that is the case above.
@@ -1225,7 +1225,7 @@ class Http2Session extends EventEmitter {
1225
1225
  // block fragment"), so the block ends where the padding starts. Feeding the
1226
1226
  // padding to the HPACK decoder is what a server must not do: the padding is
1227
1227
  // not part of the block, and a block that ends with eight zero octets decodes
1228
- // as a literal header with no name and no value -- which is what h2lite
1228
+ // as a literal header with no name and no value -- which is what h2lean
1229
1229
  // answered to a valid padded HEADERS with GOAWAY(COMPRESSION_ERROR), while node
1230
1230
  // served the request (measured: tools/probe-hdr-pad.js, "the RFC shape: pad
1231
1231
  // length, block, padding"; the same frame is what h2spec's generic/3.2 sends,
@@ -1329,7 +1329,7 @@ class Http2Session extends EventEmitter {
1329
1329
  }
1330
1330
  // No maxHeaderListSize test here, although this is where the octets are counted: the limit
1331
1331
  // is about the size of the header LIST, which is only known once the block is decoded, and
1332
- // h2lite applies it there (_processHeaders). Measured against node, an over-limit block
1332
+ // h2lean applies it there (_processHeaders). Measured against node, an over-limit block
1333
1333
  // arriving in one HEADERS frame gets RST_STREAM(ENHANCE_YOUR_CALM) and the connection
1334
1334
  // stays up; a check on the compressed length here made it GOAWAY(PROTOCOL_ERROR) instead,
1335
1335
  // which turns one bad request into an error for every other client on the session. The
@@ -1696,7 +1696,7 @@ class Http2Session extends EventEmitter {
1696
1696
  // closed the message with its own END_STREAM is a connection error of type STREAM_CLOSED
1697
1697
  // whichever fields the block holds -- measured on node (probe cases U3, U9), where the second
1698
1698
  // block is not a valid request at all and the verdict is still GOAWAY(STREAM_CLOSED). 5.1 reads a
1699
- // frame on a closed stream as "MAY ... treat ... as a connection error", and h2lite takes that
1699
+ // frame on a closed stream as "MAY ... treat ... as a connection error", and h2lean takes that
1700
1700
  // option for BOTH the still-present (half-closed(remote)) and the just-closed (_ended) shape: a
1701
1701
  // stream error would be the other reading of half-closed(remote), but node does not make that
1702
1702
  // distinction and h2spec's half-closed(remote) HEADERS check waits for the error, not the silence
@@ -1917,7 +1917,7 @@ class Http2Session extends EventEmitter {
1917
1917
  return undefined;
1918
1918
  }
1919
1919
  // RFC 9113 5.1 and 8.2: the peer already closed the message with its END_STREAM, so a DATA
1920
- // that follows it names a stream that has nothing left to receive. h2lite ends the connection
1920
+ // that follows it names a stream that has nothing left to receive. h2lean ends the connection
1921
1921
  // with STREAM_CLOSED -- the discretion 5.1 grants a closed stream ("MAY ... treat ... as a
1922
1922
  // connection error") and what h2spec's closed checks wait for; a silent discard is legal by
1923
1923
  // the letter but the conformance tool times out on it, and this is the just-closed id (it left
@@ -1959,7 +1959,7 @@ class Http2Session extends EventEmitter {
1959
1959
  // connection ends. RFC 9113 6.1 reads a DATA on a stream outside "open"/"half-closed(local)"
1960
1960
  // as a stream error of type STREAM_CLOSED, and h2spec's half-closed(remote) DATA and 6.1 checks
1961
1961
  // accept the reset, the GOAWAY, or a closed connection -- but node makes no present/closed
1962
- // distinction and answers the connection error below on BOTH stacks, so h2lite keeps node's
1962
+ // distinction and answers the connection error below on BOTH stacks, so h2lean keeps node's
1963
1963
  // verdict here rather than the stream error (which regressed the tool's DATA echo). Leaving it
1964
1964
  // is the honest call: it is node's deviation, the parity rule keeps it, and 5.1's closed-state
1965
1965
  // read is the discretionary one ("MAY ... treat ... as a connection error"). The arms that
@@ -2228,7 +2228,7 @@ class Http2Session extends EventEmitter {
2228
2228
 
2229
2229
  _onGoaway(buf, start, len) {
2230
2230
  // node begins this handler with `if (session.destroyed) return;`, because its nghttp2 handle
2231
- // can report a frame after the session was destroyed. h2lite cannot: _dispatch() answers with
2231
+ // can report a frame after the session was destroyed. h2lean cannot: _dispatch() answers with
2232
2232
  // `this._open` and the read batch stops at the frame that destroyed the session, so a
2233
2233
  // destroyed session has no frames left to reach. That stop is what is tested -- in
2234
2234
  // test/goaway.test.js, 'the read batch stops at the frame that destroyed the session' -- and
@@ -2306,7 +2306,7 @@ class Http2Session extends EventEmitter {
2306
2306
  // that had already closed), our flush-per-read-batch put a WINDOW_UPDATE(sid 0) on the wire in
2307
2307
  // front of the frame the tool was waiting for, where node sent nothing at all. Measured on a
2308
2308
  // 640 KiB upload (tools/probe-conn-credit.js): node releases the connection window in steps of
2309
- // 32768 octets -- half the 65535 it advertises -- and the same threshold here takes h2lite's
2309
+ // 32768 octets -- half the 65535 it advertises -- and the same threshold here takes h2lean's
2310
2310
  // cadence from one frame per read batch to the same half-window steps, at the same throughput.
2311
2311
  // A peer that keeps sending is therefore credited before its window can run out, so the
2312
2312
  // anti-deadlock rule that this counter exists for still holds; below the threshold the bytes
@@ -2483,7 +2483,7 @@ class Http2Session extends EventEmitter {
2483
2483
  // That is exactly what a peer's GOAWAY does -- destroy(new ERR_HTTP2_SESSION_ERROR(code),
2484
2484
  // NO_ERROR) -- and the measurement below is what made the distinction visible: the application
2485
2485
  // is told WHY (the code that arrived) while the peer is told goodbye with NO_ERROR, because
2486
- // this side did not fail. A number as the first argument is h2lite's own convenience, used by
2486
+ // this side did not fail. A number as the first argument is h2lean's own convenience, used by
2487
2487
  // the socket handlers and _fatal(): destroy with this GOAWAY code and nothing to report.
2488
2488
  destroy(error = NO_ERROR, code) {
2489
2489
  if (this.destroyed) {
@@ -2527,7 +2527,7 @@ class Http2Session extends EventEmitter {
2527
2527
  // node's teardown shape (internal/http2/core.js, finishSessionClose): the socket is
2528
2528
  // ended first and the destroy waits for the write to be flushed, so the GOAWAY queued
2529
2529
  // above reaches the peer and the connection ends with a FIN instead of a reset.
2530
- // h2lite used to destroy the socket outright, which threw that GOAWAY away and closed
2530
+ // h2lean used to destroy the socket outright, which threw that GOAWAY away and closed
2531
2531
  // the connection with a reset whenever the write had not reached the OS yet -- under
2532
2532
  // load that is what h2spec's http2/7 reads as a failure ("connection reset by peer"
2533
2533
  // where node's "connection closed" passes), measured in bench/micro/probe-goaway-ping.js.
package/lib/stream.js CHANGED
@@ -20,7 +20,7 @@
20
20
  // the same promise node:http2 makes.
21
21
  // * a response the application never started is started for it: the first write()
22
22
  // sends HEADERS{:status 200} before the DATA, as node does. An end() with no body at
23
- // all is the one shape where h2lite answers (200, empty DATA/ES) and node says nothing
23
+ // all is the one shape where h2lean answers (200, empty DATA/ES) and node says nothing
24
24
  // -- measured, and recorded in AGENTS.md section 10.
25
25
  'use strict';
26
26
 
@@ -177,10 +177,10 @@ class Http2Stream extends EventEmitter {
177
177
  cb = encoding; encoding = null;
178
178
  }
179
179
  if (!this.headersSent) {
180
- // A body takes the auto-respond of write() above; an empty end() is where h2lite and node
180
+ // A body takes the auto-respond of write() above; an empty end() is where h2lean and node
181
181
  // part company, and the split is written down in AGENTS.md section 10: node puts NOTHING on
182
182
  // the wire for `end()` before respond() -- no HEADERS, no DATA, no END_STREAM, and
183
- // headersSent stays false -- while h2lite answers a plain 200 rather than leave a stream
183
+ // headersSent stays false -- while h2lean answers a plain 200 rather than leave a stream
184
184
  // that no peer will ever see an answer on.
185
185
  this.respond({ ':status': 200 });
186
186
  }
@@ -189,7 +189,7 @@ class Http2Stream extends EventEmitter {
189
189
  // A stream that still owes a trailer block must not close the stream with its body.
190
190
  // RFC 9113 6.1 puts a trailing HEADERS before END_STREAM, and node's frame sequence
191
191
  // shows the placement: HEADERS/EH DATA HEADERS/ES/EH for a body, and no DATA at all
192
- // for an empty one -- where h2lite used to send DATA/ES first and then a trailer block
192
+ // for an empty one -- where h2lean used to send DATA/ES first and then a trailer block
193
193
  // that no peer could still read. The application-level half-close happens here (that
194
194
  // is what emits 'wantTrailers'), the wire-level one happens in sendTrailers().
195
195
  const oweTrailers = this._waitForTrailers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h2lean",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Pure-JavaScript HTTP/2 for Node.js — a drop-in node:http2 replacement (server and client, h2c and TLS) with zero dependencies",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/MakeNodeGreatAgain/h2lite.git"
12
+ "url": "git+https://github.com/MakeNodeGreatAgain/h2lean.git"
13
13
  },
14
14
  "keywords": [
15
15
  "http2",
@@ -45,6 +45,7 @@
45
45
  "devDependencies": {
46
46
  "@eslint/js": "^10.0.1",
47
47
  "eslint": "^10.14.0",
48
+ "fastify": "^5.12.8",
48
49
  "fast-xml-parser": "^5.12.0",
49
50
  "uWebSockets.js": "github:uNetworking/UWebSockets.js#v20.70.0"
50
51
  }