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
Test route handler rejection with non-Error #441
Conversation
PS: Is it possible to disable the pre-commit hook? I find it a bit annoying to wait ~20s on every commit, and to be unable to commit a failing test (TDD). |
@sebdeckers you can do |
It seems wrong to me to throw something that isn't in the |
@jsumners it is odd, but the spec supports that, and such an error can bubble up from the prototype chain. @sebdeckers good catch, would you like to try a fix? We should just change the function |
@jsumners I encountered this as some library threw an "error" object that had the Error interface but was not an Another use case would be error objects passed between workers in a cluster, where they are serialised as JSON. Even if the prototype chain were somehow preserved, they would be instances of different @mcollina 🤔 Ooh, interesting. Thanks for the hint! |
Upon closer inspection this had already been solved in |
@sebdeckers this looks really wrong to me to pass any value as the first argument to the Otherwise, all validation plugins only can respond to a string. |
@StarpTech If it's an error the payload is passed one-to-one. If it's not an error, we eventually cast it to a string anyway. (Note this is the same approach as taken in the existing code just below.) |
lib/handleRequest.js
Outdated
if (err instanceof Error) { | ||
this.reply.send(err) | ||
} else { | ||
this.reply.send(new Error(err || '')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not not very happy about this part, can you do a check if err
is a string and use that with new Error
, or log the object with a warning?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why it must be a string? I think the Error constructor does this already. See: https://tc39.github.io/ecma262/#sec-error-message
FYI the error-like objects that I was getting from my app's dependency actually have a toString
method so this fix works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to avoid:
> new Error({ hello: 'world' })
Error: [object Object]
at repl:1:1
at ContextifyScript.Script.runInThisContext (vm.js:50:33)
at REPLServer.defaultEval (repl.js:240:29)
at bound (domain.js:301:14)
at REPLServer.runBound [as eval] (domain.js:314:12)
at REPLServer.onLine (repl.js:441:10)
at emitOne (events.js:121:20)
at REPLServer.emit (events.js:211:7)
at REPLServer.Interface._onLine (readline.js:282:10)
at REPLServer.Interface._line (readline.js:631:8)
We can check if the object has a toString
method that is not Object.prototype.toString
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I feel like this type checking and coercion is premature here. It should take place after handleError (and any reply.context.errorHandler
hooks) have processed the raw error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sebdeckers all of what you have mentioned assumes an error is an Error
. Refactoring that would be a major undertaking, and I do not think it is worth it to support this edge case.
@sebdeckers we shouldn't cast anything since any value can be thrown as an error. |
@StarpTech Agreed; will undo this commit. But I don't see how better to solve this problem. Suggestions? |
@StarpTech I think the solution in #441 (comment) could get it done? IMHO we have two issues, when a non-error is catched:
|
@mcollina I think about to handle that very neutral. When the developer passes an error it's fine but when not we should just pass it as it is. It doesn't look like a responsibility of fastify to enforce that interface (since any value is valid) function wrapReplyEnd (context, req, res, statusCode, payload) {
const reply = new context.Reply(req, res, context)
if (payload instanceof Error) {
reply.code(statusCode).send(payload)
} else {
reply.code(statusCode).send(payload) // fastify can not know what it is so let's the developer decide
}
return
} |
@StarpTech So you propose to do:
|
@mcollina yes! |
@mcollina we have to mark this payload as an error even when it is not from type |
@StarpTech I've done another attempt. This is using a private flag on the reply. Thoughts? Notes:
|
lib/reply.js
Outdated
@@ -10,6 +10,8 @@ const flatstr = require('flatstr') | |||
const fastseries = require('fastseries') | |||
const runHooks = fastseries() | |||
|
|||
const isError = Symbol.for('is-error') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use just Symbol
for performances reasons. Since every symbol is different you must declare it in one file and export it.
@mcollina thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are no performance reasons to do that, more keeping it private. I agree.
@delvedor @mcollina I've changed it to a Symbol that is expoted by Now the |
lib/reply.js
Outdated
@@ -59,7 +61,7 @@ Reply.prototype.send = function (payload) { | |||
return | |||
} | |||
|
|||
if (payload instanceof Error) { | |||
if (payload instanceof Error || this[isError] === true) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you please do the this[isError]
check only once and store it in a variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, good catch. Done in 67e0ed5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
LGTM |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing reply
hidden class could be a problem. Another problem is on using Simbol
for checking isError
.
Maybe it is better to add isError
property to Reply
class (false
as default)
@StarpTech you just need to add |
lib/reply.js
Outdated
@@ -203,7 +208,7 @@ function handleError (reply, err, cb) { | |||
|
|||
cb(reply, Object.assign({ | |||
error: statusCodes[reply.res.statusCode + ''], | |||
message: err.message, | |||
message: (err || undefined) && err.message, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing something here, why err || undefined
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since err
can be any value this ensures any falsy evaluation of err
sets the message
property to undefined
. That way it will not be output into the JSON response. Otherwise the falsy value of err
(e.g. null
, false
, 0
, ''
) would be sent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the message
property should be always there.
If you don't have a value to write, just use an empty string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -10,6 +10,8 @@ const flatstr = require('flatstr') | |||
const fastseries = require('fastseries') | |||
const runHooks = fastseries() | |||
|
|||
const isError = Symbol('is-error') | |||
|
|||
function Reply (req, res, context, request) { | |||
this.res = res | |||
this.context = context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you set this[isError] = false here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I can do that. Was afraid that extra statement would add some performance cost.
I guess it also needs to be added to buildReply
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM Only a few nits.
test/reply-error.test.js
Outdated
) | ||
}) | ||
|
||
fastify.setErrorHandler((err, reply) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you move this before request injection?
|
||
fastify.setErrorHandler((err, reply) => { | ||
t.strictEqual(err, nonErr) | ||
t.end() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The end of the test should be in the injection callback, not here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This handler fires last. Not sure how I can end the test before it runs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How could it be possible? The inject callback has to be called after the setErrorHadler. @mcollina
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@allevo 😓 Upon closer inspection, it appears I still misunderstood tap's parallel subtests. I'll refactor again. WIP.
test/reply-error.test.js
Outdated
}) | ||
}) | ||
} | ||
t.end() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you use t.plan(objs.length)
instead?
Ping, I would love to get this out asap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
That's weird... tests are passing locally. Same Node 9.1.0
|
The test fails in the exact same way on my machine. |
@mcollina Sorry about that. Forgot to rebase. 🙇🏼♂️ |
@allevo are you ok with this? Seems ready to me. |
@allevo the nits have been addressed, I'm landing. |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
I noticed that Fastify does type checking (
err instanceof Error
) when a promise is rejected (async function throws). This is problematic when the thing thrown does not actually inherit from Error.It's not entirely clear to me how to address this.
Reply.prototype.error()
methodReply.prototype.send()
to mark the payload as errorerr
with anew Error()
instance (losing the original object)Suggestions?