Обработка ошибок Golang gRPC
Наиболее распространенным способом обработки ошибок в gRPC является прямой возврат ошибки, например, return nil, err
, но на практике также у нас есть коды бизнес-статуса для возврата, и распространенным способом является определение кода ошибки в возвращаемой структуре, но очень громоздко для записи, например, вам придется написать это так.
user, err := dao.GetUserByEmail(ctx, email)
if err != nil {
if err == gorm.RecordNotFound {
return &GetUserResp{Code: USER_NOT_FOUND, Msg: "user not found"}, nil
}
return nil, err
}
Здесь присутствует несколько проблем:
- возвращающие ошибки - это боль при написании, потому что вам нужно каждый раз определять ошибку, а затем преобразовывать ее в соответствующий код ошибки в полях Code, Msg.
- если вы вернете
err
напрямую, вместо самоопределяемых grpccodes.NotFound
, ошибка не может быть распознана в клиенте. - если вы используете шлюз, любая ошибка, которая не является пользовательской ошибкой grpc, будет указана как 500.
Например, для проблемы 1 мы можем вернуть err
напрямую, но это приведет к проблеме 2; для проблемы 2 мы можем использовать 1, но это проблематично для записи; для проблемы 3 мы можем использовать встроенную ошибку grpc, но ее выразительность очень ограничена, и она не может передавать коды бизнес-ошибок.
Поэтому, чтобы решить этот ряд проблем, после сравнения нескольких библиотек обработки ошибок, мы собрали набор систем обработки ошибок, которые сочетают в себе их преимущества при адаптации к бизнес-требованиям.
Библиотека обработки ошибок
Система исключений Python - очень стоящая разработка. Во-первых, мы разделяем ненормальное выполнение программы на ошибки, которые мы хотим проверить и обработать, и исключения, от которых мы можем избавиться только с помощью recover
попыток.
Сначала мы разделим ошибки на типы ошибок и экземпляры ошибок. При определении ошибки мы определяем тип ошибки, который содержит код состояния HTTP и код бизнес-ошибки, который он должен отображать. Когда выдается ошибка, то есть когда создается экземпляр ошибки, он несет информацию о стеке, информацию о выполнении и др. ошибки.
Например, ошибка определения.
ErrBadRequest = RegisterErrorType(BaseErr, http.StatusBadRequest, ErrCodeBadRequest) // 400
ErrUnauthorized = RegisterErrorType(BaseErr, http.StatusUnauthorized, ErrCodeUnauthorized) // 401
ErrPaymentRequired = RegisterErrorType(BaseErr, http.StatusPaymentRequired, ErrCodePaymentRequired) // 402
ErrForbidden = RegisterErrorType(BaseErr, http.StatusForbidden, ErrCodeForbidden) // 403
ErrNotFound = RegisterErrorType(BaseErr, http.StatusNotFound, ErrCodeNotFound) // 404
ErrMethodNotAllowed = RegisterErrorType(BaseErr, http.StatusMethodNotAllowed, ErrCodeMethodNotAllowed) // 405
Ошибка создания экземпляра.
err = validateReq(req)
if err != nil {
return nil, errs.NewBadRequest(err.Error(), err)
}
Тип ошибки обнаружения.
if errs.IsError(err, ErrBadRequest) {
//
}
Ошибка извлечения.
if baseErr, ok := errs.AsBaseErr(err); ok {
//
}
С приведенным выше набором библиотек ошибок мы можем переносить информацию о стеке ошибок, типах ошибок, бизнес-кодах ошибок, кодах состояния HTTP-ошибок, сообщения об ошибках, мета-ошибках, которые вызывают возникновение ошибок, а также выполнять определение типа и извлечение информации. Итак, как это работает с gRPC?
Обработка ошибок
Как уже упоминалось выше, если мы используем return nil, err
напрямую, клиент не сможет точно его распознать, а если мы используем return Resp{Code, Msg}, nil
, это громоздко для записи, и шлюз gRPC не может точно преобразовать его в соответствующий код состояния HTTP.
Наше решение состоит в том, чтобы напрямую вернуть систему ошибок, описанную в предыдущем разделе, например:
func (s *service) CreateUser(ctx context.Context, req *pb.CreateUserReq) (*pb.CreateUserResp, error) {
err = validateReq(req)
if err != nil {
return nil, errs.NewBadRequest(err.Error(), err)
}
}
Затем в промежуточном программном обеспечении извлекается Resp
, а code
и msg
присваиваются значения.
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
if err == nil {
return resp, err
}
if errs.IsError(err, errs.BaseErr) {
return resp, err
}
if val := reflect.ValueOf(resp); !val.IsValid() || val.IsNil() {
tp := getRespType(ctx, info)
if tp == nil {
return resp, err
}
resp = reflect.New(tp).Interface()
}
if be, ok := errs.AsBaseErr(err); ok {
grpc.SetHeader(ctx, metadata.Pairs("x-http-code", fmt.Sprintf("%d", be.HTTPCode())))
return baseErrSetter(resp, be)
}
}
}
Это позволяет нам автоматически сериализовать возвращенные ошибки в соответствующие поля в ответе.
Код состояния шлюза gRPC
Если мы вернем ошибку непосредственно после обработки ошибки на предыдущем шаге, шлюз gRPC вернет 500, потому что это не код ошибки в системе gRPC, но если мы вернем nil, шлюз gRPC снова вернет 200, ни то, ни другое не ожидается. Поскольку наша система ошибок уже содержит коды состояния HTTP, можем ли мы использовать их напрямую? Ответ да, смотрите код выше, в конце мы устанавливаем метаданные x-http-code
, мы можем зарегистрировать промежуточное программное обеспечение в gRPC gateway, используя код состояния, переданный здесь.
mux := runtime.NewServeMux(
runtime.WithForwardResponseOption(GRPCGatewayHTTPResponseModifier),
)
func GRPCGatewayHTTPResponseModifier(ctx context.Context, w http.ResponseWriter, p proto.Message) error {
md, ok := runtime.ServerMetadataFromContext(ctx)
if !ok {
return nil
}
// set http status code
if vals := md.HeaderMD.Get(httpStatusCodeKey); len(vals) > 0 {
code, err := strconv.Atoi(vals[0])
if err != nil {
return err
}
// delete the headers to not expose any grpc-metadata in http response
delete(md.HeaderMD, httpStatusCodeKey)
delete(w.Header(), grpcHTTPStatusCodeKey)
w.WriteHeader(code)
}
return nil
}
Таким образом, мы возвращаем экземпляр ErrBadRequest в gRPC, который в конечном итоге будет отражен в ответе gRPC gateway как 400, и ErrForbidden, который будет отражен в gRPC gateway как 403, и наша цель успешно достигнута.
Мониторинг
Мы также предоставляем набор промежуточного программного обеспечения, которое можно комбинировать с sentry для сбора стека ошибок.
Заключение
Конечным результатом всей этой системы является:
- gRPC и HTTP могут быть объединены, что соответствует соответствующей спецификации и полностью поддерживает бизнес-требования
- Ошибки оцениваются и классифицируются и могут образовывать дерево ошибок.
- Способен идентифицировать и определять тип, может содержать достаточно информации, может настраивать ошибку и тип ошибки
- Может комбинировать систему sentry и мониторинга для сбора ошибок и мониторинга
- Простой и понятный в использовании, легкий для понимания
- Возможность поддерживать соответствие шлюза grpc кодам состояния и кодам ошибок в grpc