Обработка ошибок Admin SDK

Ошибки Admin SDK делятся на две категории:

  1. Ошибки программирования: это ошибки программирования и конфигурации пользовательского приложения. Чаще всего они возникают из-за неправильного использования SDK (например, передачи значения null методу, который не принимает null значения) и других ошибок конфигурации на уровне проекта Firebase или SDK (отсутствующие учетные данные, неверная строка идентификатора проекта и т. д.). на).
  2. Ошибки API. К ним относятся различные исправимые ошибки, возникающие в реализации SDK, все ошибки, возникающие в серверных службах Firebase, а также другие временные ошибки (например, тайм-ауты), которые могут возникнуть при выполнении вызовов RPC.

Admin SDK сигнализирует об ошибках программирования , выдавая ошибку, встроенную в рассматриваемую платформу.

  • Java: выдает экземпляры IllegalArgumentException , NullPointerException или аналогичного встроенного типа ошибки времени выполнения.
  • Python: вызывает экземпляры ValueError , TypeError или другого встроенного типа ошибки.
  • Go: возвращает общую ошибку.
  • .NET: выдает экземпляры ArgumentException , ArgumentNullException или аналогичного встроенного типа ошибки.

В большинстве ситуаций не следует явно обрабатывать ошибки программирования. Вместо этого вам следует исправить свой код и конфигурацию, чтобы вообще избежать ошибок программирования. Рассмотрим следующий фрагмент Java:

String uid = getUserInput();

UserRecord user = FirebaseAuth.getInstance().getUser(uid);

Если метод getUserInput() возвращает null или пустые строки, API FirebaseAuth.getUser() выдает исключение IllegalArgumentException . Вместо того, чтобы явно обрабатывать эту проблему, вы можете смягчить проблему, гарантируя, что метод getUserInput() никогда не возвращает недопустимую строку UID. Если это невозможно, реализуйте необходимую проверку аргументов в своем собственном коде следующим образом:

String uid = getUserInput();
if (Strings.isNullOrEmpty(uid)) {
    log.warn("UID must not be null or empty");
    return;
}

UserRecord user = FirebaseAuth.getInstance().getUser(uid);

В принципе, никогда не повторяйте попытку в случае ошибок программирования. Обеспечение отказоустойчивой семантики ошибок программирования часто является лучшим способом действий, поскольку оно выявляет программные ошибки и ошибки конфигурации во время разработки, где их можно быстро исправить. В этом контексте отказоустойчивость может означать распространение ошибок на глобальный обработчик ошибок в вашем приложении или просто регистрацию их в целях аудита с последующим завершением текущего потока выполнения (приложение не должно аварийно завершать работу). В общем, следуйте лучшим практикам обработки ошибок вашего языка программирования и среды приложения. Одного этого часто бывает достаточно, чтобы корректно справиться с данным классом ошибок.

Обычно основная часть ваших усилий по обработке ошибок сосредоточена на обработке ошибок API . Некоторые из этих ошибок поддаются исправлению, например ошибки, возникшие в результате временной недоступности службы, а некоторые даже ожидаются во время обычного процесса выполнения программы, например, обнаружение недействительных или просроченных идентификационных токенов. В оставшейся части руководства описывается, как Admin SDK представляет такие ошибки API, а также различные варианты их обработки.

Структура ошибки API

Ошибка API состоит из следующих компонентов:

  1. Код ошибки
  2. Сообщение об ошибке
  3. Код сервисной ошибки (необязательно)
  4. HTTP-ответ (необязательно)

Каждая ошибка API гарантированно содержит код ошибки и сообщение об ошибке. Некоторые ошибки API также содержат код ошибки службы, специфичный для API, вызвавшего ошибку. Например, некоторые ошибки, генерируемые API Firebase Auth, содержат код ошибки службы, специфичный для Firebase Auth. Если ошибка возникла в результате ответа об ошибке HTTP от серверной службы, ошибка API также содержит соответствующий ответ HTTP. Это можно использовать для проверки точных заголовков и содержимого исходного ответа, что полезно для отладки, ведения журнала или реализации более сложной логики обработки ошибок.

Все реализации Admin SDK, за исключением Node.js, предоставляют API, которые позволяют получить доступ к вышеуказанным компонентам ошибок API.

Типы ошибок и API по языкам

Джава

В Java все ошибки API расширяют класс FirebaseException . Вы можете получить доступ к коду ошибки, сообщению об ошибке и дополнительному ответу HTTP из этого базового класса.

public class FirebaseException extends Exception {
    @NonNull
    public ErrorCode getErrorCode() {
        // ...
    }

    @NonNull
    public String getMessage() {
        // ...
    }

    @Nullable
    public IncomingHttpResponse getHttpResponse() {
        // ...
    }
}

API, которые предоставляют коды ошибок службы, предоставляют специфичные для API подклассы FirebaseException . Например, объявлено, что все общедоступные методы в API FirebaseAuth выбрасывают экземпляры FirebaseAuthException . Вы можете получить доступ к коду ошибки службы из этого производного класса.

public class FirebaseAuthException extends FirebaseException {
    @Nullable
    public AuthErrorCode getAuthErrorCode() {
        // ...
    }
}

Питон

В Python все ошибки API расширяют exceptions.FirebaseError . Вы можете получить доступ к коду ошибки, сообщению об ошибке и дополнительному HTTP-ответу из этого базового класса.

class FirebaseError(Exception):
    @property
    def code(self):
          # ...

    @property
    def message(self):
          # ...

    @property
    def http_response(self):
          # ...

Более того, Python Admin SDK предлагает отдельные производные классы для каждого кода ошибки. Мы называем их классами ошибок платформы .

class InvalidArgumentError(FirebaseError):
    # ...

class NotFoundError(FirebaseError):
    # ...

Вы можете либо перехватить FirebaseError в своем коде и проверить его code , либо выполнить проверку isinstance() на класс ошибок платформы. Или вы можете написать код, чтобы напрямую перехватывать определенные типы ошибок платформы. Последний подход, вероятно, приведет к более читаемому коду обработки ошибок.

API, которые предоставляют коды ошибок службы, предоставляют подклассы классов ошибок платформы, специфичные для API. Например, все общедоступные методы в модуле auth могут выдавать типы ошибок, специфичные для API, такие как auth.UserNotFoundError и auth.ExpiredIdTokenError .

class UserNotFoundError(exceptions.NotFoundError):
    # …

class ExpiredIdTokenError(exceptions.InvalidArgumentError):
    # ...

Идти

Go Admin SDK предоставляет пакет errorutils , который содержит ряд функций, позволяющих проверять коды ошибок.

package errorutils

func IsInvalidArgument(err error) bool {
    // ...
}

func IsNotFound(err error) bool {
    // ...
}

Сообщение об ошибке — это просто строка, возвращаемая функцией Error() об ошибке. Доступ к дополнительному ответу HTTP можно получить, вызвав функцию errorutils.HTTPResponse() , которая возвращает *http.Response .

Безопасно передавать nil или любое другое значение ошибки в функции проверки ошибок в пакете errorutils . Они возвращают true , если входной аргумент действительно содержит рассматриваемый код ошибки, и возвращают false для всего остального. Функция HTTPResponse() ведет себя аналогично, за исключением того, что она возвращает nil вместо false .

API, которые предоставляют коды ошибок службы, предоставляют функции проверки ошибок, специфичные для API, в соответствующих пакетах. Например, пакет auth предоставляет функции IsUserNotFound() и IsExpiredIDTokenError() .

.СЕТЬ

В .NET все ошибки API расширяют класс FirebaseException . Вы можете получить доступ к коду ошибки платформы, сообщению об ошибке и дополнительному ответу HTTP из этого базового класса.

public class FirebaseException : Exception {

    public ErrorCode ErrorCode { get; }

    public String Message { get; }

    public HttpResponseMessage HttpResponse { get; }
}

API, которые предоставляют коды ошибок службы, предоставляют специфичные для API подклассы FirebaseException . Например, объявлено, что все общедоступные методы в API FirebaseAuth выбрасывают экземпляры FirebaseAuthException . Вы можете получить доступ к коду ошибки службы из этого производного класса.

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

Коды ошибок платформы

Коды ошибок являются общими для всех сервисов Firebase и Google Cloud Platform. В следующей таблице приведены все возможные коды ошибок платформы. Это стабильный список, и ожидается, что он останется неизменным в течение длительного периода.

НЕДЕЙСТВИТЕЛЬНЫМ АРГУМЕНТ Клиент указал недопустимый аргумент.
FAILED_PRECONDITION Запрос не может быть выполнен в текущем состоянии системы, например, удаление непустого каталога.
ВНЕ ДИАПАЗОНА Клиент указал неверный диапазон.
НЕ АУТЕНТИФИЦИРОВАННЫЙ Запрос не прошел аутентификацию из-за отсутствия, недействительности или просроченного токена OAuth.
ДОСТУП ЗАПРЕЩЕН Клиент не имеет достаточного разрешения. Это может произойти из-за того, что токен OAuth не имеет нужных областей, у клиента нет разрешения или API не включен для клиентского проекта.
НЕ НАЙДЕНО Указанный ресурс не найден, или запрос отклонен по неизвестным причинам, например внесению в белый список.
КОНФЛИКТ Конфликт параллелизма, например конфликт чтения-изменения-записи. Используется только несколькими устаревшими службами. Большинство служб вместо этого используют ABORTED или ALREADY_EXISTS. Обратитесь к документации по конкретному сервису, чтобы узнать, какой из них обрабатывать в вашем коде.
ПРЕРВИРОВАНО Конфликт параллелизма, например конфликт чтения-изменения-записи.
УЖЕ СУЩЕСТВУЕТ Ресурс, который пытался создать клиент, уже существует.
RESOURCE_EXHAUSTED Либо исчерпана квота ресурсов, либо достигнуто ограничение скорости.
ОТМЕНЕНО Запрос отменен клиентом.
ПОТЕРИ ДАННЫХ Невосстановимая потеря или повреждение данных. Клиент должен сообщить об ошибке пользователю.
НЕИЗВЕСТНЫЙ Неизвестная ошибка сервера. Обычно это ошибка сервера.

Этот код ошибки также присваивается ошибкам локального анализа (демаршалирования) ответов и широкому спектру других ошибок ввода-вывода низкого уровня, которые нелегко диагностировать.

ВНУТРЕННИЙ Внутренняя ошибка сервера. Обычно это ошибка сервера.
НЕДОСТУПЕН Сервис недоступен. Обычно сервер временно не работает.

Этот код ошибки также присваивается ошибкам локальной сети (отказано в соединении, нет маршрута к хосту).

DEADLINE_EXCEEDED Срок запроса истек. Это произойдет только в том случае, если вызывающая сторона установит крайний срок, который короче срока по умолчанию для целевого API (т. е. запрошенного срока недостаточно для того, чтобы сервер обработал запрос), и запрос не был завершен в течение этого срока.

Этот код ошибки также относится к локальному соединению и тайм-аутам чтения.

Большинство API могут привести только к подмножеству приведенных выше кодов ошибок. В любом случае от вас не требуется явной обработки всех этих кодов ошибок при реализации обработчиков ошибок. Большинству приложений будут интересны только 1-2 конкретных кода ошибок, а все остальное будет рассматриваться как общий, неисправимый сбой.

Сервисные коды ошибок

Аутентификация Firebase

CERTIFICATE_FETCH_FAILED Не удалось получить сертификаты открытого ключа, необходимые для проверки JWT (токена идентификатора или файла cookie сеанса).
АДРЕС ЭЛЕКТРОННОЙ ПОЧТЫ УЖЕ СУЩЕСТВУЕТ Пользователь с указанным адресом электронной почты уже существует.
EXPIRED_ID_TOKEN Срок действия токена идентификатора, указанного для verifyIdToken() истек.
EXPIRED_SESSION_COOKIE Срок действия файла cookie сеанса, указанного для verifySessionCookie() , истек.
INVALID_DYNAMIC_LINK_DOMAIN Предоставленный домен динамических ссылок не настроен и не авторизован для текущего проекта. Связано с API-интерфейсами ссылок на действия по электронной почте.
INVALID_ID_TOKEN Токен идентификатора, указанный для verifyIdToken() недействителен.
INVALID_SESSION_COOKIE Файл cookie сеанса, указанный для verifySessionCookie() , недействителен.
PHONE_NUMBER_ALREADY_EXISTS Пользователь с указанным номером телефона уже существует.
REVOKED_ID_TOKEN Токен идентификатора, указанный для verifyIdToken() отменяется.
REVOKED_SESSION_COOKIE Срок действия файла cookie сеанса, указанного для verifySessionCookie() , истек.
UNAUTHORIZED_CONTINUE_URL Домен URL-адреса продолжения не внесен в белый список. Связано с API-интерфейсами ссылок на действия по электронной почте.
ПОЛЬЗОВАТЕЛЬ НЕ НАЙДЕН Для данного идентификатора не найдено записей пользователя.

Облачный обмен сообщениями Firebase

THIRD_PARTY_AUTH_ERROR Сертификат APNs или ключ API аутентификации через веб-приложение недействительны или отсутствуют.
НЕДЕЙСТВИТЕЛЬНЫМ АРГУМЕНТ Один или несколько аргументов, указанных в запросе, недействительны.
ВНУТРЕННИЙ Внутренняя ошибка сервера.
КВОТА ПРЕВЫШЕНА Для целевого сообщения превышен лимит отправки.
SENDER_ID_MISMATCH Идентификатор проверенного отправителя отличается от идентификатора отправителя регистрационного токена. Обычно это означает, что отправитель и целевой токен регистрации не находятся в одном проекте Firebase.
НЕДОСТУПЕН Служба облачных сообщений временно недоступна.
НЕЗАРЕГИСТРИРОВАННЫЙ Экземпляр приложения не зарегистрирован в FCM. Обычно это означает, что используемый токен регистрации устройства больше не действителен и необходимо использовать новый.

Автоматические повторные попытки

Admin SDK автоматически повторяет определенные ошибки, прежде чем раскрывать их пользователям. Как правило, следующие типы ошибок прозрачно повторяются:

  • Все ошибки API, возникающие в результате ответов HTTP 503 (служба недоступна).
  • Некоторые ошибки API, возникающие в результате ответов HTTP 500 (внутренняя ошибка сервера).
  • Большинство ошибок ввода-вывода низкого уровня (отказ в соединении, сброс соединения и т. д.).

SDK будет повторять каждую из вышеуказанных ошибок до 5 раз (исходная попытка + 4 повтора) с экспоненциальной задержкой. Если хотите, вы можете реализовать свои собственные механизмы повторных попыток на уровне приложения, но обычно это не требуется.

Повторить попытку после поддержки

Реализации Admin SDK на Go и .NET поддерживают обработку заголовка HTTP Retry-After . То есть, если ответ об ошибке, отправленный внутренними серверами, содержит стандартный заголовок Retry-After , SDK будет учитывать это при повторной попытке, если указанная продолжительность ожидания не очень велика. Если заголовок Retry-After указывает на очень длительную продолжительность ожидания, SDK пропустит повторные попытки и выдаст соответствующую ошибку API.

Python Admin SDK в настоящее время не поддерживает заголовок Retry-After и поддерживает только простую экспоненциальную отсрочку.

Примеры обработки ошибок API

Реализация общего обработчика ошибок

В большинстве случаев вам нужен универсальный обработчик ошибок, который перехватывает широкий спектр ошибок и предотвращает неожиданное завершение выполнения программы из-за ошибки API. Такие обработчики ошибок обычно просто регистрируют ошибки в целях аудита или вызывают какую-либо другую процедуру обработки ошибок по умолчанию для всех обнаруженных ошибок API. Их не обязательно интересуют различные коды ошибок или причины, которые могли вызвать ошибку.

Джава

try {
  FirebaseToken token = FirebaseAuth.getInstance().verifyIdToken(idToken);
  performPrivilegedOperation(token.getUid());
} catch (FirebaseAuthException ex) {
  System.err.println("Failed to verify ID token: " + ex.getMessage());
}

Питон

try:
  token = auth.verify_id_token(idToken)
  perform_privileged_pperation(token.uid)
except exceptions.FirebaseError as ex:
  print(f'Failed to verify ID token: {ex}')

Идти

token, err := client.VerifyIDToken(ctx, idToken)
if err != nil {
  log.Printf("Failed to verify ID token: %v", err)
  return
}

performPrivilegedOperation(token)

.Сеть

try
{
  var token = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
  PerformPrivilegedOperation(token.getUid());
}
catch (FirebaseAuthException ex) 
{
  Conole.WriteLine($"Failed to verify ID token: {ex.Message}");
}

Проверка кодов ошибок

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

Джава

try {
  FirebaseToken token = FirebaseAuth.getInstance().verifyIdToken(idToken);
  performPrivilegedOperation(token.getUid());
} catch (FirebaseAuthException ex) {
  if (ex.getAuthErrorCode() == AuthErrorCode.ID_TOKEN_EXPIRED) {
    System.err.println("ID token has expired");
  } else if (ex.getAuthErrorCode() == AuthErrorCode.ID_TOKEN_INVALID) {
    System.err.println("ID token is malformed or invalid");
  } else {
    System.err.println("Failed to verify ID token: " + ex.getMessage());
  }
}

Питон

try:
  token = auth.verify_id_token(idToken)
  perform_privileged_operation(token.uid)
except auth.ExpiredIdTokenError:
  print('ID token has expired')
except auth.InvalidIdTokenError:
  print('ID token is malformed or invalid')
except exceptions.FirebaseError as ex:
  print(f'Failed to verify ID token: {ex}')

Идти

token, err := client.VerifyIDToken(ctx, idToken)
if auth.IsIDTokenExpired(err) {
  log.Print("ID token has expired")
  return
}
if auth.IsIDTokenInvalid(err) {
  log.Print("ID token is malformed or invalid")
  return
}
if err != nil {
  log.Printf("Failed to verify ID token: %v", err)
  return
}

performPrivilegedOperation(token)

.Сеть

try
{
  var token = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
  PerformPrivilegedOperation(token.getUid());
}
catch (FirebaseAuthException ex)
{
  if (ex.AuthErrorCode == AuthErrorCode.ExpiredIdToken)
  {
    Console.WriteLine("ID token has expired");
  }
  else if (ex.AuthErrorCode == AuthErrorCode.InvalidIdToken)
  {
    Console.WriteLine("ID token is malformed or invalid");
  }
  else
  {
    Conole.WriteLine($"Failed to verify ID token: {ex.Message}");
  }
}

Вот еще один пример, в котором мы проверяем коды ошибок как верхнего уровня, так и служебных:

Джава

try {
  FirebaseMessaging.getInstance().send(createMyMessage());
} catch (FirebaseMessagingException ex){
  if (ex.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED) {
    System.err.println("App instance has been unregistered");
    removeTokenFromDatabase();
  } else if (ex.getErrorCode() == ErrorCode.Unavailable) {
    System.err.println("FCM service is temporarily unavailable");
    scheduleForRetryInAnHour();
  } else {
    System.err.println("Failed to send notification: " + ex.getMessage());
  }
}

Питон

try:
  messaging.send(create_my_message())
except messaging.UnregisteredError:
  print('App instance has been unregistered')
  remove_token_from_database()
except exceptions.UnavailableError:
  print('FCM service is temporarily unavailable')
  schedule_for_retry_in_an_hour()
except exceptions.FirebaseError as ex:
  print(f'Failed to send notification: {ex}')

Идти

_, err := client.Send(ctx, createMyMessage())
if messaging.IsUnregistered(err) {
  log.Print("App instance has been unregistered")
  removeTokenFromDatabase()
  return
}
if errorutils.IsUnavailable(err) {
  log.Print("FCM service is temporarily unavailable")
  scheduleForRetryInAnHour()
  return
}
if err != nil {
  log.Printf("Failed to send notification: %v", err)
  return
}

.Сеть

try
{
  await FirebaseMessaging.DefaultInstance.SendAsync(createMyMessage());
}
catch (FirebaseMessagingException ex)
{
  if (ex.MessagingErrorCode == MessagingErrorCode.UNREGISTERED)
  {
    Console.WriteLine("App instance has been unregistered");
    removeTokenFromDatabase();
  }
  else if (ex.ErrorCode == ErrorCode.Unavailable)
  {
    Console.WriteLine("FCM service is temporarily unavailable");
    scheduleForRetryInAnHour();
  }
  else
  {
    Console.WriteLine($"Failed to send notification: {ex.Message}");
  }
}

Доступ к HTTP-ответу

В некоторых редких случаях вам может потребоваться проверить ответ об ошибке HTTP, возвращаемый внутренней службой, и выполнить над ним некоторые действия по обработке ошибок. Admin SDK предоставляет как заголовки, так и содержимое этих ответов об ошибках. Содержимое ответа обычно возвращается в виде строки или необработанной последовательности байтов и может быть преобразовано в любой необходимый целевой формат.

Джава

try {
  FirebaseMessaging.getInstance().send(createMyMessage());
} catch (FirebaseMessagingException ex){
  IncomingHttpResponse response = ex.getHttpResponse();
  if (response != null) {
    System.err.println("FCM service responded with HTTP " + response.getStatusCode());

    Map<String, Object> headers = response.getHeaders();
    for (Map.Entry<String, Object> entry : headers.entrySet()) {
      System.err.println(">>> " + entry.getKey() + ": " + entry.getValue());
    }

    System.err.println(">>>");
    System.err.println(">>> " + response.getContent());
  }
}

Питон

try:
  messaging.send(create_my_message())
except exceptions.FirebaseError as ex:
  response = ex.http_response
  if response is not None:
    print(f'FCM service responded with HTTP {response.status_code}')

    for key, value in response.headers.items():
      print(f'>>> {key}: {value}')

    print('>>>')
    print(f'>>> {response.content}')

Идти

_, err := client.Send(ctx, createMyMessage())
if resp := errorutils.HTTPResponse(err); resp != nil {
  log.Printf("FCM service responded with HTTP %d", resp.StatusCode)

  for key, value := range resp.Header {
      log.Printf(">>> %s: %v", key, value)
  }

  defer resp.Body.Close()
  b, _ := ioutil.ReadAll(resp.Body)
  log.Print(">>>")
  log.Printf(">>> %s", string(b))

  return
}

.Сеть

try
{
  await FirebaseMessaging.DefaultInstance.SendAsync(createMyMessage());
}
catch (FirebaseMessagingException ex)
{
  var response = ex.HttpResponse
  if response != null
  {
    Console.WriteLine($"FCM service responded with HTTP { response.StatusCode}");

    var headers = response.Headers;
    for (var entry in response.Headers)
    {
      Console.WriteLine($">>> {entry.Key}: {entry.Value}");
    }

    var body = await response.Content.ReadAsString();
    Console.WriteLine(">>>");
    Console.WriteLine($">>> {body}");
  }
}