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

APIError.GRPCStatus returns "OK" for all http errors. #254

Open
ribrdb opened this issue Feb 16, 2023 · 19 comments · Fixed by #260
Open

APIError.GRPCStatus returns "OK" for all http errors. #254

ribrdb opened this issue Feb 16, 2023 · 19 comments · Fixed by #260
Labels
priority: p2 Moderately-important priority. Fix may not be included in next release. type: question Request for information or clarification. Not an issue.

Comments

@ribrdb
Copy link

ribrdb commented Feb 16, 2023

When using apierror.FromError with an http error, the status field does not get populated, so the GRPCStatus() method returns nil. It seems like this is intended to mean "no status is available". However, the status package treats nil as "OK":
https://github.com/grpc/grpc-go/blob/dba26e15a07f43875ccf806a2dd6cbcbc1c12eab/internal/status/status.go#L72

The status package also says that no non-nil error should have status of "OK".
To properly fulfil this contract, it seems that the APIError.status should never be nil. I would expect it to at least return UNKNOWN, but it looks like the json error may also include a status field with a grpc status code, and some of the http status code seem to map directly to grpc codes (e.g. 404, 429, 503):
https://cloud.google.com/apis/design/errors#handling_errors

@ribrdb ribrdb added priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Feb 16, 2023
@quartzmo
Copy link
Member

quartzmo commented Feb 24, 2023

@ribrdb The apierror.APIError type returned by apierror.FromError is intended to be used either with gRPC or HTTP. When used with HTTP, the status field should be ignored, and instead the httpErr should be regarded. The type is currently described as follows:

APIError wraps either a gRPC Status error or a HTTP googleapi.Error. It implements error and Status interfaces.

Do you think this description could be improved to make the situation clearer?

@quartzmo quartzmo added type: question Request for information or clarification. Not an issue. and removed type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Feb 24, 2023
@ribrdb
Copy link
Author

ribrdb commented Feb 24, 2023

How are we supposed to tell which one to use?
It seems incorrect to say that it implements the "Status" interface. The status interface should never return nil from the GPRCStatus method unless the error is nil. Any code that checks for a GRPCStatus method has no way to know that the method is broken because this is an http error.

@quartzmo
Copy link
Member

Does the application code not have the context of whether the transport is gRPC or HTTP?

@ribrdb
Copy link
Author

ribrdb commented Feb 27, 2023

I don't know how we would. Some google apis are returning errors wrapped with APIError. Is our code supposed to know whether that api is using grpc or rest?
The main problem is that we have generic application code that uses the status.Code() method on errors that could be from anywhere. If this is an APIError wrapping an http error, we get OK and our code thinks there was no error. If we got at least UNKNOWN our code would not be broken, but it would certainly be preferable to use the status code that's in the http error if it's included.

@quartzmo
Copy link
Member

Some google apis are returning errors wrapped with APIError.

Client libraries (not APIs) return errors wrapped with APIError. If you can list some of the client libraries you are using, I may be able to answer whether that client is using grpc or rest.

we have generic application code that uses the status.Code() method on errors that could be from anywhere.

As you know, status.Code is from gRPC-Go. It is gRPC-specific. If you have generic application code that handles errors from both gRPC and HTTP clients, is there any way to make this context aware of which type of client is returning the current error? In that case, you could only check for status.Code if the originating client is gRPC.

@ribrdb
Copy link
Author

ribrdb commented Feb 27, 2023

Well the way we checked before is to see if the error has a GRPCStatus method. But your error objects have a GRPCStatus method that returns something that method should never return. You are returning a nil status object. A nil status object means "no error ocurred" not "this wasn't a grpc error".

@quartzmo
Copy link
Member

Thanks, I think your usage makes sense and is clear now. I'll see what can be done.

@quartzmo
Copy link
Member

I'm going to change the title of this issue to indicate that apierror.APIError should support querying whether it wraps a gRPC Status error or a HTTP googleapi.Error. Let me know however if you think there is some other issue to address here.

@ribrdb
Copy link
Author

ribrdb commented Feb 27, 2023

I don't think adding a way to query solves the problem.
Then code still has to be aware of apierror.APIError. Existing code that just uses the grpc status won't know that it has to query before it can use any of the methods in the grpc status package.
It seems like either apierror.ParseError should return an object that has no GRPCStatus method for non-grpc errors, or else the GRPCStatus method should actually follow the conventions of the status package and never return nil.

Why can't setDetailsFromError do

case isHTTPErr:
		a.httpErr = herr
		a.details = parseHTTPDetails(herr)
		a.status = status.New(codes.UNKNOWN, herr.Message)

Or have parseHTTPDetails return the value of e.GetError().GetStatus() if it's there?

@ribrdb
Copy link
Author

ribrdb commented Feb 27, 2023

The current implementation of apierror.APIError makes it so that status.Code and status.FromError are broken. Either apierror.APIError should be updated so that the status package isn't broken, or the status package should be udpated to handle GRPCStatus returning nil. But it seems to me that grpc-go shouldn't have to update their code because you want to redefine how the GRPCStatus method behaves.

@noahdietz
Copy link
Contributor

Hey @ribrdb, @quartzmo will go ahead with your suggested fix here.

Would mind sharing which API clients you are using? I just want to learn more, that is all. Obviously there is a gap in how we anticipated this being used by end users and how it actually is. Specifically, when you said the following:

Some google apis are returning errors wrapped with APIError. Is our code supposed to know whether that api is using grpc or rest?

My first thought was "Yes, this should be known to the user". Now I realize that we were wrong in making that assumption, and this is definitely not the fault of the user.

Some more thinking/context you may (or may not) care about: With the clients in cloud.google.com/go, one must explicitly change their code to use HTTP/JSON (e.g. language.NewRESTClient). All of the clients in google.golang.org/api use HTTP/JSON. So the thinking was that if one were to invoke a one of these clients and inspect the errors, they'd be explicitly changing/writing code to invoke an HTTP/JSON API and would not use gRPC Status APIs to inspect the error. The naivety in our thinking here I'd say is that there could be some abstraction that uses multiple clients with varying transports, bubbling up errors to a centralized handler that needs to inspect the error type to know how to handle it.

@ribrdb
Copy link
Author

ribrdb commented Feb 28, 2023

Here's some background context on our side.
We've been using various Google Cloud apis for many years. So I guess we've gotten used to thinking "Google apis return grpc errors." Our own internal apis also use grpc errors. So that's what our team is used to.
We've more recently started using the Google Classroom api. At first we were surprised it wasn't returning the same type of errors. In some of our code we do remember to specially handle the http errors, but in other cases the http error is getting returned directly to other code, and was implicitly relying on this getting translated to codes.UNKNOWN by the status package.

When we found the apierror package we tried wrapping the http errors from Google Classroom so we could get access to the error details for debugging. But this caused many things to break because the status changed from UNKNOWN to OK.

It also seems like classroom is returning grpc statuses because we see the status code names in the error messages. It would be nice if we could access that directly because the grpc ones seem more specific, but the googleapi.Error only exposes the http status code, even if the response contains a grpc status.

@quartzmo
Copy link
Member

@ribrdb You're using google.golang.org/api/classroom/v1?

@ribrdb
Copy link
Author

ribrdb commented Feb 28, 2023

Yes.

I just want to mention what has been the most confusing thing for us. When we use the classroom api, we see errors that look a lot like grpc errors. For example, they say "permission denied" instead of "forbidden". But then we can't handle them using the concise if status.Code(err) == codes.PermissionDenied . Instead we have to remember to cast them to a googleapi.Error and check for http.StatusForbidden.
Or for example right now we're getting errors in the logs that say failed precondition. I don't even know how we're supposed to handle that in our go code because the http code 400 is used for multiple different errors. Maybe we have to look at the error details? It would certainly be a lot easier if status.Code just returned codes.FailedPrecondition.

quartzmo added a commit to quartzmo/gax-go that referenced this issue Mar 3, 2023
* return Unknown status when googleapi.Error because gRPC treats nil as OK

closes: googleapis#254
quartzmo added a commit that referenced this issue Mar 3, 2023
…260)

* return Unknown status when googleapi.Error because gRPC treats nil as OK

closes: #254
@quartzmo
Copy link
Member

quartzmo commented Mar 9, 2023

The fix in #260 has been released in 2.7.1.

@fsaintjacques
Copy link

We've been hit by this too, this becomes nasty when we make use of error unwrapping. Our code looks like this:

var res interface { GRPCStatus() *status.Status }
if errors.As(err, &res) {
  return res.GRPCStatus()
}

@noahdietz
Copy link
Contributor

noahdietz commented Apr 12, 2023

I think we might need to look into conditionally supporting the GRPCStatus interface i.e. removing the method from the APIError surface and embedding the APIError in a private type that implements that interface only when it is actually a gRPC Status related error. So we don't break existing errors.As|Is or status.FromError usage, but don't confuse HTTP errors for gRPC.

That said, users will always need to adjust their error handling based on when they are using HTTP or gRPC as a transport.

@noahdietz noahdietz reopened this Apr 12, 2023
@noahdietz
Copy link
Contributor

@codyoss wdyt?

@codyoss
Copy link
Member

codyoss commented Jul 5, 2023

I think this is better now with grpc 1.55: https://github.com/grpc/grpc-go/releases/tag/v1.55.0

"status: status.Code and status.FromError handle wrapped errors"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: p2 Moderately-important priority. Fix may not be included in next release. type: question Request for information or clarification. Not an issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants