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

fix(#5220): better support encapsulated synchronous error handlers #5222

Closed
wants to merge 16 commits into from

Conversation

domdomegg
Copy link
Contributor

@domdomegg domdomegg commented Dec 20, 2023

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

…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.
Copy link
Member

@mcollina mcollina left a 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.

lib/error-handler.js Show resolved Hide resolved
test/encapsulated-error-handler.test.js Outdated Show resolved Hide resolved
test/encapsulated-error-handler.test.js Outdated Show resolved Hide resolved
@Eomm Eomm added the bugfix Issue or PR that should land as semver patch label Dec 23, 2023
lib/error-handler.js Show resolved Hide resolved
lib/error-handler.js Outdated Show resolved Hide resolved
Comment on lines 78 to 80
if (err.cause) {
throw new Error('Child error already has a cause set')
}
Copy link
Member

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?

Copy link
Contributor Author

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)

Copy link
Member

@metcoder95 metcoder95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests seems failing

Comment on lines 78 to 80
if (err.cause) {
throw new Error('Child error already has a cause set')
}
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

@metcoder95 metcoder95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests seems failing

@domdomegg
Copy link
Contributor Author

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.

@metcoder95
Copy link
Member

Good call! First of all, thanks for taking the time on this.
Second, I partially disagree with you as the expectation is clear, but we are digging too much into the details.

The PR is halfway done, so let's clarify some points:

  • If we encounter the .cause is already set, what do we do?
    • Problably the best we can do is to log it to keep it simple.
  • If setting the .cause fails because the object is frozen or any other reason, what do we do?
    • Let's just log it for now, so implementers are aware.

What are your thoughts?
cc: @mcollina

@domdomegg
Copy link
Contributor Author

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.
@MattJustMatt
Copy link

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.

Copy link
Member

@mcollina mcollina left a 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

lib/error-handler.js Outdated Show resolved Hide resolved
Copy link
Member

@mcollina mcollina left a 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) |
Copy link
Contributor

@Uzlopak Uzlopak Jan 30, 2024

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?

Copy link
Contributor

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?

Copy link
Member

@Fdawgs Fdawgs Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| <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.

Copy link
Contributor

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?

Copy link
Member

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.

@jsumners
Copy link
Member

I'll try to review tomorrow.

Copy link
Contributor

@Uzlopak Uzlopak left a 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.

Comment on lines +78 to +89
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'
})
Copy link
Member

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).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsumners

His claim is, that if Error Object is frozen or differently protected, you can not set the attribute.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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()
Copy link
Contributor

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.

Copy link
Contributor Author

@domdomegg domdomegg Feb 3, 2024

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?

@Uzlopak
Copy link
Contributor

Uzlopak commented Feb 8, 2024

I think this feature is needed, but the implementation "feels" not "right".

@Uzlopak
Copy link
Contributor

Uzlopak commented Feb 21, 2024

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.

@domdomegg domdomegg closed this May 8, 2024
@domdomegg
Copy link
Contributor Author

Closing as effectively the same fix as the original commit has been done in #5445

@domdomegg domdomegg mentioned this pull request May 8, 2024
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bugfix Issue or PR that should land as semver patch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants