-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
Support error unwrapping (errors.As) #2934
Comments
For the time being, we have decided to not pursue any solution here until:
|
Hm, Ok I understand the concerns. Thanks for clearing this up. What would you recommend as a workaround for the time being? Add the unwrapping to the go-grpc-prometheus package untill the time comes? |
If you're interested in another viewpoint:
That seems like a random but helpful feature. I agree with waiting on this one until it's clearly worth the complexity.
I don't see why you would need to do that, and I think it is an anti-pattern.
I'm not really sure what that would accomplish? It seems like that would just reimplement
This would be my preference. Currently I'm planning to use interceptors to bring the I think this is the only option that is actually making anything new possible or impossible, the others are just helpers and don't change the actual functionality. |
Can you describe your use case here? What code is wrapping grpc status errors and why, and what code needs to access wrapped grpc status errors and why?
Apparently this option would cause a panic in Note that this restriction is entirely unnecessary, but it is there intentionally. It might be possible to lobby the Go team to remove it.
If we did this, we could implement
Adding
Unfortunately, this would be a breaking behavior change, so this option is probably completely off the table.
|
@dfawley The issue was that errors were wrapped using the github.com/pkg/errors package and the interceptor reported them as |
Yeah without making a breaking change, I'm not sure you need to make any code changes. It is trivial to fetch the status object (and associated info) manually: resp, err := runRPC(ctx, req)
var grpcstatus interface{ GRPCStatus() *status.Status }
var code codes.Code
if errors.As(err, &grpcStatus) {
code = grpcstatus.GRPCStatus().Code()
} If you add new functions or make any other changes, I think it will be confusing, because people might think GRPC is using those functions to retrieve the status. In other words, IMO, the An alternative could be to define a new interface for errors to implement. Something like I think the best way to address this without a breaking change would be to add interceptors to grpc_middleware that simplify this error conversion process (which for most casual users will be very confusing), and then document how to set them up in this package's docs. Similar to what @FUSAKLA did for that one interceptor. Maybe one interceptor for As a user I'd strongly prefer a breaking change here, especially because the breakage is easy to undo with an interceptor if people want to opt-out. When monitoring middleware get involved, keeping everything in agreement about how to fetch GRPC errors gets complicated. But I recognize breaking is not always possible. |
+1 on this feature request. Not sure how common my use case is, but would be nice to have this. I use the grpc-gateway as a gRPC client to expose my gRPC service as a REST API. The gateway allows you customize the HTTPError to return a custom HTTP response from the gateway based on the error. HTTPError has the following signature: func HTTPError(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) where the In my case, I would like to be able to do this to handle json unmarshaling errors that occurs in the gateway: func HTTPError(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
var jsonUnmarshalTypeErr *json.UnmarshalTypeError
if errors.As(err, &jsonUnmarshalTypeErr) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Payload invalid: type mismatch"))
}
...
} Just here to highlight my use case, thanks! 🙂 |
Where does that error come from? How does it get passed to |
Here's an example: The error is produced by a generated RPC function in the grpc-gateway, using func request_ABitOfEverythingService_CreateBody_0(ctx context.Context, marshaler runtime.Marshaler, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ABitOfEverything
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
// this returns a json error that is wrapped using status.Errorf
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.CreateBody(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
} This RPC call then gets invoked by the grpc-gateway server, returning the error from the above function (source). mux.Handle("POST", pattern_ABitOfEverythingService_CreateBody_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
// we are not able to unwrap this error and get the underlying cause
resp, md, err := request_ABitOfEverythingService_CreateBody_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_ABitOfEverythingService_CreateBody_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) So currently, when the error gets wrapped by On another note, I'm happy to help contribute to this feature once there's a clear direction on how this should be implemented (unless it's already done?). |
@jace-ys - this issue is about unwrapping errors to find grpc status errors within them. It sounds like what you're asking for is the ability to do the opposite: wrap arbitrary errors inside status errors. This is actually ~possible by using the error details, although admittedly not in a way that works transparently with Go's error wrapping. Unfortunately, we are not considering this kind of capability, because it would result in a status error that cannot be transmitted over the wire from a server to a client identically. (Errors are arbitrary types which may not be serializable.) |
I definitely think this feature would be helpful for gRPC clients. Say you have a gRPC client that is capable of making requests transactionally. If any of the requests return errors, it'd be nice to let the client wrap those errors so that it can provide more context about which request was executing when the server returned an error. Ex.
I'll probably use something similar to @FUSAKLA's approach here until this package supports some form of sustainable error wrapping, which I imagine should follow the Go community settling on an idiomatic approach to the problem. Probably a good idea to let the dust settle after the Go 1.13 change. @dfawley for reference. |
Based on what @mbyio mentioned above, another solution here could be: package status
type StatError interface { // needs a better name; Error already defined
error
GRPCStatus() *Status
} Usage: _, err := runRPC(req) // err has "Unwrap() error" returning a grpc status error
var unwrap status.StatError
if !errors.As(wrapped, &unwrap) {
t.Fatalf("unable tounwrap wrapped error %v", wrapped)
}
st, ok := status.FromError(unwrap)
// use st safely if ok This works more "naturally" with _, err := runRPC(req)
st, ok := status.Unwrap(err)
// use st safely if ok |
The status package provides high level functions for gRPC status and its error, so |
+1 on this. errors.Is() and errors.As() are becoming more and more common as the default way to handle error inspection, would be very useful to be able to pass a wrapped status error to status.FromError() or errors.As(). |
Use standard error wrapping/unwrapping instead. The conversion is mostly straightforward, but see grpc/grpc-go#2934 for GRPC `status.FromError`, it doesn't currently support unwrapping/errors.As(), so we're unwrapping manually here.
I use an interceptor to check for aborted/conflict grpc errors and implement an automatic retry. However I have a lot of code in the server itself that is doing wrapping at each level to implement a form of stack traces. Being able to unwrap an error would help me with this.
|
There was a PR for this which was declined and closed: #4091 I don't really understand the commentors comment about why it was declined. The argument was something like, if the handler is returning an error without attempting to transform it to a specific Status, then the author of the controller will be expecting this error to result in an Unknown status code. If later, a library decides to implement GRPCStatus() in order to return a different status code, then the controller author could be surprised that their codes are changing. But
But today, when they do that, they lose all the rich context information attached to the original error. If they are using an intercepter for transaction logging, that middleware will never get to see the original error object, and all that interesting context will be lost. Even if I use a custom error implementation which holds the original error, and implement GRPCStatus(), I can't then wrap that error any further, because grpc won't use After so many years of errors.Is() and errors.As() in the ecosystem, it's very surprising behavior that grpc doesn't use errors.As() to find GRPCStatus() implementors. |
I believe #6031 implemented this. |
Several options:
As
on*Status
so that users can do:Note that
*Status
does not implementerror
, so this may be considered an anti-pattern. Another concern is that it would not be discoverable - how would users know to do this? Possibly throughstatus
package documentation.statusError
, though there is no other reason to do so. (I don't like this.)status.Unwrap(err) *Status
function to do unwrapping. This seems pretty reasonable, though it should theoretically be unnecessary.status.FromError()
. My only minor concern with this is that it would unwrap automatically, which may not be expected by users.cc @jsm
The text was updated successfully, but these errors were encountered: