Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern for rich error handling in gRPC

Tags:

grpc

What is the pattern for sending more details about errors to the client using gRPC?

For example, suppose I have a form for registering a user, that sends a message

message RegisterUser {
  string email = 1;
  string password = 2;
}

where the email has to be properly formatted and unique, and the password must be at least 8 characters long.

If I was writing a JSON API, I'd return a 400 error with the following body:

{
  "errors": [{
    "field": "email",
    "message": "Email does not have proper format."
   }, {
     "field": "password",
     "message": "Password must be at least 8 characters."
   }],
}

and the client could provide nice error messages to the user (i.e. by highlighting the password field and specifically telling the user that there's something wrong with their input to it).

With gRPC is there a way to do something similar? It seems that in most client languages, an error results in an exception being thrown, with no way to grab the response.

For example, I'd like something like

message ValidationError {
  string field = 1;
  string message = 2;
}

message RegisterUserResponse {
  repeated ValidationError validation_errors = 1;
  ...
}

or similar.

like image 924
mindvirus Avatar asked Feb 12 '18 14:02

mindvirus


3 Answers

Include additional error details in the response Metadata. However, still make sure to provide a useful status code and message. In this case, you can add RegisterUserResponse to the Metadata.

In gRPC Java, that would look like:

Metadata.Key<RegisterUserResponse> REGISTER_USER_RESPONSE_KEY =
    ProtoUtils.keyForProto(RegisterUserResponse.getDefaultInstance());
...
Metadata metadata = new Metadata();
metadata.put(REGISTER_USER_RESPONSE_KEY, registerUserResponse);
responseObserver.onError(
    Status.INVALID_ARGUMENT.withDescription("Email or password malformed")
      .asRuntimeException(metadata));

Another option is to use the google.rpc.Status proto which includes an additional Any for details. Support is coming to each language to handle the type. In Java, it'd look like:

// This is com.google.rpc.Status, not io.grpc.Status
Status status = Status.newBuilder()
    .setCode(Code.INVALID_ARGUMENT.getNumber())
    .setMessage("Email or password malformed")
    .addDetails(Any.pack(registerUserResponse))
    .build();
responseObserver.onError(StatusProto.toStatusRuntimeException(status));

google.rpc.Status is cleaner in some languages as the error details can be passed around as one unit. It also makes it clear what parts of the response are error-related. On-the-wire, it still uses Metadata to pass the additional information.

You may also be interested in error_details.proto which contains some common types of errors.

I discussed this topic during CloudNativeCon. You can check out the slides and linked recording on YouTube.

like image 75
Eric Anderson Avatar answered Oct 19 '22 04:10

Eric Anderson


We have 3 different ways we could handle the errors in gRPC. For example lets assume the gRPC server does not accept values above 20 or below 2.

Option 1: Using gRPC status codes.

   if(number < 2 || number > 20){
        Status status = Status.FAILED_PRECONDITION.withDescription("Not between 2 and 20");
        responseObserver.onError(status.asRuntimeException());
    }

Option 2: Metadata (we can pass objects via metadata)

   if(number < 2 || number > 20){
        Metadata metadata = new Metadata();
        Metadata.Key<ErrorResponse> responseKey = ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance());
        ErrorCode errorCode = number > 20 ? ErrorCode.ABOVE_20 : ErrorCode.BELOW_2;
        ErrorResponse errorResponse = ErrorResponse.newBuilder()
                .setErrorCode(errorCode)
                .setInput(number)
                .build();
        // pass the error object via metadata
        metadata.put(responseKey, errorResponse);
        responseObserver.onError(Status.FAILED_PRECONDITION.asRuntimeException(metadata));
    }

Option 3: Using oneof - we can also use oneof to send error response

oneof response {
    SuccessResponse success_response = 1;
    ErrorResponse error_response = 2;
  }
}

client side:

switch (response.getResponseCase()){
    case SUCCESS_RESPONSE:
        System.out.println("Success Response : " + response.getSuccessResponse().getResult());
        break;
    case ERROR_RESPONSE:
        System.out.println("Error Response : " + response.getErrorResponse().getErrorCode());
        break;
}

Check here for the detailed steps - https://www.vinsguru.com/grpc-error-handling/

like image 14
vins Avatar answered Oct 19 '22 03:10

vins


As mentioned by @Eric Anderson, you can use metadata to pass error detail. The problem with metadata is that it can contain, other attributes (example - content-type). To handle that you need to add custom logic to filter error metadata.

A much cleaner approach is of using google.rpc.Status proto (as Eric has mentioned).

If you can convert your gRPC server application to spring boot using yidongnan/grpc-spring-boot-starter, then you can write @GrpcAdvice, similar to Spring Boot @ControllerAdvice as

@GrpcAdvice
public class ExceptionHandler {

  @GrpcExceptionHandler(ValidationErrorException.class)
  public StatusRuntimeException handleValidationError(ValidationErrorException cause) {

    List<ValidationError> validationErrors = cause.getValidationErrors();

    RegisterUserResponse registerUserResponse =
        RegisterUserResponse.newBuilder()
            .addAllValidationErrors(validationErrors)
            .build();


    var status =
        com.google.rpc.Status.newBuilder()
            .setCode(Code.INVALID_ARGUMENT.getNumber())
            .setMessage("Email or password malformed")
            .addDetails(Any.pack(registerUserResponse))
            .build();

    return StatusProto.toStatusRuntimeException(status);
  }
}

On the client-side, you can catch this exception and unpack the registerUserResponse as: as

} catch (StatusRuntimeException error) {
   com.google.rpc.Status status = io.grpc.protobuf.StatusProto.fromThrowable(error);
   RegisterUserResponse registerUserResponse = null;
   for (Any any : status.getDetailsList()) {
     if (!any.is(RegisterUserResponse.class)) {
       continue;
     }
     registerUserResponse = any.unpack(ErrorInfo.class);
   }
   log.info(" Error while calling product service, reason {} ", registerUserResponse.getValidationErrorsList());
   //Other action
 }

In my opinion, this can be a much cleaner approach provided you can run your gRPC server application as Spring Boot.

I was struggling with similar questions - so I decided to compile everything in a blog post

like image 1
Pankaj Avatar answered Oct 19 '22 05:10

Pankaj