From 5415d1835935708c4e5cc26782bd8925abcf922e Mon Sep 17 00:00:00 2001 From: Eno Compton Date: Wed, 21 Mar 2018 09:56:43 -0600 Subject: [PATCH] Add documentation and example of adding details to errors (#1915) --- Documentation/rpc-errors.md | 67 +++++++++++++++++++++++++ examples/rpc_errors/client/main.go | 62 +++++++++++++++++++++++ examples/rpc_errors/server/main.go | 80 ++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 Documentation/rpc-errors.md create mode 100644 examples/rpc_errors/client/main.go create mode 100644 examples/rpc_errors/server/main.go diff --git a/Documentation/rpc-errors.md b/Documentation/rpc-errors.md new file mode 100644 index 000000000..321a308c3 --- /dev/null +++ b/Documentation/rpc-errors.md @@ -0,0 +1,67 @@ +# RPC Errors + +All service method handlers should return `nil` or errors from the +`status.Status` type. Clients have direct access to the errors. + +Upon encountering an error, a gRPC server method handler should create a +`status.Status`. In typical usage, one would use [status.New][new-status] +passing in an appropriate [codes.Code][code] as well as a description of the +error to produce a `status.Status`. Calling [status.Err][status-err] converts +the `status.Status` type into an `error`. As a convenience method, there is also +[status.Error][status-error] which obviates the conversion step. Compare: + +``` +st := status.New(codes.NotFound, "some description") +err := st.Err() + +// vs. + +err := status.Error(codes.NotFound, "some description") +``` + +## Adding additional details to errors + +In some cases, it may be necessary to add details for a particular error on the +server side. The [status.WithDetails][with-details] method exists for this +purpose. Clients may then read those details by first converting the plain +`error` type back to a [status.Status][status] and then using +[status.Details][details]. + +## Example + +The [example][example] demonstrates the API discussed above and shows how to add +information about rate limits to the error message using `status.Status`. + +To run the example, first start the server: + +``` +$ go run examples/rpc_errors/server/main.go +``` + +In a separate sesssion, run the client: + +``` +$ go run examples/rpc_errors/client/main.go +``` + +On the first run of the client, all is well: + +``` +2018/03/12 19:39:33 Greeting: Hello world +``` + +Upon running the client a second time, the client exceeds the rate limit and +receives an error with details: + +``` +2018/03/19 16:42:01 Quota failure: violations: +exit status 1 +``` + +[status]: https://godoc.org/google.golang.org/grpc/status#Status +[new-status]: https://godoc.org/google.golang.org/grpc/status#New +[code]: https://godoc.org/google.golang.org/grpc/codes#Code +[with-details]: https://godoc.org/google.golang.org/grpc/status#Status.WithDetails +[details]: https://godoc.org/google.golang.org/grpc/status#Status.Details +[status-err]: https://godoc.org/google.golang.org/grpc/status#Status.Err +[example]: https://github.com/grpc/grpc-go/blob/master/examples/rpc_errors diff --git a/examples/rpc_errors/client/main.go b/examples/rpc_errors/client/main.go new file mode 100644 index 000000000..b50fa8ce9 --- /dev/null +++ b/examples/rpc_errors/client/main.go @@ -0,0 +1,62 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "log" + "os" + "time" + + "golang.org/x/net/context" + epb "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc" + pb "google.golang.org/grpc/examples/helloworld/helloworld" + "google.golang.org/grpc/status" +) + +func main() { + // Set up a connection to the server. + conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer func() { + if e := conn.Close(); e != nil { + log.Printf("failed to close connection: %s", e) + } + }() + c := pb.NewGreeterClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"}) + if err != nil { + s := status.Convert(err) + for _, d := range s.Details() { + switch info := d.(type) { + case *epb.QuotaFailure: + log.Printf("Quota failure: %s", info) + default: + log.Printf("Unexpected type: %s", info) + } + } + os.Exit(1) + } + log.Printf("Greeting: %s", r.Message) +} diff --git a/examples/rpc_errors/server/main.go b/examples/rpc_errors/server/main.go new file mode 100644 index 000000000..ced95d3ef --- /dev/null +++ b/examples/rpc_errors/server/main.go @@ -0,0 +1,80 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "fmt" + "log" + "net" + "sync" + + "golang.org/x/net/context" + epb "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + pb "google.golang.org/grpc/examples/helloworld/helloworld" + "google.golang.org/grpc/status" +) + +const ( + port = ":50051" +) + +// server is used to implement helloworld.GreeterServer. +type server struct { + mu sync.Mutex + count map[string]int +} + +// SayHello implements helloworld.GreeterServer +func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + s.mu.Lock() + defer s.mu.Unlock() + // Track the number of times the user has been greeted. + s.count[in.Name]++ + if s.count[in.Name] > 1 { + st := status.New(codes.ResourceExhausted, "Request limit exceeded.") + ds, err := st.WithDetails( + &epb.QuotaFailure{ + Violations: []*epb.QuotaFailure_Violation{{ + Subject: fmt.Sprintf("name:%s", in.Name), + Description: "Limit one greeting per person", + }}, + }, + ) + if err != nil { + return nil, st.Err() + } + return nil, ds.Err() + } + return &pb.HelloReply{Message: "Hello " + in.Name}, nil +} + +func main() { + log.Printf("server starting on port %s...", port) + lis, err := net.Listen("tcp", port) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + pb.RegisterGreeterServer(s, &server{count: make(map[string]int)}) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +}