fastify / fastify

Fast and low overhead web framework, for Node.js

Home Page:https://www.fastify.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Request hangs if you `await reply.status` in an async handler.

sdeprez opened this issue · comments

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

4.27.0

Plugin version

No response

Node.js version

20.12.0

Operating system

Linux

Operating system version (i.e. 20.04, 11.3, 10)

22

Description

MRE:

const fastify = require('fastify');

const app = fastify({ logger: true }); 

app.get('/', async (request, reply) => {
  await reply.status(400);                                                                                                                                                                                                                                                                                                    
  return {error: 'Bad Request'};
});

app.listen({ port: 3000 })

Then curl -v http://localhost:3000 will hang forever. Now the trick is that nowhere in the docs it says that you should await the reply.status call but I'm also using the eslint rule no-floating-promises which forces to either await or void the promise to explicitly handle it and that rule triggers here. Using void indeed solves the problem but it feels a bit weird and also it's very easy to forget about it, use an await and then 💥 . So I guess there are two questions:

  1. Is it a bug of the typings or the eslint rule that detects a false positive?
  2. Regardless, using await should not make the handler hang forever, at worst it should be like await 42 which is useless but harmless.

Expected behavior

Either reply.status should not be a promise or you should be able to await without having the handler hanging forever.

commented

It is a false positive alarm.
reply.status return reply to allows chain-able feature.

reply has a method .then which cause ESLint to always think it must be await-ed, but it should not in current case.

I will mark it as won't fix.

Hi thanks for the quick reply, but can you reconsider? Regardless of the eslint rule, it's abnormal and quite dangerous than await reply.status hangs the handler forever.

commented

How can we bypass the await when it is actually valid to await reply?

We can never knows which case must be await-ed as it share the same object.

If I understand the problem correctly:

  1. the modifiers return the same object that they were called on, i.e. reply.status(...) returns the reply itself
  2. reply can be awaited to find out when it was sent
  3. the reply will never be sent before we call reply.send(...)
  4. we await the reply before sending it when we call await reply.status(...), thus causing a Deadlock

So I have to agree with the maintainer that the eslint rule shouldn't apply to this custom Reply object, that is not even a native Promise

But I also wonder, what can we do to signal to no-floating-promises that this is, in fact, not a floating promise? (I mean at the API level, not by using workarounds such as disabling the rule or using (void))

Maybe at least a warning about the Deadlock situation should be added to the docs.

It's invalid to await the reply in the provided example as it isn't the line sending the reply. The correct implementation would be:

await reply.status(400).send({ error: 'Bad Request' })

Ok thanks for the answers, I understand better now (I didn't know there was a usecase to await a reply). At the fastify level, I agree then that there may not be much to do, without sacrificing a feature or breaking the API (but as you said, documentation would already be really nice!). I try to think how we could tweak eslint on the other hand.

commented

But I also wonder, what can we do to signal to no-floating-promises that this is, in fact, not a floating promise?

I hope there was a regexp to provide a way to disable this rules, but there isn't.

As least someone open an issue typescript-eslint/typescript-eslint#4961 before but consider it is intentional.

For me, I always chain until the .send to prevent this error pop-up.

@jsumners your code would solve the issue but I believe that setting the status then later returning plain objects (without send) is also valid in Fastify?

Also sidenote for references: I've created the issue for reply.status but it's the same for every method that return a FastifyReply, the first time I've met this bug, it was actually with await reply.setCookie (from fastify-cookie) which also return a FastifyReply.

commented

I didn't know there was a usecase to await a reply

If you are using some API that to not provide Promise, or you want to minimize the Promise creation.

async function(request ,reply) {
  fs.readFile('somefile', (err, content) => {
    if(err) {
      reply.status(400).send('not ok')
    } else {
      reply.send(content)
    }
  })
  await reply.status(200)
}

At minimum, an addendum in the documentation that warns of this scenario is appropriate.
Being able to rely on types and tooling is vital for efficient dev workflows, so it should be logged when this is not the case.

It is unfortunate that this is how eslint is forced to implement the no-floating-promises rule, but this is a pitfall that is easy to overlook.
This is further compounded by the fact that no-floating-promises is enabled by extending "plugin:@typescript-eslint/recommended-type-checked", which is "marketed" as a recommended procedure.

Re the issue for tslint: there may be hope one day with typescript-eslint/typescript-eslint#7008 (with @mcollina actually himself commenting on that PR for a similar usecase.)

Is it actually that important to make Reply awaitable? Wouldn't it be more idiomatic to return promise only from a method, which initiates some asynchronous work, such as send(), and keep Reply a "normal" object.

IMHO, this promise-like Reply is neat, but it rather causes troubles/misunderstandings than provides benefits.

What about changing it in v5?

commented

I am open to discussion, but better to open a dedicated issue for the changes.
You can reference this issue in the new one.

Is it actually that important to make Reply awaitable?

Yes. There is plenty of information about this in the issues and pull requests histories.

At minimum, an addendum in the documentation that warns of this scenario is appropriate.

It is documented. Improvements are welcome https://fastify.dev/docs/latest/Reference/Routes/#promise-resolution

@jsumners your code would solve the issue but I believe that setting the status then later returning plain objects (without send) is also valid in Fastify?

async function handler (req, reply) {
  // do stuff
  reply.status(500)
  // do more stuff
  return { an: 'object' }
}