Error Handling in gRPC – gRPC中的错误处理

最后修改: 2021年 10月 4日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

gRPC is a platform to do inter-process Remote Procedure Calls (RPC). It’s highly performant and can run in any environment.

gRPC是一个用于进行进程间远程过程调用(RPC)的平台。它具有很高的性能,可以在任何环境中运行。

In this tutorial, we’ll focus on gRPC error handling using Java. gRPC has very low latency and high throughput, so it’s ideal to use in complex environments like microservice architectures. In these systems, it’s critical to have a good understanding of the state, performance, and failures of the different components of the network. Therefore, a good error handling implementation is critical to help us achieve the previous goals.

在本教程中,我们将重点介绍使用Java的gRPC错误处理。gRPC具有非常低的延迟和高的吞吐量,因此它非常适合在复杂的环境中使用,如微服务架构。在这些系统中,对网络中不同组件的状态、性能和故障有一个很好的了解是至关重要的。因此,一个好的错误处理实现对于帮助我们实现前面的目标至关重要。

2. Basics of Error Handling in gRPC

2.gRPC中错误处理的基础知识

Errors in gRPC are first-class entities, i.e., every call in gRPC is either a payload message or a status error message.

gRPC中的错误是第一类实体,也就是说,gRPC中的每个调用都是一个有效载荷信息或一个状态错误信息

The errors are codified in status messages and implemented across all supported languages.

这些错误被编入状态信息,并在所有支持的语言中实施

In general, we should not include errors in the response payload. To that end, always use StreamObserver::OnError, which internally adds the status error to the trailing headers. The only exception, as we’ll see below, is when we’re working with streams.

一般来说,我们不应该在响应的有效载荷中包含错误。为此,总是使用StreamObserver::OnError,它在内部将状态错误添加到尾部头文件中。唯一的例外,正如我们将在下面看到的,是当我们在使用流时。

All client or server gRPC libraries support the official gRPC error model. Java encapsulates this error model with the class io.grpc.Status. This class requires a standard error status code and an optional string error message to provide additional information. This error model has the advantage that it is supported independently of the data encoding used (protocol buffers, REST, etc.). However, it is pretty limited since we cannot include error details with the status.

所有客户端或服务器gRPC库都支持官方的gRPC错误模型。Java通过 io.grpc.Status类来封装该错误模型。该类需要一个标准的错误状态代码和一个可选的字符串错误信息以提供额外的信息。这种错误模型的优点是独立于所使用的数据编码(协议缓冲区、REST等)的支持。然而,它是相当有限的,因为我们不能在状态中包含错误细节。

If your gRPC application implements protocol buffers for data encoding, then you can use the richer error model for Google APIs. The com.google.rpc.Status class encapsulates this error model. This class provides com.google.rpc.Code values, an error message, and additional error details are appended as protobuf messages. Additionally, we can utilize a predefined set of protobuf error messages, defined in error_details.proto that cover the most common cases.  In the package com.google.rpc we have the classes: RetryInfo, DebugInfo, QuotaFailure, ErrorInfo, PrecondicionFailure, BadRequest, RequestInfo, ResourceInfo, and Help that encapsulate all the error messages in error_details.proto.

如果您的gRPC应用程序实现了用于数据编码的协议缓冲区,那么您可以使用更丰富的Google APIs的错误模型com.google.rpc.Status类封装了这种错误模型。该类提供了com.google.rpc.Code值、错误信息和额外的错误细节被追加为protobuf信息。此外,我们可以利用一组预定义的protobuf错误信息,定义在error_details.proto,涵盖了最常见的情况。 在包com.google.rpc中,我们有几个类。RetryInfo, DebugInfo, QuotaFailure, ErrorInfo, PrecondicionFailure, BadRequest, RequestInfo, ResourceInfo,Help 封装了所有错误信息在error_details.proto

In addition to the two error models, we can define custom error messages that can be added as key-value pairs to the RPC metadata.

除了这两种错误模型之外,我们还可以定义自定义的错误信息,可以作为键值对添加到RPC元数据中

We’re going to write a very simple application to show how to use these error models with a pricing service where the client sends commodity names, and the server provides pricing values.

我们将编写一个非常简单的应用程序,以展示如何使用这些错误模型与定价服务,其中客户发送商品名称,而服务器提供定价值。

3. Unary RPC Calls

3.单一的RPC调用

Let’s start considering the following service interface defined in commodity_price.proto:

让我们开始考虑以下定义在commodity_price.proto中的服务接口。

service CommodityPriceProvider {
    rpc getBestCommodityPrice(Commodity) returns (CommodityQuote) {}
}

message Commodity {
    string access_token = 1;
    string commodity_name = 2;
}

message CommodityQuote {
    string commodity_name = 1;
    string producer_name = 2;
    double price = 3;
}

message ErrorResponse {
    string commodity_name = 1;
    string access_token = 2;
    string expected_token = 3;
    string expected_value = 4;
}

The input of the service is a Commodity message. In the request, the client has to provide an access_token and a commodity_name.

该服务的输入是一个Commodity消息。在请求中,客户必须提供一个access_token和一个commodity_name

The server responds synchronously with a CommodityQuote that states the comodity_name, producer_name, and the associated price for the Commodity.

服务器同步响应一个CommodityQuote,说明comodity_nameproducer_name,Commodity的相关price

For illustration purposes, we also define a custom ErrorResponse.  This is an example of a custom error message that we’ll send to the client as metadata.

为了说明问题,我们还定义了一个自定义的ErrorResponse。 这是一个自定义错误信息的例子,我们将把它作为元数据发送给客户端。

3.1. Response Using io.grpc.Status

3.1.使用io.grpc.Status的响应

In the server’s service call, we check the request for a valid Commodity:

在服务器的服务调用中,我们检查请求是否有效Commodity

public void getBestCommodityPrice(Commodity request, StreamObserver<CommodityQuote> responseObserver) {

    if (commodityLookupBasePrice.get(request.getCommodityName()) == null) {
 
        Metadata.Key<ErrorResponse> errorResponseKey = ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance());
        ErrorResponse errorResponse = ErrorResponse.newBuilder()
          .setCommodityName(request.getCommodityName())
          .setAccessToken(request.getAccessToken())
          .setExpectedValue("Only Commodity1, Commodity2 are supported")
          .build();
        Metadata metadata = new Metadata();
        metadata.put(errorResponseKey, errorResponse);
        responseObserver.onError(io.grpc.Status.INVALID_ARGUMENT.withDescription("The commodity is not supported")
          .asRuntimeException(metadata));
    } 
    // ...
}

In this simple example, we return an error if the Commodity doesn’t exist in the commodityLookupBasePrice HashTable.

在这个简单的例子中,如果Commodity不存在于commodityLookupBasePrice HashTable中,我们会返回一个错误。

First, we build a custom ErrorResponse and create a key-value pair which we add to the metadata in metadata.put(errorResponseKey, errorResponse).

首先,我们建立一个自定义的ErrorResponse,并创建一个键值对,在metadata.put(errorResponseKey, errorResponse)中添加到元数据。

We use io.grpc.Status to specify the error status. The function responseObserver::onError takes a Throwable as a parameter, so we use asRuntimeException(metadata) to convert the Status into a Throwable. asRuntimeException can optionally take a Metadata parameter (in our case, an ErrorResponse key-value pair), which adds to the trailers of the message.

我们使用io.grpc.Status来指定错误状态。函数responseObserver::onError需要一个Throwable作为参数,所以我们使用asRuntimeException(metadata) 来将Status转换成ThrowableasRuntimeException可以选择接受一个Metadata参数(在我们的例子中,是一个ErrorResponse键值对),它增加了消息的线索。

If the client makes an invalid request, it will get back an exception:

如果客户端提出一个无效的请求,它将得到一个异常的反馈。

@Test
public void whenUsingInvalidCommodityName_thenReturnExceptionIoRpcStatus() throws Exception {
 
    Commodity request = Commodity.newBuilder()
      .setAccessToken("123validToken")
      .setCommodityName("Commodity5")
      .build();

    StatusRuntimeException thrown = Assertions.assertThrows(StatusRuntimeException.class, () -> blockingStub.getBestCommodityPrice(request));

    assertEquals("INVALID_ARGUMENT", thrown.getStatus().getCode().toString());
    assertEquals("INVALID_ARGUMENT: The commodity is not supported", thrown.getMessage());
    Metadata metadata = Status.trailersFromThrowable(thrown);
    ErrorResponse errorResponse = metadata.get(ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance()));
    assertEquals("Commodity5",errorResponse.getCommodityName());
    assertEquals("123validToken", errorResponse.getAccessToken());
    assertEquals("Only Commodity1, Commodity2 are supported", errorResponse.getExpectedValue());
}

The call to blockingStub::getBestCommodityPrice throws a StatusRuntimeExeption since the request has an invalid commodity name.

blockingStub::getBestCommodityPrice的调用抛出了StatusRuntimeExeption,因为请求有一个无效的商品名称。

We use Status::trailerFromThrowable to access the metadata. ProtoUtils::keyForProto gives us the metadata key of ErrorResponse.

我们使用Status::trailerFromThrowable来访问元数据。ProtoUtils::keyForProto给了我们ErrorResponse的元数据键。

3.2. Response Using com.google.rpc.Status

3.2.使用com.google.rpc.Status的响应

Let’s consider the following server code example:

让我们考虑一下下面的服务器代码例子。

public void getBestCommodityPrice(Commodity request, StreamObserver<CommodityQuote> responseObserver) {
    // ...
    if (request.getAccessToken().equals("123validToken") == false) {

        com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
          .setCode(com.google.rpc.Code.NOT_FOUND.getNumber())
          .setMessage("The access token not found")
          .addDetails(Any.pack(ErrorInfo.newBuilder()
            .setReason("Invalid Token")
            .setDomain("com.baeldung.grpc.errorhandling")
            .putMetadata("insertToken", "123validToken")
            .build()))
          .build();
        responseObserver.onError(StatusProto.toStatusRuntimeException(status));
    }
    // ...
}

In the implementation, getBestCommodityPrice returns an error if the request doesn’t have a valid token.

在实现中,getBestCommodityPrice如果请求没有一个有效的标记,则返回一个错误。

Moreover, we set the status code, message, and details to com.google.rpc.Status.

此外,我们将状态代码、消息和细节设置为com.google.rpc.Status

In this example, we’re using the predefined com.google.rpc.ErrorInfo instead of our custom ErrorDetails (although we could have used both if needed). We serialize ErrorInfo using Any::pack().

在这个例子中,我们使用预定义的com.google.rpc.ErrorInfo,而不是我们自定义的ErrorDetails(尽管如果需要的话,我们可以同时使用两者)。我们使用Any::pack()来序列化ErrorInfo

The class StatusProto::toStatusRuntimeException converts the com.google.rpc.Status into a Throwable.

StatusProto::toStatusRuntimeExceptioncom.google.rpc.Status转换为一个Throwable

In principle, we could also add other messages defined in error_details.proto to further customized the response.

原则上,我们也可以添加在error_details.proto中定义的其他信息,以进一步定制响应。

The client implementation is straightforward:

客户端的实现是直截了当的。

@Test
public void whenUsingInvalidRequestToken_thenReturnExceptionGoogleRPCStatus() throws Exception {
 
    Commodity request = Commodity.newBuilder()
      .setAccessToken("invalidToken")
      .setCommodityName("Commodity1")
      .build();

    StatusRuntimeException thrown = Assertions.assertThrows(StatusRuntimeException.class,
      () -> blockingStub.getBestCommodityPrice(request));
    com.google.rpc.Status status = StatusProto.fromThrowable(thrown);
    assertNotNull(status);
    assertEquals("NOT_FOUND", Code.forNumber(status.getCode()).toString());
    assertEquals("The access token not found", status.getMessage());
    for (Any any : status.getDetailsList()) {
        if (any.is(ErrorInfo.class)) {
            ErrorInfo errorInfo = any.unpack(ErrorInfo.class);
            assertEquals("Invalid Token", errorInfo.getReason());
            assertEquals("com.baeldung.grpc.errorhandling", errorInfo.getDomain());
            assertEquals("123validToken", errorInfo.getMetadataMap().get("insertToken"));
        }
    }
}

StatusProto.fromThrowable is a utility method to get the com.google.rpc.Status directly from the exception.

StatusProto.fromThrowable是一个实用方法,可以直接从异常中获取com.google.rpc.Status

From status::getDetailsList we get the com.google.rpc.ErrorInfo details.

status::getDetailsList我们得到com.google.rpc.ErrorInfo的详细信息。

4. Errors with gRPC Streams

4.gRPC流的错误

gRPC streams allow servers and clients to send multiple messages in a single RPC call.

gRPC流允许服务器和客户端在单个RPC调用中发送多个消息。

In terms of error propagation, the approach that we have used so far is not valid with gRPC streams. The reason is that onError() has to be the last method invoked in the RPC because, after this call, the framework severs the communication between the client and server.

在错误传播方面,到目前为止我们所使用的方法对gRPC流是无效的原因是onError()必须是RPC中最后调用的方法,因为在这个调用之后,框架会切断客户端和服务器之间的通信。

When we’re using streams, this is not the desired behavior. Instead, we want to keep the connection open to respond to other messages that might come through the RPC.

当我们使用流时,这不是我们想要的行为。相反,我们希望保持连接开放,以响应可能通过RPC而来的其他消息

A good solution to this problem is to add the error to the message itself, as we show in commodity_price.proto:

一个好的解决方案是将错误添加到消息本身,正如我们在commodity_price.proto中展示的那样。

service CommodityPriceProvider {
  
    rpc getBestCommodityPrice(Commodity) returns (CommodityQuote) {}
  
    rpc bidirectionalListOfPrices(stream Commodity) returns (stream StreamingCommodityQuote) {}
}

message Commodity {
    string access_token = 1;
    string commodity_name = 2;
}

message StreamingCommodityQuote{
    oneof message{
        CommodityQuote comodity_quote = 1;
        google.rpc.Status status = 2;
   }   
}

The function bidirectionalListOfPrices returns a StreamingCommodityQuote. This message has the oneof keyword that signals that it can use either a CommodityQuote or a google.rpc.Status.

函数bidirectionalListOfPrices返回一个StreamingCommodityQuote。这个消息有oneof关键字,表明它可以使用CommodityQuotegoogle.rpc.Status

In the following example, if the client sends an invalid token, the server adds a status error to the body of the response:

在下面的例子中,如果客户端发送了一个无效的令牌,服务器会在响应的正文中添加一个状态错误。

public StreamObserver<Commodity> bidirectionalListOfPrices(StreamObserver<StreamingCommodityQuote> responseObserver) {

    return new StreamObserver<Commodity>() {
        @Override
        public void onNext(Commodity request) {

            if (request.getAccessToken().equals("123validToken") == false) {

                com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
                  .setCode(Code.NOT_FOUND.getNumber())
                  .setMessage("The access token not found")
                  .addDetails(Any.pack(ErrorInfo.newBuilder()
                    .setReason("Invalid Token")
                    .setDomain("com.baeldung.grpc.errorhandling")
                    .putMetadata("insertToken", "123validToken")
                    .build()))
                  .build();
                StreamingCommodityQuote streamingCommodityQuote = StreamingCommodityQuote.newBuilder()
                  .setStatus(status)
                  .build();
                responseObserver.onNext(streamingCommodityQuote);
            }
            // ...
        }
    }
}

The code creates an instance of com.google.rpc.Status and adds it to the StreamingCommodityQuote response message. It does not invoke onError(), so the framework does not interrupt the connection with the client.

该代码创建了一个com.google.rpc.Status的实例,并将其添加到StreamingCommodityQuote响应消息中。它没有调用onError(),所以框架没有中断与客户端的连接。

Let’s look at the client implementation:

我们来看看客户端的实现。

public void onNext(StreamingCommodityQuote streamingCommodityQuote) {

    switch (streamingCommodityQuote.getMessageCase()) {
        case COMODITY_QUOTE:
            CommodityQuote commodityQuote = streamingCommodityQuote.getComodityQuote();
            logger.info("RESPONSE producer:" + commodityQuote.getCommodityName() + " price:" + commodityQuote.getPrice());
            break;
        case STATUS:
            com.google.rpc.Status status = streamingCommodityQuote.getStatus();
            logger.info("Status code:" + Code.forNumber(status.getCode()));
            logger.info("Status message:" + status.getMessage());
            for (Any any : status.getDetailsList()) {
                if (any.is(ErrorInfo.class)) {
                    ErrorInfo errorInfo;
                    try {
                        errorInfo = any.unpack(ErrorInfo.class);
                        logger.info("Reason:" + errorInfo.getReason());
                        logger.info("Domain:" + errorInfo.getDomain());
                        logger.info("Insert Token:" + errorInfo.getMetadataMap().get("insertToken"));
                    } catch (InvalidProtocolBufferException e) {
                        logger.error(e.getMessage());
                    }
                }
            }
            break;
        // ...
    }
}

The client gets the returned message in onNext(StreamingCommodityQuote) and uses a switch statement to distinguish between a CommodityQuote or a com.google.rpc.Status.

客户端 onNext(StreamingCommodityQuote)中获取返回的消息,并且 使用switch语句来区分CommodityQuotecom.google.rpc.Status

5. Conclusion

5.总结

In this tutorial, we have shown how to implement error handling in gRPC for unary and stream-based RPC calls.

在本教程中,我们展示了如何在gRPC中实现对基于单数和流的RPC调用的错误处理

gRPC is a great framework to use for remote communications in distributed systems. In these systems, it’s important to have a very robust error handling implementation to help to monitor the system. This is even more critical in complex architectures like microservices.

gRPC是一个很好的框架,可用于分布式系统的远程通信。在这些系统中,有一个非常强大的错误处理实现来帮助监控系统是非常重要的。这在微服务等复杂的架构中更为关键。

The source code of the examples can be found over on GitHub.

这些例子的源代码可以在GitHub上找到