使用 gRPC 进行错误处理

作者:James Newton-King

本文讨论错误处理和 gRPC:

  • 使用 gRPC 状态代码和错误消息的内置错误处理功能。
  • 使用丰富的错误处理功能,发送复杂的结构化错误信息。

内置错误处理

gRPC 调用通过状态码来传达成功或失败的信息。 gRPC 调用成功完成后,服务器将状态返回到 OK 客户端。 如果发生错误,gRPC 将返回:

  • 一个错误状态代码,例如 CANCELLEDUNAVAILABLE
  • 可选的字符串错误消息。

通常用于进行错误处理的类型包括:

  • StatusCodegRPC 状态代码的枚举。 OK 信号成功;其他值是失败的。
  • Status:一个合并了 StatusCode 和可选字符串错误消息的 struct。 该错误消息会提供有关所发生情况的更多详细信息。
  • RpcException:具有 Status 值的异常类型。 此异常在 gRPC 服务器方法中引发,由 gRPC 客户端捕获。

内置错误处理仅支持状态代码和字符串说明。 若要将复杂的错误信息从服务器发送到客户端,请使用丰富的错误处理功能

引发服务器错误

gRPC 服务器调用始终会返回状态。 当一个方法成功完成时,服务器会自动返回 OK

public class GreeterService : GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" });
    }

    public override async Task SayHelloStreaming(HelloRequest request,
        IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
    {
        for (var i = 0; i < 5; i++)
        {
            await responseStream.WriteAsync(new HelloReply { Message = $"Hello {request.Name} {i}" });
            await Task.Delay(TimeSpan.FromSeconds(1));
        }
    }
}

前面的代码:

  • 实现一元 SayHello 方法,该方法在返回响应信息时成功完成。
  • 实现务器流式处理 SayHelloStreaming 方法,该方法完成后会成功完成。

服务器错误状态

gRPC 方法通过引发异常来返回错误状态代码。 在服务器上引发 RpcException 时,其状态代码和说明将返回到客户端:

public class GreeterService : GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        if (string.IsNullOrEmpty(request.Name))
        {
            throw new RpcException(new Status(StatusCode.InvalidArgument, "Name is required."));
        }
        return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" });
    }
}

引发的非 RpcException 异常类型也会导致调用失败,但会提供 UNKNOWN 状态代码和泛型消息 Exception was thrown by handler

Exception was thrown by handler 会发送到客户端,而不是异常消息,以防止暴露潜在的敏感信息。 若要在开发环境中查看更详细的错误消息,请配置 EnableDetailedErrors

处理客户端错误

当 gRPC 客户端进行调用时,在访问响应时会自动验证状态代码。 例如,等待一元 gRPC 调用会返回服务器在调用成功时发送的消息,并在失败时引发 RpcException。 捕获 RpcException 以处理客户端中的错误:

var client = new Greet.GreeterClient(channel);

try
{
    var response = await client.SayHelloAsync(new HelloRequest { Name = "World" });
    Console.WriteLine("Greeting: " + response.Message);
}
catch (RpcException ex)
{
    Console.WriteLine("Status code: " + ex.Status.StatusCode);
    Console.WriteLine("Message: " + ex.Status.Detail);
}

前面的代码:

  • SayHello 方法进行一元 gRPC 调用。
  • 如果成功,则将响应消息写入控制台。
  • 捕获 RpcException 并写出有关失败的错误详细信息。

错误方案

错误由具有错误状态代码和可选详细信息消息的 RpcException 表示。 在很多情况下都会引发 RpcException

  • 调用在服务器上失败,服务器发送了错误状态代码。 例如,gRPC 客户端启动了一个调用,该调用缺少请求消息中所需的数据,服务器会返回 INVALID_ARGUMENT 状态代码。
  • 发出 gRPC 调用时,客户端内发生错误。 例如,客户端发出 gRPC 调用,无法连接到服务器,并引发状态为 UNAVAILABLE 的错误。
  • 传递给 gRPC 调用的 CancellationToken 已取消。 gRPC 调用已停止,客户端将引发状态为 CANCELLED 的错误。
  • gRPC 调用超过其配置的截止时间。 gRPC 调用已停止,客户端将引发状态为 DEADLINE_EXCEEDED 的错误。

丰富的错误处理

丰富的错误处理功能允许使用错误消息发送复杂的结构化信息。 例如,验证返回无效字段名称和说明的列表的传入消息字段。 google.rpc.Status 错误模型 通常用于在 gRPC 应用之间发送复杂的错误信息。

.NET 上的 gRPC 支持使用 Grpc.StatusProto 包的丰富错误模型。 此包包含用于在服务器上创建丰富错误模型的方法,并由客户端读取它们。 丰富的错误模型基于 gRPC 的内置处理功能而构建,可以并行使用它们。

重要

错误包含在标头中,响应中的标头总数通常限制为 8 KB(8,192 字节)。 确保包含错误的标头不超过 8 KB。

在服务器上创建丰富错误

Google.Rpc.Status 创建丰富错误。 此类型为 不同Grpc.Core.Status

Google.Rpc.Status 具有状态、消息和详细信息字段。 最重要的字段是详细信息,它是 Any 值的重复字段。 详细信息是添加复杂有效负载的位置。

尽管任何消息类型都可以用作有效负载,但建议使用标准错误有效负载之一:

  • BadRequest
  • PreconditionFailure
  • ErrorInfo
  • ResourceInfo
  • QuotaFailure

Grpc.StatusProto 包括 ToRpcException,一个将 Google.Rpc.Status转换为错误的帮助程序方法。 从 gRPC 服务器方法引发错误:

public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        ArgumentNotNullOrEmpty(request.Name);

        return Task.FromResult(new HelloReply { Message = "Hello " + request.Name });
    }
    
    public static void ArgumentNotNullOrEmpty(string value, [CallerArgumentExpression(nameof(value))] string? paramName = null)
    {
        if (string.IsNullOrEmpty(value))
        {
            var status = new Google.Rpc.Status
            {
                Code = (int)Code.InvalidArgument,
                Message = "Bad request",
                Details =
                {
                    Any.Pack(new BadRequest
                    {
                        FieldViolations =
                        {
                            new BadRequest.Types.FieldViolation { Field = paramName, Description = "Value is null or empty" }
                        }
                    })
                }
            };
            throw status.ToRpcException();
        }
    }
}

读取客户端的丰富错误

从客户端中捕获的 RpcException 中读取丰富错误。 捕获异常并使用由 Grpc.StatusCode 提供的帮助程序方法,获取其 Google.Rpc.Status 实例:

var client = new Greet.GreeterClient(channel);

try
{
    var reply = await client.SayHelloAsync(new HelloRequest { Name = name });
    Console.WriteLine("Greeting: " + reply.Message);
}
catch (RpcException ex)
{
    Console.WriteLine($"Server error: {ex.Status.Detail}");
    var badRequest = ex.GetRpcStatus()?.GetDetail<BadRequest>();
    if (badRequest != null)
    {
        foreach (var fieldViolation in badRequest.FieldViolations)
        {
            Console.WriteLine($"Field: {fieldViolation.Field}");
            Console.WriteLine($"Description: {fieldViolation.Description}");
        }
    }
}

前面的代码:

  • 在捕获 RpcException 的 try/catch 内进行 gRPC 调用。
  • 调用 GetRpcStatus() 以尝试从异常中获取丰富错误模型。
  • 调用 GetDetail<BadRequest>() 以尝试从丰富错误获取 BadRequest 有效负载。

其他资源