【问题标题】:Pattern for rich error handling in gRPCgRPC 中丰富的错误处理模式
【发布时间】:2018-07-22 18:10:07
【问题描述】:

使用 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;
  ...
}

或类似的。

【问题讨论】:

    标签: grpc


    【解决方案1】:

    在响应元数据中包含其他错误详细信息。但是,仍然要确保提供有用的状态代码和消息。在这种情况下,您可以将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 上查看幻灯片和链接的录音。

    【讨论】:

    • 这篇博客文章为 Go 描述了这一点:jbrandhorst.com/post/grpc-errors 基本上,您可以使用 status.WithDetails() 函数使用额外的用户定义数据扩展标准状态。似乎与 Eric 的回答中为 Java 所做的相同。
    • 现在应该com.google.rpc.StatusProtoio.grpc.StatusProto 吗?或者,它们之间有什么区别,您什么时候想使用其中一个?
    • io.grpc.StatusProto 有实用方法来转换到com.google.rpc.Status 原型/从com.google.rpc.Status 原型转换。 com.google.rpc.Statusio.grpc.Status 有一个好处,它可以包含明显与错误相关的“细节”(而不是随便扔到元数据中)。如果您想将更多错误详细信息作为服务提供,com.google.rpc.Status 应该可以正常工作。
    • 对于任何想要使用通用原型的人,this 是 Maven 工件搜索。
    • @EricAnderson 如何将异常cause 添加到com.google.rpc.Status?似乎com.google.api.grpc:proto-google-common-protos:1.18.0 中也没有StatusProto.toStatusRuntimeException
    【解决方案2】:

    我们有 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/

    【讨论】:

    • 很遗憾 vinsguru.com 网站已关闭
    • 这是我的博客。我刚检查过。它似乎已启动并正在运行。
    【解决方案3】:

    正如@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

    【讨论】:

      猜你喜欢
      • 2021-01-13
      • 2021-04-26
      • 1970-01-01
      • 1970-01-01
      • 2011-04-04
      • 2011-08-29
      • 2012-05-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多