用于gRPC中丰富错误处理的模式

时间:2018-02-12 14:29:11

标签: grpc

使用gRPC向客户端发送有关错误的更多详细信息的模式是什么?

例如,假设我有一个注册用户的表单,它发送一条消息

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

电子邮件必须格式正确且唯一,密码长度必须至少为8个字符。

如果我正在编写JSON API,我将使用以下正文返回400错误:

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

并且客户端可以向用户提供很好的错误消息(即通过突出显示密码字段并特别告诉用户他们输入的内容有问题)。

使用gRPC有没有办法做类似的事情?似乎在大多数客户端语言中,错误会导致抛出异常,无法获取响应。

例如,我喜欢像

这样的东西
message ValidationError {
  string field = 1;
  string message = 2;
}

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

或类似。

3 个答案:

答案 0 :(得分:18)

在响应元数据中包含其他错误详细信息。但是,仍然要确保提供有用的状态代码和消息。在这种情况下,您可以将RegisterUserResponse添加到元数据。

在gRPC Java中,它看起来像:

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));

另一种选择是使用google.rpc.Status proto,其中包含Any details。每种语言都支持处理该类型。在Java中,它看起来像:

// 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在某些语言中比较清晰,因为错误详细信息可以作为一个单元传递。它还清楚地表明响应的哪些部分与错误相关。在线,它仍然使用元数据传递附加信息。

您可能也对error_details.proto感兴趣,其中包含一些常见类型的错误。

CloudNativeCon期间我discussed this topic。您可以在YouTube上查看幻灯片和关联的录制内容。

答案 1 :(得分:1)

我们有3种不同的方式来处理gRPC中的错误。例如,假设gRPC服务器不接受大于20或小于2的值。

  1. 使用gRPC状态代码。

    if(number < 2 || number > 20){
         Status status = Status.FAILED_PRECONDITION.withDescription("Not between 2 and 20");
         responseObserver.onError(status.asRuntimeException());
     }
    
  2. 元数据(我们可以通过元数据传递对象)

    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));
     }
    
  3. 使用oneof-我们也可以使用oneof发送错误响应

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

客户端:

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;
}

在此处查看详细步骤-https://www.vinsguru.com/grpc-error-handling/

答案 2 :(得分:1)

正如@Eric Anderson 所提到的,您可以使用元数据来传递错误详细信息。元数据的问题在于它可以包含其他属性(例如 - 内容类型)。要处理该问题,您需要添加自定义逻辑来过滤错误元数据。

更简洁的方法是使用 google.rpc.Status proto(正如 Eric 提到的)。

如果您可以使用 yidongnan/grpc-spring-boot-starter 将您的 gRPC 服务器应用程序转换为 Spring Boot,那么您可以编写 @GrpcAdvice,类似于 Spring Boot @ControllerAdvice

@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);
  }
}

在客户端,您可以捕获此异常并将 registerUserResponse 解包为: 作为

} 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
 }

在我看来,如果您可以将 gRPC 服务器应用程序作为 Spring Boot 运行,这可能是一种更简洁的方法。

我一直在为类似的问题苦苦挣扎 - 所以我决定在 blog post

中编译所有内容