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

cloneIncomingMessage does not handle headers field in Node.js v16 #212

Closed
hrsh7th opened this issue Feb 25, 2022 · 12 comments · Fixed by #214
Closed

cloneIncomingMessage does not handle headers field in Node.js v16 #212

hrsh7th opened this issue Feb 25, 2022 · 12 comments · Fixed by #214

Comments

@hrsh7th
Copy link
Contributor

hrsh7th commented Feb 25, 2022

I met the problem described in the title. I've investigated it and found the cause.
The src/interceptors/ClientRequest/utils/cloneIncomingMessage is not properly.
But I don't know how to fix it properly. I'm planning to consider it but first I share it via this issue.

--- Edit

I removed the meaningless tests.

@hrsh7th
Copy link
Contributor Author

hrsh7th commented Feb 25, 2022

Sorry. The proposed test improvements don't seem to make sense.

It seems to pass on Node.js v12 but not on the Node.js v16 or higher.

@trevoro
Copy link

trevoro commented Feb 26, 2022

The issue here seems to be related to the fact that cloneIncomingMessage doesn't really return a ClonedIncomingMessage, it returns a PassThrough. But a PassThrough doesn't have a headers method. Forcing the type signature to unknown -> ClonedIncomingMessage seems to make the other methods that call this method also behave incorrectly. I'm still unclear what the best way is to fix it, but the issue seems to be in src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts

@hrsh7th
Copy link
Contributor Author

hrsh7th commented Feb 26, 2022

The test seems to expect to clone the headers feild.

@kettanaito
Copy link
Member

Hey, folks. Thanks for elaborating on this.

I'll get to reproduce this in Node 16 when I have a minute.

Overall, the incoming message cloning is perhaps the most unstable part of this library. I couldn't find any good resources on how to do that, and the only approach I found was to split IncomingMessage into two PassThrough, which is precisely how it works at the moment.

Cloning headers may look like a viable solution for now, but I think it'd be much better to find a reliable way to clone IncomingMessage without having to manually add headers and other data potentially missing from PassThrough. I'd highly appreciate any input on this!

Also, the only reason we need to clone the incoming message is to be able to read it on the library's side and don't block the reading on the consumer's side. If we can't find a reliable way to clone incoming message, the last resort would be to drop the response body from the response event in the library, so that developers could read the body if they want by IncomingMessage reference (which would block any upstream reading still).

@hrsh7th
Copy link
Contributor Author

hrsh7th commented Feb 26, 2022

The IncomingMessage seems to use getter/setter with its own prototype.

https://github.com/nodejs/node/blob/master/lib/_http_incoming.js#L107

So currently implementation doesn't clone headers field because the cloneIncomingMessage clones only the instance properties.

I try to clone the IncomingMessage.prototype in my local environment and passes the failing tests.

However, it is a bit scary code.

  const stream = message.pipe(new PassThrough())

  const proto = {}
  for (const prop of [
    ...Object.getOwnPropertyNames(IncomingMessage.prototype),
    ...Object.getOwnPropertySymbols(IncomingMessage.prototype),
  ]) {
    Object.defineProperty(proto, prop, {
      ...Object.getOwnPropertyDescriptor(IncomingMessage.prototype, prop),
    });
  }
  for (const prop of [
    ...Object.getOwnPropertyNames(PassThrough.prototype),
    ...Object.getOwnPropertySymbols(PassThrough.prototype),
  ]) {
    Object.defineProperty(proto, prop, {
      ...Object.getOwnPropertyDescriptor(PassThrough.prototype, prop),
    });
  }
  Object.setPrototypeOf(stream, proto);

@kettanaito
Copy link
Member

kettanaito commented Feb 26, 2022

That's a good observation, @hrsh7th. Then we can extract property descriptors from IncomingMessage and infer those when cloning. Do you think that'd work?

Object.getOwnPropertyDescriptors should return both plain properties and getter/setters (and Symbols).

Okay, that seems to be how we're doing it now:

Object.defineProperty(stream, propertyName, {
...propertyDescriptor,
value: message[propertyName],
})

I wonder why proprertyDescriptor doesn't clone the getter/setter for headers.

@hrsh7th
Copy link
Contributor Author

hrsh7th commented Feb 26, 2022

The current implementation will clone properties defined in instance but not clone properties defined in prototype.

So I think we should add it. I'll make PR soon.

@hrsh7th
Copy link
Contributor Author

hrsh7th commented Feb 26, 2022

I've submitted the PR. I'm not an English speaker so I'm sorry. I'll talk about this via actual codes.

@trevoro
Copy link

trevoro commented Feb 26, 2022 via email

@hrsh7th
Copy link
Contributor Author

hrsh7th commented Feb 26, 2022

Oh. If we just want to clone (without using PassThrough), it's can use clone module, I think so too.

@lionralfs
Copy link

#195 seems related

Also nodejs/node#36550 could have valuable background info related to the lazy IncomingMessage.headers change (nodejs/node#35281)

@github-actions
Copy link

github-actions bot commented Mar 1, 2022

🎉 This issue has been resolved in version 0.13.6 🎉

The release is available on:

Your semantic-release bot 📦🚀

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 a pull request may close this issue.

4 participants