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

oidc: use %w verb for wrapping errors #381

Merged
merged 1 commit into from Jun 14, 2023
Merged

Conversation

rliebz
Copy link
Contributor

@rliebz rliebz commented Jun 9, 2023

Errors wrapped using %v cannot be unwrapped. This means if there is an underlying error such as context.Canceled, a caller cannot reliably discover that error using any method other than string comparison. By swapping to the %w verb, errors.Is and errors.As become valuable tools for error discovery and behavior differentiation.

While by convention, some of the errors like fetching keys %v should probably be changed to fetching keys: %w (with the : character), this change opts not to do that to help preserve backward compatibility for external error handling that uses string comparison today.

@ericchiang
Copy link
Collaborator

I don't think we want to do this everywhere. Any wrapped errors should have an associated test, since that'd now be an API we have to ensure works for a while.

Is the request here effectively, "methods should wrap context errors"? Does net/http do the same thing?

@rliebz
Copy link
Contributor Author

rliebz commented Jun 11, 2023

In particular the reason that I made this change is to capture context.Canceled. When handling an HTTP request, if the client disconnects, the context attached to the request gets canceled and subsequent calls that use that context will abort early with a context.Canceled error. For the application I'm working on, we don't particularly consider those to be actionable "errors", so we log them at a different level than unexpected errors. Using %v to "opaque" the error produces the exact same error message as %w, except that the error cannot be unwrapped with Unwrap(), and therefore errors.Is and errors.As.

context.Canceled is a good example of an error that might pop up despite this library not directly interacting with it, but IMO wrapping as a best practice has nothing to do with the context library. You can find a fairly large list of standard errors you might see doing HTTP-related things: https://pkg.go.dev/net/http#pkg-variables, as well as things like net.ErrClosed, io.EOF, and others. Any of which might be relevant to a caller.

The reason you might want to opaque an error is if the underlying code is signalling something to the caller that you don't want to signal to your caller. Sometimes error messages are fine to bubble up, but there's a specific reason why you wouldn't want the behavior of underlying errors to become the behavior of your errors. For example if there is an ErrNotFound for something your code is trying to do, but ErrNotFound is not appropriate for the operation that your public interface is exposing.

Does net/http do the same thing?

Sometimes yes, sometimes no (not an exhaustive list, just grabbed a couple of each):

So why would the standard library choose one vs. the other? From that cherry-picked sample, the two examples that use %w were written in the past year, and the two that use %v were written before the existence of the %w verb. This isn't universally true, but it does cleanly explain a lot of the behavior.

@ericchiang
Copy link
Collaborator

Thanks for the reply! I'm familiar with error wrapping. The reason I was asking about net/http was to see if context.Canceled values get passed back up to go-oidc. For example, net/http docs don't claim to wrap errors:

Error values we return become part of our API contract, and not wrapping errors by default is intentional. I don't want some underlying change to break clients because they were expecting ErrFoo, and we're suddenly returning ErrBar.

Happy to wrap errors where its appropriate. But any wrapped errors need a test to verify that those values are actually surfaced.

@rliebz
Copy link
Contributor Author

rliebz commented Jun 12, 2023

I don't think I've really ever seen functions advertise their internal errors as wrapped vs. opaque(ed?). But net/http certainly does wrap frequently. Following the first link for Client.Do, we find the first error here. And url.Error implements the Unwrap method, even though aren't wrapping specifically with %w. They also define an error wrapper shortly after which wraps all subsequent errors in the function that end up being returned. In one specific place we do see opaquing before wrapping, but that line hasn't been touched since error wrapping was introduced to the standard library, so I think that's a side-effect of attempting to annotate the error with a message rather than a specific intent to opaque it, although I could definitely be wrong.

One thing to also point out is that not wrapping errors produces the same behavior as wrapping with the %w verb. So here for example, there's an error bubbling up that's going to satisfy errors.Is on whatever the underlying error returns because it wasn't actively being opaqued with %v.

That said, I don't think an "opaque by default" strategy is unreasonable, so happy to swap most of this back to how it was. I went ahead and pared this change down to the errors that specifically bubble up from HTTP calls, since that should cover my use case, and added a test to demonstrate. A number of error code paths aren't covered yet and would be quite tricky to try to hit, but hopefully what's there now is a good coverage for what's changing.

Copy link
Collaborator

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

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

thanks! just a couple nits

oidc/jwks_test.go Outdated Show resolved Hide resolved
oidc/jwks_test.go Outdated Show resolved Hide resolved
Errors wrapped using %v cannot be unwrapped. This means if there is an
underlying error such as `context.Canceled`, a caller cannot reliably
discover that error using any method other than string comparison. By
swapping to the %w verb, `errors.Is` and `errors.As` become valuable
tools for error discovery and behavior differentiation.

While by convention, some of the errors like `fetching keys %v` should
probably be changed to `fetching keys: %w` (with the `:` character),
this change opts not to do that to help preserve backward compatibility
for external error handling that uses string comparison today.
@ericchiang ericchiang merged commit f0117a5 into coreos:v3 Jun 14, 2023
1 check passed
@rliebz rliebz deleted the w-errors branch June 14, 2023 03:13
@rliebz
Copy link
Contributor Author

rliebz commented Jun 14, 2023

@ericchiang Thanks for your time and help here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants