错误处理
使用 gRPC 状态码和自定义错误详情实现强大的错误处理
gRPC 提供了一个全面的错误处理系统,使用状态码、错误消息和可选的错误详情。正确的错误处理确保客户端能够适当地响应不同的故障场景。
gRPC 状态码
gRPC 使用标准状态码来指示 RPC 调用的结果:
error_service.proto
syntax = "proto3";package errors.v1;import "google/rpc/status.proto";import "google/rpc/error_details.proto";service ErrorDemoService {// 演示各种错误场景rpc ValidateUser(ValidateUserRequest) returns (ValidateUserResponse);// 可能返回速率限制错误rpc RateLimitedOperation(OperationRequest) returns (OperationResponse);// 可能返回资源耗尽错误rpc ResourceIntensiveOperation(ResourceRequest) returns (ResourceResponse);}message ValidateUserRequest {string user_id = 1;string email = 2;}message ValidateUserResponse {bool valid = 1;repeated ValidationError errors = 2;}message ValidationError {string field = 1;string message = 2;ErrorCode code = 3;}enum ErrorCode {ERROR_CODE_UNSPECIFIED = 0;ERROR_CODE_REQUIRED_FIELD = 1;ERROR_CODE_INVALID_FORMAT = 2;ERROR_CODE_DUPLICATE_VALUE = 3;ERROR_CODE_OUT_OF_RANGE = 4;}
标准错误处理
实现标准 gRPC 错误响应:
error_handling.py
import grpcfrom grpc import ServicerContextfrom google.rpc import status_pb2, error_details_pb2from google.protobuf import any_pb2class ErrorDemoServiceServicer(errors_pb2_grpc.ErrorDemoServiceServicer):def ValidateUser(self,request: errors_pb2.ValidateUserRequest,context: ServicerContext) -> errors_pb2.ValidateUserResponse:"""演示验证错误。"""validation_errors = []# 检查必填字段if not request.user_id:validation_errors.append(errors_pb2.ValidationError(field="user_id",message="User ID is required",code=errors_pb2.ERROR_CODE_REQUIRED_FIELD))if not request.email:validation_errors.append(errors_pb2.ValidationError(field="email",message="Email is required",code=errors_pb2.ERROR_CODE_REQUIRED_FIELD))# 验证邮箱格式if request.email and not self._is_valid_email(request.email):validation_errors.append(errors_pb2.ValidationError(field="email",message="Invalid email format",code=errors_pb2.ERROR_CODE_INVALID_FORMAT))# 检查用户是否重复if request.user_id and self.user_exists(request.user_id):validation_errors.append(errors_pb2.ValidationError(field="user_id",message="User ID already exists",code=errors_pb2.ERROR_CODE_DUPLICATE_VALUE))# 如果有验证错误则返回if validation_errors:context.set_code(grpc.StatusCode.INVALID_ARGUMENT)context.set_details("Validation failed")return errors_pb2.ValidateUserResponse(valid=False,errors=validation_errors)return errors_pb2.ValidateUserResponse(valid=True)def RateLimitedOperation(self,request: errors_pb2.OperationRequest,context: ServicerContext) -> errors_pb2.OperationResponse:"""演示速率限制错误。"""# 检查速率限制if not self.rate_limiter.is_allowed(request.client_id):# 创建详细的错误信息retry_info = error_details_pb2.RetryInfo()retry_info.retry_delay.seconds = 60 # 60 秒后重试quota_failure = error_details_pb2.QuotaFailure()violation = quota_failure.violations.add()violation.subject = f"client:{request.client_id}"violation.description = "API rate limit exceeded"# 设置带详情的错误context.set_code(grpc.StatusCode.RESOURCE_EXHAUSTED)context.set_details("Rate limit exceeded")# 添加错误详情(丰富的错误信息)self._add_error_details(context, [retry_info, quota_failure])return errors_pb2.OperationResponse()# 正常处理操作return self._process_operation(request)def ResourceIntensiveOperation(self,request: errors_pb2.ResourceRequest,context: ServicerContext) -> errors_pb2.ResourceResponse:"""演示资源耗尽错误。"""try:# 检查系统资源if not self.resource_manager.has_capacity():context.set_code(grpc.StatusCode.RESOURCE_EXHAUSTED)context.set_details("System overloaded, please try again later")return errors_pb2.ResourceResponse()# 处理密集型操作result = self._process_intensive_operation(request)return errors_pb2.ResourceResponse(result=result)except TimeoutError:context.set_code(grpc.StatusCode.DEADLINE_EXCEEDED)context.set_details("Operation timed out")return errors_pb2.ResourceResponse()except PermissionError:context.set_code(grpc.StatusCode.PERMISSION_DENIED)context.set_details("Insufficient permissions for this operation")return errors_pb2.ResourceResponse()except FileNotFoundError as e:context.set_code(grpc.StatusCode.NOT_FOUND)context.set_details(f"Required resource not found: {str(e)}")return errors_pb2.ResourceResponse()except Exception as e:context.set_code(grpc.StatusCode.INTERNAL)context.set_details(f"Internal server error: {str(e)}")return errors_pb2.ResourceResponse()def _add_error_details(self, context: ServicerContext, details):"""向 gRPC 响应添加丰富的错误详情。"""rich_status = status_pb2.Status()rich_status.code = context._state.code.value[0]rich_status.message = context._state.detailsfor detail in details:detail_any = any_pb2.Any()detail_any.Pack(detail)rich_status.details.append(detail_any)# 添加到尾随元数据context.set_trailing_metadata([('grpc-status-details-bin', rich_status.SerializeToString())])
自定义错误类型
为特定领域的错误定义自定义错误类型:
custom_errors.proto
syntax = "proto3";package errors.v1;// 自定义错误详情message BusinessLogicError {string error_code = 1;string error_message = 2;map<string, string> error_context = 3;repeated string suggested_actions = 4;}message ValidationFailure {repeated FieldError field_errors = 1;string global_error = 2;}message FieldError {string field_path = 1;string error_message = 2;string error_code = 3;google.protobuf.Any invalid_value = 4;}message ServiceUnavailableError {string service_name = 1;google.protobuf.Timestamp estimated_recovery_time = 2;repeated string alternative_endpoints = 3;}
自定义错误实现:
custom_errors.py
class CustomErrorHandler:@staticmethoddef business_logic_error(context: ServicerContext,error_code: str,message: str,error_context: dict = None,suggested_actions: list = None):"""创建业务逻辑错误。"""business_error = errors_pb2.BusinessLogicError(error_code=error_code,error_message=message,error_context=error_context or {},suggested_actions=suggested_actions or [])context.set_code(grpc.StatusCode.FAILED_PRECONDITION)context.set_details(message)# 添加自定义错误详情CustomErrorHandler._add_custom_details(context, business_error)@staticmethoddef validation_failure(context: ServicerContext,field_errors: list,global_error: str = None):"""创建验证失败错误。"""validation_error = errors_pb2.ValidationFailure(field_errors=field_errors,global_error=global_error or "")context.set_code(grpc.StatusCode.INVALID_ARGUMENT)context.set_details("Validation failed")CustomErrorHandler._add_custom_details(context, validation_error)@staticmethoddef service_unavailable(context: ServicerContext,service_name: str,estimated_recovery: datetime = None,alternatives: list = None):"""创建服务不可用错误。"""error = errors_pb2.ServiceUnavailableError(service_name=service_name,alternative_endpoints=alternatives or [])if estimated_recovery:error.estimated_recovery_time.FromDatetime(estimated_recovery)context.set_code(grpc.StatusCode.UNAVAILABLE)context.set_details(f"Service {service_name} is currently unavailable")CustomErrorHandler._add_custom_details(context, error)@staticmethoddef _add_custom_details(context: ServicerContext, error_detail):"""向响应添加自定义错误详情。"""detail_any = any_pb2.Any()detail_any.Pack(error_detail)rich_status = status_pb2.Status()rich_status.code = context._state.code.value[0]rich_status.message = context._state.detailsrich_status.details.append(detail_any)context.set_trailing_metadata([('grpc-status-details-bin', rich_status.SerializeToString())])# 使用示例class UserService(user_pb2_grpc.UserServiceServicer):def CreateUser(self, request, context):# 验证业务规则if self.user_repository.email_exists(request.email):CustomErrorHandler.business_logic_error(context,error_code="DUPLICATE_EMAIL",message="Email address is already registered",error_context={"email": request.email},suggested_actions=["Use a different email address","Reset password if you forgot your account"])return user_pb2.User()# 继续用户创建...
错误拦截器
使用拦截器实现全局错误处理:
error_interceptor.py
import loggingimport tracebackfrom grpc import ServicerContext, StatusCodeclass ErrorInterceptor(grpc.ServerInterceptor):def __init__(self, logger=None):self.logger = logger or logging.getLogger(__name__)def intercept_service(self, continuation, handler_call_details):def error_wrapper(behavior):def wrapper(request, context):try:return behavior(request, context)except ValueError as e:# 将 Python ValueError 转换为 gRPC INVALID_ARGUMENTcontext.set_code(StatusCode.INVALID_ARGUMENT)context.set_details(f"Invalid input: {str(e)}")self.logger.warning(f"Validation error in {handler_call_details.method}: {e}")return self._get_default_response(behavior)except PermissionError as e:context.set_code(StatusCode.PERMISSION_DENIED)context.set_details("Access denied")self.logger.warning(f"Permission denied in {handler_call_details.method}: {e}")return self._get_default_response(behavior)except TimeoutError as e:context.set_code(StatusCode.DEADLINE_EXCEEDED)context.set_details("Operation timed out")self.logger.error(f"Timeout in {handler_call_details.method}: {e}")return self._get_default_response(behavior)except Exception as e:# 记录未预期的错误self.logger.error(f"Unexpected error in {handler_call_details.method}: {e}\n"f"Traceback: {traceback.format_exc()}")context.set_code(StatusCode.INTERNAL)context.set_details("Internal server error")return self._get_default_response(behavior)return wrapperreturn grpc.unary_unary_rpc_method_handler(error_wrapper(continuation(handler_call_details).unary_unary))def _get_default_response(self, behavior):"""返回正确类型的空响应。"""# 这需要根据您的服务方法进行实现return None
客户端错误处理
在客户端处理错误:
client_error_handling.py
import grpcfrom google.rpc import status_pb2, error_details_pb2from google.protobuf import any_pb2def handle_grpc_errors(stub_method, request):"""gRPC 客户端调用的通用错误处理。"""try:response = stub_method(request)return response, Noneexcept grpc.RpcError as e:error_info = {'code': e.code(),'details': e.details(),'status': e.code().name}# 如果可用,提取丰富的错误详情metadata = dict(e.trailing_metadata())if 'grpc-status-details-bin' in metadata:try:status_detail = status_pb2.Status()status_detail.ParseFromString(metadata['grpc-status-details-bin'])error_info['rich_details'] = []for detail in status_detail.details:# 尝试解包常见错误类型if detail.Is(error_details_pb2.RetryInfo.DESCRIPTOR):retry_info = error_details_pb2.RetryInfo()detail.Unpack(retry_info)error_info['rich_details'].append({'type': 'retry_info','retry_delay_seconds': retry_info.retry_delay.seconds})elif detail.Is(error_details_pb2.QuotaFailure.DESCRIPTOR):quota_failure = error_details_pb2.QuotaFailure()detail.Unpack(quota_failure)error_info['rich_details'].append({'type': 'quota_failure','violations': [{'subject': v.subject,'description': v.description} for v in quota_failure.violations]})except Exception:# 如果无法解析丰富的详情,那也没关系passreturn None, error_info# 使用示例def create_user_with_error_handling(stub, user_data):"""创建用户并进行全面的错误处理。"""request = user_pb2.CreateUserRequest(**user_data)response, error = handle_grpc_errors(stub.CreateUser, request)if error:if error['code'] == grpc.StatusCode.INVALID_ARGUMENT:print(f"Validation failed: {error['details']}")return Noneelif error['code'] == grpc.StatusCode.ALREADY_EXISTS:print(f"User already exists: {error['details']}")return Noneelif error['code'] == grpc.StatusCode.RESOURCE_EXHAUSTED:# 检查重试信息for detail in error.get('rich_details', []):if detail['type'] == 'retry_info':retry_delay = detail['retry_delay_seconds']print(f"Rate limited. Retry after {retry_delay} seconds")return Noneprint("Resource exhausted")return Noneelse:print(f"Unexpected error: {error['status']} - {error['details']}")return Nonereturn response
错误响应模式
定义一致的错误响应模式:
error_responses.proto
syntax = "proto3";// 标准错误响应包装器message ErrorResponse {string error_code = 1;string error_message = 2;map<string, string> error_context = 3;repeated string suggestions = 4;google.protobuf.Timestamp timestamp = 5;string request_id = 6;}// 联合响应模式message CreateUserResult {oneof result {User success = 1;ErrorResponse error = 2;}}
gRPC 中正确的错误处理确保了强大、可维护的服务,为客户端提供关于出了什么问题以及如何修复的明确反馈。