Skip to content

http2: allow client request header injection via diagnostics_channel - #67069

Open
bengl wants to merge 1 commit into
nodejs:mainfrom
bengl:bengl/h2-dc-request-headers
Open

bengl wants to merge 1 commit into
nodejs:mainfrom
bengl:bengl/h2-dc-request-headers

Conversation

@bengl

@bengl bengl commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds an http2.client.request.headers diagnostics channel that lets subscribers add or modify outgoing HTTP/2 client request headers before they are serialized. This gives tracing instrumentation (OpenTelemetry and friends) a supported way to propagate context headers like traceparent on node:http2 client requests, which today requires monkey-patching ClientHttp2Session#request().

Why a new channel instead of changing the existing ones

http2.client.stream.created and http2.client.stream.start cannot serve as the injection point:

  • request() serializes the headers (prepareRequestHeadersObject/prepareRequestHeadersArray call buildNgHeaderString) before the created publish fires, and on an already-connected session requestOnConnect submits the HEADERS frame before the publish too. Mutating the published sentHeaders object has no effect on the wire.
  • Publishing created earlier with the live headers object would change what existing subscribers see. They currently get the post-defaults snapshot, including the :method, :scheme, :authority, and :path defaults. It would also require moving header validation after stream creation, which changes error timing. Existing subscribers that treat the payload as read-only must not observe a change, so those channels are left alone.

The new channel is published at the top of ClientHttp2Session#request(), before header validation and serialization, on both the connecting and already-connected session paths. Subscribers get { headers, session }, where headers is a shallow copy of the object or array the caller passed to request() (with sensitiveHeaders preserved), or a newly created object when no headers argument was given. Headers must be mutated in place for the change to take effect. Injected headers go through the same validation as headers passed directly to request(), so an injected unknown pseudo-header still throws synchronously.

Behavior notes

  • created and start payloads, timing, and contents are unchanged. Mutating their published headers still has no effect. The new channel is the intended injection point.
  • Subscribers mutate a shallow copy of the caller's headers, so the caller's object, including its sensitiveHeaders list, is never modified. Header reuse across requests is unaffected.
  • When there are no subscribers the channel is not published at all, so there is no overhead and no behavior change.

Tests

test/parallel/test-diagnostics-channel-http2-client-request-headers.js covers:

  • injected header reaches the server when request() is called while the session is still connecting
  • the caller's headers object and array (including sensitiveHeaders) are not modified by the subscriber, in both object and raw array form
  • injected header reaches the server on an already-connected session
  • injection when request() is called with no headers argument
  • injection into raw array headers
  • a subscriber that does not mutate leaves the request unchanged
  • injected headers hit the same validation as caller-provided headers

Also ran the full test-http2 and test-diagnostics-channel parallel suites locally (364 tests, all passing), plus make lint-js and make lint-md on the changed files.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/http2
  • @nodejs/net

@nodejs-github-bot nodejs-github-bot added http2 Issues and PRs related to the http2 subsystem. needs-ci PRs that need a full CI run. labels Oct 27, 2026
@bengl
bengl marked this pull request as ready for review October 27, 2026 20:52
@bengl
bengl force-pushed the bengl/h2-dc-request-headers branch from e478a71 to 8e53cea Compare October 27, 2026 20:52
@bengl
bengl requested review from Qard and pimterry October 29, 2026 20:04
@bengl bengl added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 30, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 30, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

if (onClientRequestHeadersChannel.hasSubscribers) {
if (headers === undefined)
headers = { __proto__: null };
onClientRequestHeadersChannel.publish({ headers, session: this });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exposes the caller's original headers param. If you mutate that, you'll unexpectedly mutate values in user code, not just this request. E.g. if a user builds headers then passes them into multiple requests or anywhere else, they'll be passing around the mutated values between separate requests unexpectedly. If the app is proxying and passes incoming headers to upstream requests, you've modified their incoming header data.

If you clone headers here instead you can avoid that. Would need to handle both arrays & object forms and preserve sensitiveHeaders in both cases.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Fixed in latest rev.


> Stability: 1 - Experimental

##### Event: `'http2.client.request.headers'`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not http2.client.stream.headers, which would match all the other methods here? No strong feelings here myself, just a bit surprising.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The channel fires before the stream exists and the payload has no stream, so http2.client.stream.* felt wrong for it. It also lines up with the http.client.request family on the http/1 side. Happy to rename if you'd rather.

@bengl
bengl force-pushed the bengl/h2-dc-request-headers branch from f41b4ec to 8e53cea Compare October 30, 2026 17:09
The http2.client.stream.created and http2.client.stream.start channels
publish the stream's sentHeaders object, but by that point the request
headers have already been serialized in ClientHttp2Session#request().
Mutating the published object has no effect on the wire, so tracing
instrumentation has no way to add context headers such as traceparent to
outgoing HTTP/2 client requests.

Add a new http2.client.request.headers channel, published when
ClientHttp2Session#request() is called, before the headers are validated
and serialized. Subscribers receive a shallow copy of the headers object
passed by the caller and can mutate it in place to add or modify request
headers. When request() is called without headers, a fresh object is
published so subscribers can still inject headers. The caller's own
headers object and its sensitiveHeaders list are never modified, so
header objects can be safely reused or forwarded across requests.
Injected headers go through the same validation as headers passed
directly to request().

The existing created and start channels are unchanged.

Assisted-by: pi
Signed-off-by: Bryan English <bryan@bryanenglish.com>
@bengl
bengl force-pushed the bengl/h2-dc-request-headers branch from f41b4ec to fa8ffdb Compare October 30, 2026 17:38
@codecov

codecov Bot commented Oct 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.70%. Comparing base (75191c4) to head (fa8ffdb).
⚠️ Report is 53 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #67069      +/-   ##
==========================================
- Coverage   92.97%   90.70%   -2.27%     
==========================================
  Files         428      797     +369     
  Lines      195284   278797   +83513     
  Branches    30205    53619   +23414     
==========================================
+ Hits       181560   252881   +71321     
- Misses      13397    16284    +2887     
- Partials      327     9632    +9305     
Files with missing lines Coverage Δ
lib/internal/http2/core.js 94.95% <100.00%> (+0.12%) ⬆️

... and 504 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

http2 Issues and PRs related to the http2 subsystem. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants