Repository: nodejs/node
Pull request: http2: set `Http2Stream#sentHeaders` for raw headers

When https://github.com/nodejs/node/pull/57917 added support for sending raw header arrays, `Http2Stream#sentHeaders` was set only for header objects. This change also sets it for raw headers by lazily instantiating the property to avoid any performance impact on the fast path.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/http2/core.js
patch: @@ -254,6 +254,7 @@ const kProceed = Symbol('proceed');
 const kRemoteSettings = Symbol('remote-settings');
 const kRequestAsyncResource = Symbol('requestAsyncResource');
 const kSentHeaders = Symbol('sent-headers');
+const kRawHeaders = Symbol('raw-headers');
 const kSentTrailers = Symbol('sent-trailers');
 const kServer = Symbol('server');
 const kState = Symbol('state');
@@ -1807,12 +1808,14 @@ class ClientHttp2Session extends Http2Session {
 
     let headersList;
     let headersObject;
+    let rawHeaders;
     let scheme;
     let authority;
     let method;
 
     if (ArrayIsArray(headersParam)) {
       ({
+        rawHeaders,
         headersList,
         scheme,
         authority,
@@ -1859,6 +1862,7 @@ class ClientHttp2Session extends Http2Session {
     // eslint-disable-next-line no-use-before-define
     const stream = new ClientHttp2Stream(this, undefined, undefined, {});
     stream[kSentHeaders] = headersObject; // N.b. Only set for object headers, not raw headers
+    stream[kRawHeaders] = rawHeaders; // N.b. Only set for raw headers, not object headers
     stream[kOrigin] = `${scheme}://${authority}`;
     const reqAsync = new AsyncResource('PendingRequest');
     stream[kRequestAsyncResource] = reqAsync;
@@ -2128,6 +2132,33 @@ class Http2Stream extends Duplex {
   }
 
   get sentHeaders() {
+    if (this[kSentHeaders] || !this[kRawHeaders]) {
+      return this[kSentHeaders];
+    }
+
+    const rawHeaders = this[kRawHeaders];
+    const headersObject = { __proto__: null };
+
+    for (let i = 0; i < rawHeaders.length; i += 2) {
+      const key = rawHeaders[i];
+      const value = rawHeaders[i + 1];
+
+      const existing = headersObject[key];
+      if (existing === undefined) {
+        headersObject[key] = value;
+      } else if (ArrayIsArray(existing)) {
+        existing.push(value);
+      } else {
+        headersObject[key] = [existing, value];
+      }
+    }
+
+    if (rawHeaders[kSensitiveHeaders] !== undefined) {
+      headersObject[kSensitiveHeaders] = rawHeaders[kSensitiveHeaders];
+    }
+
+    this[kSentHeaders] = headersObject;
+
     return this[kSentHeaders];
   }
 

filename: lib/internal/http2/util.js
patch: @@ -678,15 +678,23 @@ function prepareRequestHeadersArray(headers, session) {
       throw new ERR_HTTP2_CONNECT_PATH();
   }
 
-  const headersList = buildNgHeaderString(
+  const rawHeaders =
     additionalPsuedoHeaders.length ?
       additionalPsuedoHeaders.concat(headers) :
-      headers,
+      headers;
+
+  if (headers[kSensitiveHeaders] !== undefined) {
+    rawHeaders[kSensitiveHeaders] = headers[kSensitiveHeaders];
+  }
+
+  const headersList = buildNgHeaderString(
+    rawHeaders,
     assertValidPseudoHeader,
     headers[kSensitiveHeaders],
   );
 
   return {
+    rawHeaders,
     headersList,
     scheme,
     authority: authority ?? headers[HTTP2_HEADER_HOST],

filename: test/parallel/test-http2-raw-headers.js
patch: @@ -39,6 +39,16 @@ const http2 = require('http2');
       'a', 'c',
     ]).end();
 
+    assert.deepStrictEqual(req.sentHeaders, {
+      '__proto__': null,
+      ':path': '/foobar',
+      ':scheme': 'http',
+      ':authority': `localhost:${server.address().port}`,
+      ':method': 'GET',
+      'a': [ 'b', 'c' ],
+      'x-FOO': 'bar',
+    });
+
     req.on('response', common.mustCall((headers) => {
       assert.strictEqual(headers[':status'], 200);
       client.close();

filename: test/parallel/test-http2-sensitive-headers.js
patch: @@ -72,6 +72,16 @@ const { duplexPair } = require('stream');
 
   const req = client.request(rawHeaders);
 
+  assert.deepStrictEqual(req.sentHeaders, {
+    '__proto__': null,
+    ':method': 'GET',
+    ':authority': 'localhost:80',
+    ':scheme': 'http',
+    ':path': '/',
+    'secret': 'secret-value',
+    [http2.sensitiveHeaders]: [ 'secret' ],
+  });
+
   req.on('response', common.mustCall((headers) => {
     assert.strictEqual(headers[':status'], 200);
   }));