Как в gRPC передавать и обрабатывать структурированные (кастомные) ошибки?

Ответ

В gRPC, в отличие от стандартных ошибок Go, которые являются просто строками, ошибки представляют собой структурированные данные. Это позволяет передавать клиенту не только текстовое сообщение, но и машиночитаемые детали.

Для этого используется пакет google.golang.org/grpc/status.

На сервере:

  1. Создается статус с помощью status.New(), который принимает стандартный код ошибки (из google.golang.org/grpc/codes) и сообщение.
  2. К статусу можно добавить детали (details) — это любые Protobuf-сообщения. Часто используют предопределенные типы из google.golang.org/genproto/googleapis/rpc/errdetails.
  3. Из статуса получается ошибка с помощью метода st.Err().
import (
    "context"
    "google.golang.org/genproto/googleapis/rpc/errdetails"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (s *Server) SomeMethod(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    if req.Email == "" { // Пример валидации
        // 1. Создаем базовый статус
        st := status.New(codes.InvalidArgument, "invalid request: email is required")

        // 2. Создаем и добавляем детализированную информацию об ошибке
        br := &errdetails.BadRequest_FieldViolation{
            Field:       "email",
            Description: "email field cannot be empty",
        }
        stWithDetails, err := st.WithDetails(br)
        if err != nil {
            // Если детали не удалось сериализовать, возвращаем исходный статус
            return nil, st.Err()
        }

        // 3. Возвращаем ошибку с деталями
        return nil, stWithDetails.Err()
    }
    // ...
}

На клиенте:

  1. Полученная ошибка error преобразуется обратно в status.Status с помощью status.FromError().
  2. Из статуса извлекаются детали с помощью метода st.Details().
// ... после вызова RPC
if err != nil {
    // 1. Преобразуем ошибку в статус
    st, ok := status.FromError(err)
    if !ok {
        // Ошибка не является gRPC статусом, обрабатываем как обычную
        log.Printf("Unknown error: %v", err)
        return
    }

    log.Printf("gRPC Error: code = %s, message = %s", st.Code(), st.Message())

    // 2. Проверяем детали
    for _, detail := range st.Details() {
        switch t := detail.(type) {
        case *errdetails.BadRequest_FieldViolation:
            log.Printf("Bad Request Field: '%s' - %s", t.Field, t.Description)
        default:
            log.Printf("Unexpected detail type: %s", t)
        }
    }
}