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

Implement the errors.Unwrap interface as appropriate #4567

Closed
wants to merge 2 commits into from

Conversation

LukeShu
Copy link

@LukeShu LukeShu commented Jun 26, 2021

Since Go 1.13, implementations of error that wrap another error should implement an Unwrap() error method, which will make the error be usable with the errors.Is, errors.As, and errors.Unwrap inspection functions.

There is an existing ticket about supporting error unwrapping (#2934) but most of what is being talked about in that ticket is about wrapping for gRPC statuses being sent over the network. This PR doesn't deal with that. It just deals with implementing Wrap on local error types.

It is tempting to also search for fmt.Errorf("whatever: %v", err) and change the %v to %w, but (1) that would have required either depending on Go 1.13 (compared to the current 1.11) or replacing fmt.Errorf with golang.org/x/xerrors.Errorf, and (2) seemed tedious and I didn't really want to put in the effort. The existing PR covers the errors that I happen to care about.

As an example of what this allows: If I dial an AF_UNIX socket that doesn't exist, (conn, err := grpc.DialContext(ctx, "unix:/path/to/file.sock", ...)) I can now check for that condition with errors.Is(err, os.ErrNotExist).

RELEASE NOTES:

  • Custom error types now implement the interface for errors.Unwrap as appropriate.

LukeShu added 2 commits June 26, 2021 12:42
- The comment speaks of "future error.Is functionality"; well Go 1.13 has
  since been released; it isn't "future" anymore.  Change "future" to
  "Go 1.13".
- In that same phrase, change "error.Is" to "errors.Is"; since it's
  referring to the functionality in the "errors" package.
- The second sentence starts "A Error"; change that to "An Error".
There are several implementations of `error` that happen to wrap an inner
`error`.  Have these also implement the standard interface for
`errors.Unwrap`.
@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Jun 26, 2021

CLA Signed

The committers are authorized under a signed CLA.

@irl-segfault
Copy link

Nice. Looking forward to this being the default error treatment so that we can always leverage .Is and .As in error handling.

@zasweq zasweq added this to the 1.40 Release milestone Jun 29, 2021
@zasweq zasweq added the Type: Behavior Change Behavior changes not categorized as bugs label Jun 29, 2021
zasweq
zasweq previously approved these changes Jun 29, 2021
@zasweq zasweq requested a review from dfawley June 29, 2021 14:23
@zasweq zasweq dismissed their stale review June 29, 2021 16:05

Team lead disagrees with idea.

@dfawley
Copy link
Member

dfawley commented Jun 29, 2021

There is an existing ticket about supporting error unwrapping (#2934) but most of what is being talked about in that ticket is about wrapping for gRPC statuses being sent over the network

Correction: #2934 is about wrapping Status errors inside other errors.

I'm not sure this is something we want. The errors coming back from gRPC (Dial* and RPCs) should be status.Status errors, and they should be self-sufficient (i.e. not wrap other errors). In your example, you should be getting status.Convert(err).Code() == codes.Unavailable. Is that not the case / does that not work?

@LukeShu
Copy link
Author

LukeShu commented Jul 1, 2021

@dfawley wrote:

The errors coming back from gRPC (Dial* and RPCs) should be status.Status errors, and they should be self-sufficient (i.e. not wrap other errors). In your example, you should be getting status.Convert(err).Code() == codes.Unavailable. Is that not the case / does that not work?

That is not the case / that does not work.

Given the code

_, err := grpc.Dial("unix:/does/not/exist.sock",
	grpc.WithInsecure(),
	grpc.WithBlock(),
	grpc.FailOnNonTempDialError(true),
)

err will be

err := transport.ConnectionError{ // google.golang.org/grpc/internal/transport
	Desc: "transport: error while dialing: dial unix /does/not/exist.sock: connect: no such file or directory",
	temp: false,
	err: &net.OpError{
		Op:     "dial",
		Net:    "unix",
		Source: nil,
		Addr: &net.UnixAddr{
			Name: "/does/not/exist.sock",
			Net:  "unix",
		},
		Err: syscall.ENOENT,
	},
}

Because transport.ConnectionError does not implement GRPCStatus, status.Convertstatus.FromError will hit the fallback return New(codes.Unknown, err.Error()), false case. And so status.Convert(err).Code() will be codes.Unknown.

@LukeShu
Copy link
Author

LukeShu commented Jul 1, 2021

@dfawley wrote:

I'm not sure this is something we want. The errors coming back from gRPC (Dial* and RPCs) should be status.Status errors, and they should be self-sufficient (i.e. not wrap other errors).

I'm not sure I agree.


The google.golang.org/grpc/status documentation reads:

Package status implements errors returned by gRPC. These errors are serialized and transmitted on the wire between server and client, and allow for additional data to be transmitted via the Details field in the status proto.

It does not speak of synthesizing statuses for errors not returned over gRPC over the wire. To use statuses that way would be surprising and unexpected to current users.


In HTTP client libraries we don't represent network errors as HTTP 500 responses; we differentiet between protocol-errors and errors-sent-over-the-protocol. Between in-band and out-of-band errors. I believe that gRPC libraries should follow the same principle.

@LukeShu
Copy link
Author

LukeShu commented Jul 1, 2021

Also note that in the case of ConnectionError being able to extract that underlying error is already supported functionality; just via the non-standard Origin method. All this PR is doing for that type is adding an Unwrap "alias" for it (actually they behave differently for nil), to conform to the standard interface for errors.Unwrap.

@dfawley
Copy link
Member

dfawley commented Jul 13, 2021

Note that Dial shouldn't actually be blocking or error (except for things like target parse errors, illegal DialOption values, etc). That's not how client creation works in other languages. In other languages, you create the client and then make RPCs. If the client didn't connect to the server for whatever reason, then you get UNAVAILABLE RPC errors. (This is the basis behind #1786.)

IMO, considering that, we should really be returning only status errors from Dial for the things that are connection related. However, I may be convinced that #3616 (wrapping other errors inside status errors that don't go over the wire) should happen. gRPC-Java apparently does something similar to this and it seems to work fine for them.

Because transport.ConnectionError does not implement GRPCStatus, status.Convertstatus.FromError will hit the fallback return New(codes.Unknown, err.Error()), false case. And so status.Convert(err).Code() will be codes.Unknown.

So maybe this is what we can do for now? Implement GRPCStatus in these errors so they can be converted to status errors, but also can be inspected and unwrapped otherwise?

@github-actions
Copy link

github-actions bot commented Aug 2, 2021

This PR is labeled as requiring an update from the reporter, and no update has been received after 6 days. If no update is provided in the next 7 days, this issue will be automatically closed.

@github-actions github-actions bot added the stale label Aug 2, 2021
@github-actions github-actions bot closed this Aug 9, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants