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

Error refinement [NotFound and Conflict] #1599

Merged
merged 2 commits into from
Jul 8, 2022
Merged

Error refinement [NotFound and Conflict] #1599

merged 2 commits into from
Jul 8, 2022

Conversation

enrichman
Copy link
Member

I was having a look at the errors package of the APIs and it's probably a bit "over complicated", or maybe just verbose.

I wanted to simplify it a bit, and I've started just doing a small couple of things for the NotFound and Conflict errors.

I've changed the NewNotFoundError signature to accept an error instead of just a message. In my opinion it makes sense to create an API error from another error, that has more context about what happened and where. So I just needed to modify the callers adding a Wrap or using a simple errors.New.

I've also added a NewConflictError, and used it (along with the NewNotFoundError) to construct the other "domain" errors.

@enrichman enrichman requested a review from a team as a code owner June 24, 2022 10:35
@enrichman enrichman self-assigned this Jun 24, 2022
@codecov-commenter
Copy link

codecov-commenter commented Jun 24, 2022

Codecov Report

Merging #1599 (c2f2256) into main (11d6947) will decrease coverage by 1.41%.
The diff coverage is 60.86%.

❗ Current head c2f2256 differs from pull request most recent head 6c04568. Consider uploading reports for the commit 6c04568 to get more accurate results

@@            Coverage Diff             @@
##             main    #1599      +/-   ##
==========================================
- Coverage   62.02%   60.60%   -1.42%     
==========================================
  Files         167      167              
  Lines       12347    12319      -28     
==========================================
- Hits         7658     7466     -192     
- Misses       3696     3882     +186     
+ Partials      993      971      -22     
Flag Coverage Δ
acceptance-api 48.42% <52.17%> (+0.25%) ⬆️
acceptance-apps 37.91% <26.08%> (+0.36%) ⬆️
acceptance-cli 55.96% <47.82%> (-1.36%) ⬇️
unittests 8.63% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
internal/api/v1/service/catalog.go 45.94% <0.00%> (ø)
internal/api/v1/service/delete.go 65.15% <0.00%> (ø)
internal/cli/server/server.go 58.08% <0.00%> (ø)
internal/api/v1/service/validate.go 69.23% <66.66%> (ø)
pkg/api/core/v1/errors/errors.go 72.72% <70.58%> (-1.75%) ⬇️
internal/cli/usercmd/chart.go 32.67% <0.00%> (-38.62%) ⬇️
internal/cli/login.go 16.00% <0.00%> (-36.00%) ⬇️
internal/cli/usercmd/login.go 0.00% <0.00%> (-35.63%) ⬇️
internal/cli/chart.go 27.65% <0.00%> (-27.66%) ⬇️
internal/cli/settings.go 44.44% <0.00%> (-23.24%) ⬇️
... and 17 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1ba782a...6c04568. Read the comment docs.

@andreas-kupries
Copy link
Contributor

So I just needed to modify the callers adding a Wrap or using a simple errors.New.

Which makes all the callers more verbose.

Copy link
Contributor

@andreas-kupries andreas-kupries left a comment

Choose a reason for hiding this comment

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

See the one place where I commented.

@@ -61,7 +60,7 @@ func ValidateService(
srv, err := client.GetRelease(releaseName)
if err != nil {
if errors.Is(err, helmdriver.ErrReleaseNotFound) {
return apierror.NewNotFoundError(fmt.Sprintf("%s - %s", err.Error(), releaseName))
return apierror.NewNotFoundError(errors.Wrapf(err, "release %s not found", releaseName))
Copy link
Contributor

Choose a reason for hiding this comment

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

I can see this being more sensible than what we had, i.e. providing the entire base error instead of just its message.
OTOH the

-		return apierror.NewNotFoundError("service not found")
+		return apierror.NewNotFoundError(errors.New("service not found"))

in line 47 above just becomes more verbose than necessary.

How about having two functions, with slightly differing signatures, i.e. the old function, keeping its signature

NewXError (msg string, ...)

and a new

NewXFromError (err error, ...)

with the new signature for where we wish to pass in an underlying error ?

And coming back to this place, as written even that is actually to verbose, as it does not use the ...string argument of the old and new function, while it could have:

Suggested change
return apierror.NewNotFoundError(errors.Wrapf(err, "release %s not found", releaseName))
return apierror.NewNotFoundError(err, "release", releaseName, "not found")

Although that runs afoul of the ", " separator used in the underlying strings.Join. A " " separator would be nicer IMHO.

Looking at other places using fmt.Errorf, fmt.Sprintf, and the like a simple reorg of the error message could get rid of many of these I believe. I.e. instead of fmt.Errorf("Foo '%s' does not exist", X) use "Foo does not exist:", X, i.e. the dynamic part is just another detail argument of the NewX... function.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, I've commented before have read this comment. 😄
I'm fine with having the two signatures! 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that NewX functions seem to use only the err.Error() of the passed error. The errors.Wrapf in the callers effectively devolve into an extended error message. and that is something we already can have without the additional Wrap, through the details argument.

@enrichman
Copy link
Member Author

So I just needed to modify the callers adding a Wrap or using a simple errors.New.

Which makes all the callers more verbose.

That's true, but an error usually has more information than a simple string (like the stacktrace), and it's not the common case to return an error without an error (usually an error occurred, and you need to create an HTTP error from it).

https://github.com/pkg/errors/blob/master/errors.go#L16-L18

// The errors.Wrap function returns a new error that adds context to the
// original error by recording a stack trace at the point Wrap is called,
// together with the supplied message.

So I think that this is a valid fix:

// BEFORE
if err != nil {
	if k8serrors.IsNotFound(err) {
		return apierror.NewNotFoundError("service not found")
=	}
}

// AFTER
if err != nil {
	if k8serrors.IsNotFound(err) {
		return apierror.NewNotFoundError(errors.Wrap(err, "service not found"))
	}
}

while sometimes it's not very useful when you need to return an error without errors (but as I said it's not the most common case)

// BEFORE
if theService == nil {
	return apierror.NewNotFoundError("service not found")
}

// AFTER
if theService == nil {
	return apierror.NewNotFoundError(errors.New("service not found"))
}

So if you think that adding the errors.New is sometimes too verbose I'm fine with keeping both the implementation. 👍

Also about the stacktrace I've seen that the pkg/errors is archived and no longer maintained, and they suggest to move to the standard fmt lib actually: pkg/errors#245

But I think it's not really an issue, it's just something that I've noticed and that we could add in the backlog to remove a dependency.

@enrichman
Copy link
Member Author

@andreas-kupries As you pointed out we are not really using the error, we are losing the information with err.Error().
And also the details are a bit strange, with the , separator, while sometimes we want to format the error or the details in a different way.

Also in many places we are not sure on what to put into the details, and we are passing a blank string.

I was thinking a bit about it, and I wanted to simplify everything a bit more.

I'm going to push a new implementation, with only the string message, and without the need to specify the details. We can add the details later with a WithDetails and WithDetailsf func.

This seems to simplify everything, and it looks a bit less verbose.

Please note that I'm changing only the NotFound and Conflict errors, but if that's a feasible approach I will obviosuly port these changes also in the other errors (to avoid too many changes all at once).

@andreas-kupries
Copy link
Contributor

👍 Will wait until the new changes are and then revisit. I am guessing that the removed details ... is not everything coming.

Copy link
Contributor

@andreas-kupries andreas-kupries left a comment

Choose a reason for hiding this comment

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

Good enough to me now.

The nits I saw can be done in a separate PR.

Example:

NewNotFoundError(fmt.Sprintf("Application Chart '%s' does not exist", app))

could be written as

NewNotFoundError("Application Chart", app)

with a slight change to the signature of the new helper. And the fmt.Sprintf moves inside.

Similar for NewConflictError.

Maybe as part of #1600 ?

@enrichman
Copy link
Member Author

Thanks, yes, I'll add those changes in the other PR, so I can merge this.

I was thinking to add a NewXXXErrorf, as the std and other libraries have, that adds the formatting capability.

@enrichman enrichman merged commit 4c3ce50 into main Jul 8, 2022
@enrichman enrichman deleted the error-refinement branch July 8, 2022 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

3 participants