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

fix(H2 Client): bind stream 'data' listener only after received 'response' event #2985

Merged
merged 6 commits into from
Mar 25, 2024
4 changes: 3 additions & 1 deletion lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ function writeH2 (client, request) {

/** @type {import('node:http2').ClientHttp2Stream} */
let stream
let isStreamResponseStarted = false

const { hostname, port } = client[kUrl]

Expand Down Expand Up @@ -388,6 +389,7 @@ function writeH2 (client, request) {
++session[kOpenStreams]

stream.once('response', headers => {
isStreamResponseStarted = true
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
request.onResponseStarted()

Expand Down Expand Up @@ -419,7 +421,7 @@ function writeH2 (client, request) {
})

stream.on('data', (chunk) => {
if (request.onData(chunk) === false) {
if (!isStreamResponseStarted || request.onData(chunk) === false) {
Copy link
Member

Choose a reason for hiding this comment

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

this will throw away the first chunk?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ronag thaks, good point, I was focused on seeing that the error no longer occurs.

Is it ok to declare a temp chunks array and if isStreamResponseStarted is false in 'data' event push the chunk and then in 'response' event if the temp chunks array has length > 0 call the request.onData method after the request.onHeaders?

Copy link
Member

Choose a reason for hiding this comment

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

This does kind of feel like an error in node core http, it doesn't make sense to me that data is emitted before response. Do you think you could report it in the node core repo in addition to this workaround PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ronag before report,
i was checking nodejs documentation about http2

there's one example here: https://nodejs.org/api/http2.html#clienthttp2sessionrequestheaders-options

const http2 = require('node:http2');
const clientSession = http2.connect('https://localhost:1234');
const {
  HTTP2_HEADER_PATH,
  HTTP2_HEADER_STATUS,
} = http2.constants;

const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' });
req.on('response', (headers) => {
  console.log(headers[HTTP2_HEADER_STATUS]);
  req.on('data', (chunk) => { /* .. */ });
  req.on('end', () => { /* .. */ });
});

the data event listener is binded inside the response listener callback.
So i have moved client-h2.js stream on data event inside stream on response (at the and of the callback) and it seems to be ok now

 stream.once('response', headers => {
    const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
    request.onResponseStarted()

    if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
      stream.pause()
    }

    stream.on('data', (chunk) => {
      if (request.onData(chunk) === false) {
        stream.pause()
      }
    })
  })

my test script is:

const Agent = require("./lib/dispatcher/agent");
const { fetch } = require("./lib/web/fetch");

const dispatcher = new Agent({ allowH2: true });

(async () => {
  while (true) {
    //console.log("fetching 1000 requests");
    //console.time();
    await Promise.all(
      Array.from({ length: 1000 }).map(async () => {
        try {
          const response = await fetch("https://http2.pro/api/v1", {
            dispatcher,
          });
          const text = await response.text();
          //console.log(text);
          if(!text) throw new Error("NOT GOOD")
        } catch (e) {
          console.error(e);
        }
      }),
    );
    //console.timeEnd();
  }
})();

This make more sense to you?
Thanks

Copy link
Member

Choose a reason for hiding this comment

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

Make sense, but yea, please open an issue because this seems wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Issue opened

stream.pause()
}
})
Expand Down