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

StreamableFile pipe can leak resources if response is closed/errored prematurely #9759

Closed
4 of 15 tasks
alexd6631 opened this issue Jun 13, 2022 · 6 comments · Fixed by #9819
Closed
4 of 15 tasks

StreamableFile pipe can leak resources if response is closed/errored prematurely #9759

alexd6631 opened this issue Jun 13, 2022 · 6 comments · Fixed by #9819
Labels
needs triage This issue has not been looked into

Comments

@alexd6631
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Current behavior

return body.getStream().pipe(response);

When returning a StreamableFile from a controller, it seems the express adapter will .pipe() it to the express response stream.
As you may know pipe method is somewhat deprecated because it does not properly closes streams in case of abnormal termination, newer code should prefer use pipeline which automatically closes source stream on destination stream error.

So if a client cancels a request while it is streaming a StreamableFile, the stream wrapped by the StreamableFile will be kept forever opened, leading to a potential ressource leak.

Minimum reproduction code

WIP (see below)

Steps to reproduce

I am noticing the issue on a production app I am currently working on and I can't publish the code source without some adaptation effort to create the minimum reproduction code.

In my setup I have an endpoint that streams images store on S3 using the official AWS S3 client and StreamableFile on the controller side. The S3 client use keep alive and a pool of sockets, so if a GetObject stream is not fully consumed it will leak the socket and prevent it to return it to the pool.

I have a front that displays these images, and If I refresh the page several times quickly, it will cancel some in-flights requests on the server, but due to the .pipe behavior, some S3 Stream will not be destroyed. When the whole client pool is "poisoned", all S3 call will now hang forever, which is quite serious condition, only solved by restarting the nest server.

I could work on a minimum reproduction code, but the above setup is already quite involved.
Let me know if there is enough information, or how "minimal" the reproduction code should be in my case.

Expected behavior

StreamableFile should destroy the underlying stream not only on full consumption but also in case of error / early abortion of the consumer stream. This should be achievable easily by replacing .pipe() method by the pipeline function

Package

  • I don't know. Or some 3rd-party package
  • @nestjs/common
  • @nestjs/core
  • @nestjs/microservices
  • @nestjs/platform-express
  • @nestjs/platform-fastify
  • @nestjs/platform-socket.io
  • @nestjs/platform-ws
  • @nestjs/testing
  • @nestjs/websockets
  • Other (see below)

Other package

No response

NestJS version

8.4.5

Packages versions

Node.js version

16.15.0

In which operating systems have you tested?

  • macOS
  • Windows
  • Linux

Other

No response

@alexd6631 alexd6631 added the needs triage This issue has not been looked into label Jun 13, 2022
@micalevisk
Copy link
Member

PRs are more than welcomed 😸

@kamilmysliwiec
Copy link
Member

@jmcdo29 I think this is related to your PR (that introduces the StreamableFile). Do you think we can safely switch to using pipeline instead of .pipe?

@jmcdo29
Copy link
Member

jmcdo29 commented Jun 14, 2022

I think so, and we have tests set up for the feature so we should be able to see if it breaks. I'll take a pass at it today and see what we're looking at.

@jmcdo29
Copy link
Member

jmcdo29 commented Jun 20, 2022

Okay, so pipeline works well. I wanted to get your ideas on an enhancement here:

pipeline takes in the source, and modifiers (none needed here), the destination (response) and a callback. In that callback, if an error occurs it shows up here. Something we could probably do, is if an error happens then call res.status(400).send(err.message), instead of letting a 500 happen. We could also enhance the StreamableFile API to allow for setting of an error handler that would take in the res object and the error that occurs so that people could then implement there own error handling of the stream. This is better than the current 500 to server crash, which is a win. I unfortunately do not see a way for us to get the error to make its way over to the ExceptionFilter due to the error happening in the http adapter.

@kamilmysliwiec do you have any ideas or thoughts here?

@kamilmysliwiec
Copy link
Member

Something we could probably do, is if an error happens then call res.status(400).send(err.message), instead of letting a 500 happen.

I think this would be reasonable yeah.

We could also enhance the StreamableFile API to allow for setting of an error handler that would take in the res object and the error that occurs so that people could then implement there own error handling of the stream

This could be a nice addition as well! Optional error handler that let devs provide their own custom implementation. I t think it's OK if we keep it separate from exception filters

jmcdo29 added a commit to jmcdo29/nest that referenced this issue Jun 21, 2022
`pipeline` ends up destroying streams used if there is an error in one of
the streams. Due to this, there's no chance of a memory leak from errored out
streams. There's also now an addition of adding an error handler to the
`StreamableFile` so that stream errors by default return a 400 and can be
customized to return an error message however a developer would like. These
only effect the express adapter because of how Fastify already internally
handles streams.

fix: nestjs#9759
jmcdo29 added a commit to jmcdo29/nest that referenced this issue Jun 21, 2022
`pipeline` ends up destroying streams used if there is an error in one of
the streams. Due to this, there's no chance of a memory leak from errored out
streams. There's also now an addition of adding an error handler to the
`StreamableFile` so that stream errors by default return a 400 and can be
customized to return an error message however a developer would like. These
only effect the express adapter because of how Fastify already internally
handles streams.

fix: nestjs#9759
jmcdo29 added a commit to jmcdo29/nest that referenced this issue Jun 23, 2022
`pipeline` ends up destroying streams used if there is an error in one of
the streams. Due to this, there's no chance of a memory leak from errored out
streams. There's also now an addition of adding an error handler to the
`StreamableFile` so that stream errors by default return a 400 and can be
customized to return an error message however a developer would like. These
only effect the express adapter because of how Fastify already internally
handles streams.

fix: nestjs#9759
@kamilmysliwiec
Copy link
Member

Let's track this here #9819

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs triage This issue has not been looked into
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants