Skip to content

Instantly share code, notes, and snippets.

@uzzz
Created May 6, 2021 11:43
Show Gist options
  • Save uzzz/90e7972867b598fac22a7048c82257b7 to your computer and use it in GitHub Desktop.
Save uzzz/90e7972867b598fac22a7048c82257b7 to your computer and use it in GitHub Desktop.
1. Errors in details
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string greeting = 1;
}
message Error {
string message = 1;
}
// server implementation
func (s *HelloService) SayHello(
ctx context.Context,
r *HelloRequest,
) (*HelloResponse, error) {
if r.Name == "" {
st := status.New(codes.InvalidArgument, "invalid argument")
st, err := st.WithDetails(&Error{Message: "name is required"})
if err != nil {
return nil, status.Error(codes.Internal, "internal error")
}
return nil, st.Err()
}
return &HelloResponse{Greeting: "Hello," + r.Name}, nil
}
// client implementation
resp, err := client.SayHello(ctx, &HelloRequest{Name: "Maksim"})
if err != nil {
st, ok := status.FromError(err)
if !ok {
log.Error("failed to say hello", "error", err)
}
switch st.Code() {
case codes.InvalidArgument:
for _, detail := range st.Details() {
er, ok := detail.(*Error)
if !ok {
log.Error("details is not error")
break
}
// display error
}
}
}
// ... deal with resp
Pros:
- Request & response proto definitions are clean and cover happy path
- We can write generic code, for example GRPC logging interceptor that logs error with details if code != OK
Cons:
- Details are untyped: can accidentaly put anything there
2. Errors as a part of response
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string greeting = 1;
repeated Error errors = 2;
}
message Error {
string message = 1;
}
// server implementation
func (s *HelloService) SayHello(
ctx context.Context,
r *HelloRequest,
) (*HelloResponse, error) {
if r.Name == "" {
return &HelloResponse{Errors: []*Error{Message: "name is required"}}, nil
}
return &HelloResponse{Greeting: "Hello," + r.Name}, nil
}
// client implementation
resp, err := client.SayHello(ctx, &HelloRequest{Name: "Maksim"})
if err != nil {
log.Error("failed to say hello", "error", err)
}
if len(resp.Errors) > 0 {
for _, err := range resp.Errors {
// display error
}
}
Pros:
- Errors are explicit and typed
- Less code
Cons:
- Message (HelloResponse) has two responsibilities - nothing stops user from setting actual data AND errors at the same time
- We don't utilize GRPC status codes, so can't use generic code (loggers, metrics handlers, etc)
2.1. Errors as a part of response using oneof
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
message HelloSuccessResponse {
string greeting = 1;
}
message HelloErrorResponse {
repeated Error errors = 1;
}
oneof response {
HelloSuccessResponse success_response = 1;
HelloErrorResponse error_response = 2;
}
}
message Error {
string message = 1;
}
This is a variant of of 2, but
Pro:
- We can't set success & error at the same time
Con:
- We need extra wrappers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment