gRPC 中丰富的错误处理模式
Pattern for rich error handling in gRPC
使用 gRPC 向客户端发送有关错误的更多详细信息的模式是什么?
例如,假设我有一个用于注册用户的表单,它发送一条消息
message RegisterUser {
string email = 1;
string password = 2;
}
其中电子邮件必须格式正确且唯一,密码长度必须至少为 8 个字符。
如果我正在写一个 JSON API,我会 return 一个 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;
...
}
或类似。
在响应元数据中包含其他错误详细信息。但是,仍然确保提供有用的状态代码和消息。在这种情况下,您可以将 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,其中包括 details
的附加 Any
。支持每种语言来处理类型。在 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 感兴趣。
我 discussed this topic 在 CloudNativeCon 期间。您可以在 YouTube 上查看幻灯片和链接的录音。
我们有 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/
如@Eric Anderson 所述,您可以使用元数据传递错误详细信息。元数据的问题在于它可以包含其他属性(例如 - 内容类型)。要处理这个问题,您需要添加自定义逻辑来过滤错误元数据。
一种更简洁的方法是使用 google.rpc.Status proto(正如 Eric 所提到的)。
如果你可以使用yidongnan/grpc-spring-boot-starter将你的gRPC服务器应用程序转换为spring启动,那么你可以写@GrpcAdvice
,类似于Spring启动@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 启动。
我一直在为类似的问题而苦苦挣扎 - 所以我决定将所有内容编译成 blog post
使用 gRPC 向客户端发送有关错误的更多详细信息的模式是什么?
例如,假设我有一个用于注册用户的表单,它发送一条消息
message RegisterUser {
string email = 1;
string password = 2;
}
其中电子邮件必须格式正确且唯一,密码长度必须至少为 8 个字符。
如果我正在写一个 JSON API,我会 return 一个 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;
...
}
或类似。
在响应元数据中包含其他错误详细信息。但是,仍然确保提供有用的状态代码和消息。在这种情况下,您可以将 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,其中包括 details
的附加 Any
。支持每种语言来处理类型。在 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 感兴趣。
我 discussed this topic 在 CloudNativeCon 期间。您可以在 YouTube 上查看幻灯片和链接的录音。
我们有 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/
如@Eric Anderson 所述,您可以使用元数据传递错误详细信息。元数据的问题在于它可以包含其他属性(例如 - 内容类型)。要处理这个问题,您需要添加自定义逻辑来过滤错误元数据。
一种更简洁的方法是使用 google.rpc.Status proto(正如 Eric 所提到的)。
如果你可以使用yidongnan/grpc-spring-boot-starter将你的gRPC服务器应用程序转换为spring启动,那么你可以写@GrpcAdvice
,类似于Spring启动@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 启动。
我一直在为类似的问题而苦苦挣扎 - 所以我决定将所有内容编译成 blog post