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

http2: connection hangs forever ♾️ when blob stream is piped 🚰 through client to server #48685

Closed
bricss opened this issue Jul 6, 2023 · 1 comment

Comments

@bricss
Copy link

bricss commented Jul 6, 2023

Version

v20.4.0

Platform

Microsoft Windows NT 10.0.22621.0 x64

Subsystem

No response

What steps will reproduce the bug?

Run following scripts:

  1. Generate cert & key 🔐
openssl req -days 365 -keyout localhost.key -newkey ec -nodes -pkeyopt ec_paramgen_curve:prime256v1 -subj //SKIP=1/CN=localhost -out localhost.cert -x509

  1. Save repro.mjs in the same directory as the cert and key, run node repro.mjs
import { once } from 'node:events';
import { readFileSync } from 'node:fs';
import http2 from 'node:http2';
import { Readable } from 'node:stream';

const {
  HTTP2_HEADER_AUTHORITY,
  HTTP2_HEADER_METHOD,
  HTTP2_HEADER_PATH,
  HTTP2_HEADER_SCHEME,
  HTTP2_METHOD_POST,
} = http2.constants;

const baseH2URL = new URL('https://localhost:3443');

const cwd = process.cwd();
const cert = readFileSync(`${ cwd }/localhost.cert`);
const key = readFileSync(`${ cwd }/localhost.key`);

const h2server = http2.createSecureServer({ cert, key }, (req, res) => {
  console.log(req.url);
  req.pipe(res);
});

await once(h2server.listen(baseH2URL.port), 'listening');

console.log('h2::server listening on', h2server.address());

const client = http2.connect(baseH2URL, { rejectUnauthorized: false });

client.on('error', (err) => console.error(err));

const req = client.request({
  [HTTP2_HEADER_AUTHORITY]: baseH2URL.host,
  [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST,
  [HTTP2_HEADER_PATH]: `${ baseH2URL.pathname }${ baseH2URL.search }`,
  [HTTP2_HEADER_SCHEME]: baseH2URL.protocol.replace(/\p{Punctuation}/gu, ''),
});

req.on('end', () => console.log('response::ended')); // never get called <---|
req.on('error', (err) => console.error(err));
req.on('data', (chunk) => console.info(chunk));
req.on('response', async (headers) => {
  console.log(`response::status ${ headers[':status'] }`);
  const body = [];

  for await (const chunk of req) {
    console.info('before::chunk');
    body.push(chunk);
    console.log(chunk);
    console.info('after::chunk');
  }

  console.log(Buffer.concat(body).toString()); // not reachable <---|
});

const body = new Blob(['bits']);

Readable.from(body.stream()).pipe(req);
// Readable.fromWeb(body.stream()).pipe(req); // issue persists with both methods <---|

console.log('h2::brrrr!');

  1. Check logs 🪵 and get surprised 😯

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

Always reproduces 😮‍💨

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

The connection should close 📪 after the data transfer is complete 🏁

What do you see instead?

Connection hangs for an infinite ♾️ amount of time

Additional information

The issue only affects the node:http2 module, and does not appear in the node:http module
Initially identified in -> rekwest package 📦 tests 🧪

@bricss
Copy link
Author

bricss commented Jul 20, 2023

Solution to resolve the issue has been found 🔍 in shape 🗿 and form 💎 of async generator wrapper:

import { once } from 'node:events';
import { readFileSync } from 'node:fs';
import http2 from 'node:http2';
import { Readable } from 'node:stream';

const {
  HTTP2_HEADER_AUTHORITY,
  HTTP2_HEADER_METHOD,
  HTTP2_HEADER_PATH,
  HTTP2_HEADER_SCHEME,
  HTTP2_METHOD_POST,
} = http2.constants;

const baseH2URL = new URL('https://localhost:3443');

const cwd = process.cwd();
const cert = readFileSync(`${ cwd }/localhost.cert`);
const key = readFileSync(`${ cwd }/localhost.key`);

const h2server = http2.createSecureServer({ cert, key }, (req, res) => {
  console.log(req.url);
  req.pipe(res);
});

await once(h2server.listen(baseH2URL.port), 'listening');

console.log('h2::server listening on', h2server.address());

const client = http2.connect(baseH2URL, { rejectUnauthorized: false });

client.on('error', (err) => console.error(err));

const req = client.request({
  [HTTP2_HEADER_AUTHORITY]: baseH2URL.host,
  [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST,
  [HTTP2_HEADER_PATH]: `${ baseH2URL.pathname }${ baseH2URL.search }`,
  [HTTP2_HEADER_SCHEME]: baseH2URL.protocol.replace(/\p{Punctuation}/gu, ''),
});

req.on('end', () => console.log('response::ended')); // get called <---|
req.on('error', (err) => console.error(err));
req.on('data', (chunk) => console.info(chunk));
req.on('response', async (headers) => {
  console.log(`response::status ${ headers[':status'] }`);
  const body = [];

  for await (const chunk of req) {
    console.info('before::chunk');
    body.push(chunk);
    console.log(chunk);
    console.info('after::chunk');
  }

  console.log(Buffer.concat(body).toString()); // reachable <---|
});

const body = new Blob(['bits']);

async function* tap(value) {
  yield* value.stream();
}

Readable.from(tap(body)).pipe(req);
// Readable.fromWeb(tap(body)).pipe(req); // works with both methods <---|

console.log('h2::brrrr!');

@bricss bricss closed this as completed Jul 20, 2023
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

No branches or pull requests

1 participant