Skip to content

Fix http2 route flag silently dropped in toJsRoute()#26

Open
kriszyp wants to merge 1 commit into
mainfrom
kris/http2-tojsroute
Open

Fix http2 route flag silently dropped in toJsRoute()#26
kriszyp wants to merge 1 commit into
mainfrom
kris/http2-tojsroute

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 10, 2026

Copy link
Copy Markdown
Member

Problem

toJsRoute() in ts/proxy.ts builds the napi JsRouteConfig field-by-field, but never copied http2. So http2: true on a RouteConfig never reached the Rust side — the object handed to napi simply had no http2 key, and r.http2.unwrap_or(false) in proxy.rs defaulted every route to false. The result: no route ever advertised h2 in its TLS ALPN extension, regardless of config, and the feature was silently inert.

Both config entry points (the constructor and updateConfig) share this mapper, so both were affected.

The rest of the pipeline was already correctly wired — the napi JsRouteConfig struct, config parse (proxy.rs:519), the ALPN builder (tls.rs:106), and the terminateTls-required warning. Only the JS→napi hop dropped it.

Fix

One-line passthrough in toJsRoute():

 		sourceAddressHeader: r.sourceAddressHeader,
+		http2: r.http2,

Tests

Added end-to-end ALPN regression coverage in proxy.spec.ts (+ a tlsAlpn helper): a client offering ['h2','http/1.1'] negotiates

  • h2 when the route sets http2: true
  • nothing (default, ALPN empty) when http2 is unset
  • h2 after enabling it via updateConfig

Before the fix, the first case negotiates '' and fails. All 8 tests in proxy.spec.ts pass locally against a debug build.

🤖 Generated with Claude Code

toJsRoute() built the JsRouteConfig field-by-field but never copied
`http2`, so `http2: true` on a route never reached Rust. Every route
defaulted to http2=false (no `h2` in the TLS ALPN advertisement),
regardless of config. Both config paths (constructor and updateConfig)
share this mapper, so both were affected.

Add the missing passthrough, plus end-to-end ALPN regression tests that
assert `h2` is negotiated only when http2: true, including via
updateConfig.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request fixes a bug where the http2 option was dropped in toJsRoute and never reached the Rust side, preventing HTTP/2 from being advertised in ALPN. It introduces a regression test suite and a tlsAlpn helper function to verify ALPN negotiation. The review feedback correctly identifies a resource leak in the tlsAlpn helper where the setTimeout timer is not cleared upon connection success or failure, and provides a code suggestion to properly manage the timer and destroy the socket on timeout.

Comment thread __test__/util.ts
Comment on lines +240 to +251
return new Promise((resolve, reject) => {
const { port, host = '127.0.0.1', servername, caCert, alpnProtocols, rejectUnauthorized = false } = opts;
const socket = tls.connect(
{ port, host, servername, ca: caCert, ALPNProtocols: alpnProtocols, rejectUnauthorized },
() => {
resolve(socket.alpnProtocol || '');
socket.end();
},
);
socket.on('error', reject);
setTimeout(() => reject(new Error('tlsAlpn timeout')), 5000);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The setTimeout timer is never cleared when the connection succeeds or fails. This can lead to resource leaks, keeping the Node.js event loop active and delaying process exit, or potentially causing unexpected behavior in tests. Additionally, if a timeout occurs, the socket is not destroyed, which could leave a hanging connection.

We should store the timer reference, clear it when the promise resolves or rejects, and destroy the socket on timeout.

	return new Promise((resolve, reject) => {
		const { port, host = '127.0.0.1', servername, caCert, alpnProtocols, rejectUnauthorized = false } = opts;
		let timer: any;
		const socket = tls.connect(
			{ port, host, servername, ca: caCert, ALPNProtocols: alpnProtocols, rejectUnauthorized },
			() => {
				clearTimeout(timer);
				resolve(socket.alpnProtocol || '');
				socket.end();
			},
		);
		timer = setTimeout(() => {
			socket.destroy();
			reject(new Error('tlsAlpn timeout'));
		}, 5000);
		socket.on('error', (err) => {
			clearTimeout(timer);
			reject(err);
		});
	});

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant