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
fix(#5220): better support encapsulated synchronous error handlers #5222
Conversation
…lers Parent error handling for synchronous error handlers was largely a unintended byproduct of some other try catches previously. This could result in a server crash if multiple error handlers were used, and rethrew errors (see fastify#5220). This PR fixes this behavior by retrying synchronous error handling (similar to how we already do in wrapThenable for asynchronous error handling). It also adds regression tests to ensure this continues to work properly.
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.
Good job. Only one consideration.
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
lib/error-handler.js
Outdated
if (err.cause) { | ||
throw new Error('Child error already has a cause set') | ||
} |
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.
Curious, should we throw?
What are the expectations here, if let's say, I throw a custom Error
that already has a cause
property already set?
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.
Feels unintuitive to only sometimes edit the cause property. But overwriting it and losing data sometimes seems worse.
I initially suggested we add it as a symbol property, but cause was preferred: #5222 (comment)
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.
Tests seems failing
lib/error-handler.js
Outdated
if (err.cause) { | ||
throw new Error('Child error already has a cause set') | ||
} |
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 over the fence on this one; that can cause more confusion when dealing with unexpected errors.
In the scenario that I have a situation like this, where my err.cause
already is set, I might prefer to have another wrapper on top that preserves the stack trace so I can fully track down the issue instead of throwing a new error that swallows the original one.
Maybe we can find a better strategy, but not sure throwing and swallowing might be the best way to go
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 clarify what you think is swallowing the error? It'd be the custom error handler implementation that could overwrite the cause - otherwise users would get the full stack trace by default.
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.
Tests seems failing
It feels like we keep going back and forth over what fastify should overwrite and not overwrite here, and what should be logged. This seems secondary to fixing what seems more important: that the current error handling behaviour can cause a fatal crash when multiple error handlers are used. If you have specific preferences in mind, could you state how you'd like this to be implemented? Because at the moment it feels like at each review we tweak it a little but the PR is actually getting worse and no closer to being merged. I don't think it's worth continuing work on this until it's clear what the maintainers would prefer. |
Good call! First of all, thanks for taking the time on this. The PR is halfway done, so let's clarify some points:
What are your thoughts? |
Thank you for your quick response. I'm happy to implement it the way you've laid out. However, I'm a little stuck as I think the thing you've written out there is what has already been implemented. There are also tests for the cases you've suggested (.cause set should log, .cause fails because it's frozen should log). I'm therefore not sure what the other half of the PR is you are looking for ("The PR is halfway done"), or where the comment threads we've been having are going. Could you clarify further on this, because I think this is where I am probably most confused! Also sorry if I'm being dumb here and missing something obvious - do please restate it if you think so! I acknowledge you think the expectations are clear, but as a first-time contributor to fastify I'm still a little lost 😅. I appreciate your support in reviewing this PR and helping clarify things thus far! |
Because of how error handlers wrap things, following the control flow in the tets can be tricky. In this test file I've added numbered comments indicate the order statements are expected to execute. I've also standardised the names of things being thrown so it's easier to follow them.
I wanted to chime in as a user hitting this issue in prod- we noticed that throwing from one of our error handlers was crashing the Fastify process. We switched to manually log the issue but losing out on automated fastify-sentry alerting etc... of course we can add this in the manual handling of the error, but if this PR solves the issue I'd like to help. If there are any eyes or extra hands needed for this PR please let me know if I can help. |
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.
Good work, one tiny bit and it's good to go
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
Signed-off-by: Aras Abbasi <aras.abbasi@googlemail.com>
@@ -322,6 +323,7 @@ but the body is being consumed. | Make sure you don't consume the `Response.body | |||
| <a id="fst_err_bad_status_code">FST_ERR_BAD_STATUS_CODE</a> | The status code is not valid. | Use a valid status code. | [#2082](https://github.com/fastify/fastify/pull/2082) | | |||
| <a id="fst_err_bad_trailer_name">FST_ERR_BAD_TRAILER_NAME</a> | Called `reply.trailer` with an invalid header name. | Use a valid header name. | [#3794](https://github.com/fastify/fastify/pull/3794) | | |||
| <a id="fst_err_bad_trailer_value">FST_ERR_BAD_TRAILER_VALUE</a> | Called `reply.trailer` with an invalid type. Expected a function. | Use a function. | [#3794](https://github.com/fastify/fastify/pull/3794) | | |||
| <a id="fst_err_error_cause_already_present">FST_ERR_ERROR_CAUSE_ALREADY_PRESENT</a> | An error handler threw an error with the `cause` property set, so Fastify will not set the cause to the original error. | If used, ensure the cause property is as expected. | [#5222](https://github.com/fastify/fastify/pull/5222) | |
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.
"If used, ensure the cause property is as expected." sounds so strange.
I mean this is the remedy to solve this error. But reading this, I would not know what to do. Can you please elaborate?
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.
@Fdawgs
Can you please give your opinion?
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.
| <a id="fst_err_error_cause_already_present">FST_ERR_ERROR_CAUSE_ALREADY_PRESENT</a> | An error handler threw an error with the `cause` property set, so Fastify will not set the cause to the original error. | If used, ensure the cause property is as expected. | [#5222](https://github.com/fastify/fastify/pull/5222) | | |
| <a id="fst_err_error_cause_already_present">FST_ERR_ERROR_CAUSE_ALREADY_PRESENT</a> | An error handler threw an error with the `cause` property set, so Fastify will not set `cause` to the original error. | If used, ensure the `cause` property is as expected. | [#5222](https://github.com/fastify/fastify/pull/5222) | |
@Uzlopak I think 'cause' needs to be wrapped in backticks consistently. Otherwise 'cause' would be read as a verb rather than a noun, which I think is why it reads strangely currently.
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.
Yeah, but what is the remedy? You get this error, and you should check if it i what you expected? How does this help?
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.
Ah gotcha, I see what you mean. Needs a think.
I'll try to review tomorrow. |
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.
Having some doubts... Will elaborate them in few hours.
try { | ||
if (err.cause) { | ||
throw FST_ERR_ERROR_CAUSE_ALREADY_PRESENT() | ||
} | ||
err.cause = error | ||
} catch (failedToAssignError) { | ||
reply.log.warn({ | ||
err: error, | ||
parentError: err, | ||
reason: failedToAssignError, | ||
message: 'Failed to assign child error as cause to parent error thrown from error handler' | ||
}) |
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.
It's not clear to me why we don't:
if (Object.prototype.hasOwnProperty.call(err, 'cause') === true) {
err = Error('something or other', { cause: error })
} else {
err.cause = error
}
Instead of a second try/catch
and adding a log people are going to miss (and likely won't want, resulting in new issues being filed when they do notice it).
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.
His claim is, that if Error Object is frozen or differently protected, you can not set the attribute.
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 would not be concerned about encountering a frozen error instance. That is extremely unlikely to happen, and probably a bug if it does.
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.
That is extremely unlikely to happen, and probably a bug if it does.
The whole point of this function is to handle errors, i.e. cases where things aren't going as expected. I'd argue this is exactly the place to be robust to edge cases, especially because if we do throw an error here it crashes the entire server.
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 provide an example of anything generating and throwing a frozen error object?
reply[kReplyIsError] = true | ||
try { | ||
if (err.cause) { | ||
throw FST_ERR_ERROR_CAUSE_ALREADY_PRESENT() |
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.
We throw an Error, to abort the control flow. But we instantiate an Error and generate a stacktrace, which we probably dont need anyway?
We dont need it to be in the try catch in this case.
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.
Are you suggesting we could do this instead if err.cause is already set, without entering the try/catch?:
reply.log.warn({
err: error
parentError: err,
reason: FST_ERR_ERROR_CAUSE_ALREADY_PRESENT(),
message: 'Failed to assign child error as cause to parent error thrown from error handler'
})
If so, what would the benefit be here?
I think this feature is needed, but the implementation "feels" not "right". |
I think following thing needs to be modified: In lib/error-handler.js instead of wrapping it into a second try catch block just to be sure we can assign a .cause attribute to the error we create a new FST_ERR_FAILED_ERROR (name pending) and assign a .cause to this always. Avoids to use a second try catch. We then dont even need to warn of a failed attempt to set cause because we could not set .cause of the error The rest was not disputed. |
Closing as effectively the same fix as the original commit has been done in #5445 |
Parent error handling for synchronous error handlers was largely a unintended byproduct of some other try catches previously.
This could result in a server crash if multiple error handlers were used, and rethrew errors (see #5220).
This PR fixes this behavior by retrying synchronous error handling (similar to how we already do in wrapThenable for asynchronous error handling). It also adds regression tests to ensure this continues to work properly.
Checklist
npm run test
andnpm run benchmark
and the Code of conduct