Давайте работать с proto ошибками правильно :)
В программировании всегда есть несколько способов решения одной и той же проблемы. Но не все они одинаково эффективны. Сегодня поговорим о способах обработки ошибок в gRPC — удачных и не очень.
message Result {
oneof response {
error.v1.Error error = 1;
info.v1.Info info = 2;
}
}
На первый взгляд может показаться, что использование oneof
для представления либо ошибки, либо результата — это удобно. Однако такой подход вносит ненужную сложность в протокол обмена сообщениями и ухудшает читаемость кода. gRPC предоставляет встроенные средства для работы с ошибками, которые позволяют элегантно и эффективно передавать информацию об ошибках.
Почему же использование oneof
для ошибок — плохая идея? Во-первых, это затрудняет использование стандартного механизма ошибок gRPC и кодов состояний, которые предназначены для этой цели. Во-вторых, это может привести к путанице на стороне клиента при необходимости различать успешные ответы от ошибок.
На схеме видно, как обработка двух типов запроса утяжеляет логику клиента
Коды ошибок в gRPC
Коды ошибок в архитектуре gRPC особенно важны для эффективного взаимодействия между клиентом и сервером. Они помогают клиенту понять причину проблемы и правильно отреагировать на нее.
Правильное и эффективное управление ошибками в gRPC играет ключевую роль в создании надежных и поддерживаемых систем. Использование стандартных кодов ошибок и механизмов gRPC позволяет не только упростить обработку ошибок на стороне клиента, но и обеспечить ясность и предсказуемость поведения системы. Вместо того чтобы использовать конструкции вроде oneof
для обработки ошибок, лучше воспользоваться встроенными возможностями gRPC для передачи детализированной информации об ошибках.
Вот, как можно использовать gRPC-код codes.NotFound, чтобы сообщить об отсутствии чего-либо
import "google.golang.org/grpc/status"
import "google.golang.org/grpc/codes"
// ...
err := status.Error(codes.NotFound, "не нашел котика")
// ...
Такой подход упрощает обработку ошибок на стороне клиента, а это облегчает понимание структуры данных ответа. Кроме того, ошибки, возвращаемые через status.Error преобразуются в HTTP-статусы при транспортировке через gRPC-Gateway, в таком случае ошибки становятся понятными и вне gRPC
Но, что делать, если нам нужно больше гибкости в ответе ошибки? К примеру добавить дополнительную мета-инфу или же кастомные коды ошибок?
В самой системе gRPC есть возможность прикрепить доп. данные к ошибке — и таким способом расширить контекст проблемы
import (
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
// ...
st := status.New(codes.InvalidArgument, "invalid parameter")
// Общая форма ошибки
errInfo := &errdetails.ErrorInfo{
Reason: "Не хватает средств на счету",
Domain: "finance",
Metadata: map[string]string{
"my_meta_info": "my_meta_details",
},
}
st, err := st.WithDetails(errInfo)
if err != nil {
return fmt.Sprintf("st.WithDetails: %w", err)
}
return st.Err()
Но, в случаях, когда хочется получать более детализированные ошибки — например, с уточнением проблемного поля. В таком случае можно воспользоваться типом BadRequest и прописать больше деталей об ошибке.
Определение и использование кастомной ошибки
Но! Что делать, если стандартные варианты details не подходят? Мы можем сделать свои собственные типы ошибок! :)
Сначала определим кастомную ошибку в proto
файле. Нам нужно создать message
ошибки CustomErrorDetail. В ней будет содержаться информация об ошибках, связанных с пользовательскими данными:
syntax = "proto3";
package myerrors;
message CustomErrorDetail {
string reason = 1;
string field = 2;
string help = 3;
}
Теперь, когда у нас есть определение кастомной ошибки, мы можем использовать её, чтобы передавать более специфичную и детализированную информацию об ошибках. Это особенно полезно, когда требуется указать на конкретные поля или параметры, вызвавшие ошибку. Создание и использование такогоCustomErrorDetail
в коде сервера позволяет не только сообщать о проблемах, но и предоставлять клиенту рекомендации по их исправлению, что делает взаимодействие более прозрачным и эффективным.
import (
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/anypb"
"myerrors"
)
// ...
customErrorDetail := &myerrors.CustomErrorDetail{
Reason: "Value out of range",
Field: "age",
Help: "The age must be between 0 and 120",
}
st := status.New(codes.InvalidArgument, "invalid parameter")
st, err = st.WithDetails(customErrorDetail)
if err != nil {
return fmt.Sprintf("Unexpected error attaching custom error detail: %w", err)
}
return st.Err()
Работа с клиентской стороны
Теперь давайте рассмотрим, как клиентская сторона будет взаимодействовать с системой обработки ошибок в gRPC, которую мы описали ранее.
Обработка стандартных ошибок
Когда клиент получает ответ от gRPC сервера, он может проверять наличие ошибок с помощью стандартных механизмов gRPC, например:
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"log"
)н
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := NewYourServiceClient(conn)
response, err := client.YourMethod(context.Background(), &YourRequest{})
if err != nil {
st, ok := status.FromError(err)
if ok {
switch st.Code() {
case codes.InvalidArgument:
log.Println("Invalid argument error:", st.Message())
case codes.NotFound:
log.Println("Not found error:", st.Message())
// Обработайте другие коды ошибок по необходимости
default:
log.Println("Unexpected error:", st.Message())
}
} else {
log.Fatalf("failed to call YourMethod: %v", err)
}
} else {
log.Println("Response:", response)
}
}
Извлечение дополнительных деталей ошибок
А теперь самое интересное: чтобы клиентская сторона могла извлечь details для анализа, нам нужно эти самые details обработать.
Вот, как это можно сделать:
import (
"google.golang.org/grpc/status"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"myerrors"
"log"
)
// ...
func handleError(err error) {
st, ok := status.FromError(err)
if !ok {
log.Fatalf("An unexpected error occurred: %v", err)
}
for _, detail := range st.Details() {
switch t := detail.(type) {
case *errdetails.BadRequest:
// Обработка деталей неверного запроса
for _, violation := range t.GetFieldViolations() {
log.Printf("The field %s was wrong: %s\\\\n", violation.GetField(), violation.GetDescription())
}
case *myerrors.CustomErrorDetail:
// Обработка кастомных деталей ошибок
log.Printf("Custom error detail: Reason: %s, Field: %s, Help: %s\\\\n", t.Reason, t.Field, t.Help)
// Добавьте обработку других типов ошибок по необходимости
default:
log.Printf("Received an unknown error detail type: %v\\\\n", t)
}
}
}
Заключение
Мы рассмотрели, как использовать стандартные коды ошибок gRPC, как добавлять дополнительные данные к ошибкам и как создавать и обрабатывать кастомные ошибки. Эти подходы позволяют более гибко и детализированно подходить к обработке ошибок, что особенно важно для сложных систем, где простое сообщение об ошибке может быть недостаточным.
При проектировании API важно помнить, что клиентская сторона должна иметь возможность легко и однозначно интерпретировать ответы сервера. Использование стандартных механизмов gRPC для ошибок помогает достичь этой цели, улучшая взаимодействие между клиентом и сервером и делая систему в целом более устойчивой и понятной.