Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot CONNECT http2-over-http2 in Node 20.12.0 #52344

Closed
pimterry opened this issue Apr 3, 2024 · 0 comments · Fixed by #52368
Closed

Cannot CONNECT http2-over-http2 in Node 20.12.0 #52344

pimterry opened this issue Apr 3, 2024 · 0 comments · Fixed by #52368
Labels
http2 Issues or PRs related to the http2 subsystem.

Comments

@pimterry
Copy link
Member

pimterry commented Apr 3, 2024

Version

v20.12.0

Platform

Linux 6.5.0-21-generic #21-Ubuntu SMP PREEMPT_DYNAMIC Wed Feb 7 14:17:40 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

Subsystem

http2

What steps will reproduce the bug?

Test-suite ready demo:

'use strict';

const common = require('../common');
const assert = require('assert');
const h2 = require('http2');

const server = h2.createServer();

server.listen(0, common.mustCall(function() {
  const proxyClient = h2.connect(`http://localhost:${server.address().port}`);

  const request = proxyClient.request({
    ':method': 'CONNECT',
    ':authority': 'example.com:80'
  });

  request.on('response', common.mustCall((connectResponse) => {
    assert.strictEqual(connectResponse[':status'], 200);

    const proxiedClient = h2.connect('http://example.com', {
      createConnection: () => request // Tunnel via first request stream
    });

    const proxiedRequest = proxiedClient.request();
    proxiedRequest.on('response', common.mustCall((proxiedResponse) => {
      assert.strictEqual(proxiedResponse[':status'], 204);

      proxiedClient.close();
      proxyClient.close();
      server.close();
    }));
  }));
}));

server.once('connect', common.mustCall((req, res) => {
  assert.strictEqual(req.headers[':method'], 'CONNECT');
  res.writeHead(200); // Accept the CONNECT tunnel

  // Handle this stream as a new 'proxied' connection (pretend to forward
  // but actually just unwrap the tunnel ourselves):
  server.emit('connection', res.stream);
}));

// Handle the 'proxied' request itself:
server.once('request', common.mustCall((req, res) => {
  res.writeHead(204);
  res.end();
}));

How often does it reproduce? Is there a required condition?

Fails every time in Node v20.12.

Works every time in Node v20.11 and previous releases (for a few years, at least)

What is the expected behavior? Why is that the expected behavior?

It should be possible to make an HTTP/2 CONNECT request (opening a tunnel to a target server through an HTTP/2 proxy) and then make another HTTP/2 request to the target server.

What do you see instead?

Instead, creating an HTTP/2 connection on top of an HTTP/2 tunnel fails with:

Error [ERR_HTTP2_SOCKET_BOUND]: The socket is already bound to an Http2Session
    at new Http2Session (node:internal/http2/core:1238:13)
    at new ServerHttp2Session (node:internal/http2/core:1640:5)
    at Http2Server.connectionListener (node:internal/http2/core:3102:19)
    at Http2Server.emit (node:events:518:28)
    at Http2Server.<anonymous> (/home/tim/Dropbox/programming/javascript/node/test/parallel/test-http2-client-proxy-over-http2.js:41:10)
    at Http2Server.<anonymous> (/home/tim/Dropbox/programming/javascript/node/test/common/index.js:473:15)
    at Object.onceWrapper (node:events:633:26)
    at Http2Server.emit (node:events:518:28)
    at Http2Server.onServerStream (node:internal/http2/compat:956:17)
    at Http2Server.emit (node:events:518:28) {
  code: 'ERR_HTTP2_SOCKET_BOUND'
}

Additional information

I'm fairly sure I know the issue here: kSession is used in the HTTP/2 code as a single symbol with two meanings:

  • On sockets, marking them as being owned by an HTTP/2 session (i.e. there is a session currently running within this socket)
  • On HTTP/2 streams, providing access to the HTTP/2 session that contains them (i.e. this stream is running within that session)

In this case, these two meanings mix: the CONNECT stream should be within an HTTP/2 session (the initial connection) and should act as a socket and contain an HTTP/2 session. That conflict is why this blows up.

This didn't happen in the past because the inner stream uses a JSStreamSocket to handle this, and kSession wasn't passed through that wrapper, so the JSS and the inner H2 stream had independent kSession values. That changed in c50524a#diff-8dd7127678f00bf090a030256bad7ae645834f7100e54154b4fc81c8cbe9c3fc, which (reasonably I think) exposed the 'real' kSession through that wrapper.

I think the solution here is two split these two use cases, and track the 'socket-in-use-by' and 'stream-powered-by' state under separate keys, so a single H2 stream can have both. I'm going to keep digging and hopefully open a PR for this within a day or two, but if anybody has any context on that in the meantime and whether this is going to work or whether there's a good reason the two are the same, that would be very interesting.

@VoltrexKeyva VoltrexKeyva added the http2 Issues or PRs related to the http2 subsystem. label Apr 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
http2 Issues or PRs related to the http2 subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants