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:
- Is it a bug of the typings or the eslint rule that detects a false positive?
- Regardless, using
await
should not make the handler hang forever, at worst it should be likeawait 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.
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.
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:
- the modifiers return the same object that they were called on, i.e.
reply.status(...)
returns thereply
itself reply
can be awaited to find out when it was sent- the reply will never be sent before we call
reply.send(...)
- 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.
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
.
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?
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' }
}