管理 SDK 错误处理

Admin SDK 错误分为两类:

  1. 编程错误:这些是用户应用程序中的编程和配置错误。它们的发生主要是由于 SDK 的错误使用(例如将null传递给不接受null值的方法),以及 Firebase 项目或 SDK 级别的其他配置错误(缺少凭据、不正确的项目 ID 字符串等)在)。
  2. API 错误:其中包括 SDK 实现中发生的各种可恢复错误、源自 Firebase 后端服务的所有错误以及进行 RPC 调用时可能发生的其他暂时性错误(例如超时)。

Admin SDK 通过抛出相关平台本机的错误来发出编程错误信号。

  • Java:抛出IllegalArgumentExceptionNullPointerException或类似内置运行时错误类型的实例。
  • Python:引发ValueErrorTypeError或其他内置错误类型的实例。
  • Go:返回一般错误。
  • .NET:抛出ArgumentExceptionArgumentNullException或类似内置错误类型的实例。

在大多数情况下,您不应该显式处理编程错误。相反,您应该修复代码和配置,以避免出现编程错误。考虑以下 Java 片段:

String uid = getUserInput();

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

如果getUserInput()方法返回null或空字符串, FirebaseAuth.getUser() API 会引发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 错误上。其中一些错误是可恢复的,例如由于服务暂时不可用而导致的错误,有些错误甚至是在正常程序执行流程中可以预见到的,例如检测到无效或过期的 ID 令牌。本指南的其余部分概述了 Admin SDK 如何表示此类 API 错误,以及可用于处理这些错误的各种选项。

API 错误的结构

API 错误由以下部分组成:

  1. 错误代码
  2. 错误信息
  3. 服务错误代码(可选)
  4. HTTP 响应(可选)

每个 API 错误都保证包含错误代码和错误消息。某些 API 错误还包含特定于生成错误的 API 的服务错误代码。例如,Firebase Auth API 生成的某些错误包含特定于 Firebase Auth 的服务错误代码。如果错误是后端服务的 HTTP 错误响应的结果,则 API 错误还包含相应的 HTTP 响应。这可用于检查原始响应的确切标头和内容,这对于调试、记录或实现更复杂的错误处理逻辑很有用。

除 Node.js 之外的所有 Admin SDK 实现都提供可访问上述 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 提供FirebaseException的特定于 API 的子类。例如, FirebaseAuth API 中的所有公共方法都被声明为抛出FirebaseAuthException的实例。您可以从此派生类访问服务错误代码。

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

Python

在 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.UserNotFoundErrorauth.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()函数返回的字符串。可以通过调用errorutils.HTTPResponse()函数来访问可选的 HTTP 响应,该函数返回*http.Response

nil或任何其他错误值传递给errorutils包中的错误检查函数是安全的。如果输入参数实际上包含有问题的错误代码,则它们返回true ,而对于其他所有内容,它们返回falseHTTPResponse()函数具有类似的行为,只不过它返回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 提供FirebaseException的特定于 API 的子类。例如, FirebaseAuth API 中的所有公共方法都声明为抛出FirebaseAuthException实例。您可以从此派生类访问服务错误代码。

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

平台错误代码

错误代码在所有 Firebase 和 Google Cloud Platform 服务中都很常见。下表概述了所有可能的平台错误代码。这是一个稳定的列表,预计将长期保持不变。

无效的论点客户端指定了无效参数。
失败_前提条件当前系统状态下无法执行请求,例如删除非空目录。
超出范围客户端指定了无效的范围。
未经验证由于 OAuth 令牌丢失、无效或过期,请求未经过身份验证。
没有权限客户端没有足够的权限。发生这种情况的原因可能是 OAuth 令牌没有正确的范围、客户端没有权限或尚未为客户端项目启用 API。
未找到未找到指定资源,或由于白名单等未公开原因而拒绝请求。
冲突并发冲突,例如读-修改-写冲突。仅由少数遗留服务使用。大多数服务使用 ABORTED 或 ALREADY_EXISTS 来代替它。请参阅特定于服务的文档,了解要在代码中处理哪一个。
中止并发冲突,例如读-修改-写冲突。
已经存在客户端尝试创建的资源已存在。
RESOURCE_EXHAUSTED超出资源配额或达到速率限制。
取消请求已被客户端取消。
数据丢失不可恢复的数据丢失或数据损坏。客户端应向用户报告错误。
未知未知的服务器错误。通常是服务器错误。

此错误代码还分配给本地响应解析(解组)错误以及各种其他不易诊断的低级 I/O 错误。

内部的内部服务器错误。通常是服务器错误。
不可用暂停服务。通常服务器会暂时关闭。

此错误代码也分配给本地网络错误(连接被拒绝,没有到主机的路由)。

DEADLINE_EXCEEDED请求期限已过。仅当调用者设置的截止时间短于目标 API 的默认截止时间(即请求的截止时间不足以让服务器处理请求)并且请求未在截止时间内完成时,才会发生这种情况。

此错误代码也分配给本地连接和读取超时。

大多数 API 只能导致上述错误代码的子集。无论如何,在实现错误处理程序时,您不需要显式处理所有这些错误代码。大多数应用程序只对 1-2 个特定错误代码感兴趣,并将其他所有内容视为一般的、不可恢复的故障。

特定于服务的错误代码

Firebase 身份验证

CERTIFICATE_FETCH_FAILED无法获取验证 JWT(ID 令牌或会话 cookie)所需的公钥证书。
电子邮件已经存在已存在使用所提供的电子邮件的用户。
EXPIRED_ID_TOKEN指定给verifyIdToken()的 ID 令牌已过期。
EXPIRED_SESSION_COOKIE指定用于verifySessionCookie()的会话 cookie 已过期。
INVALID_DYNAMIC_LINK_DOMAIN当前项目未配置或授权提供的动态链接域。与电子邮件操作链接 API 相关。
INVALID_ID_TOKEN指定给verifyIdToken() ID 令牌无效。
INVALID_SESSION_COOKIE指定给verifySessionCookie()的会话 cookie 无效。
PHONE_NUMBER_ALREADY_EXISTS已存在使用所提供的电话号码的用户。
REVOKED_ID_TOKEN指定给verifyIdToken()的 ID 令牌已被撤销。
REVOKED_SESSION_COOKIE指定用于verifySessionCookie()的会话 cookie 已过期。
UNAUTHORIZED_CONTINUE_URL继续 URL 的域未列入白名单。与电子邮件操作链接 API 相关。
USER_NOT_FOUND找不到给定标识符的用户记录。

Firebase 云消息传递

第三方验证错误APNs 证书或 Web 推送身份验证 API 密钥无效或丢失。
无效的论点请求中指定的一个或多个参数无效。
内部的内部服务器错误。
超出配额超出消息目标的发送限制。
SENDER_ID_MISMATCH经过身份验证的发件人 ID 与注册令牌的发件人 ID 不同。这通常意味着发送者和目标注册令牌不在同一个 Firebase 项目中。
不可用云消息服务暂时不可用。
未注册应用程序实例已从 FCM 取消注册。这通常意味着所使用的设备注册令牌不再有效,必须使用新的。

自动重试

Admin SDK 会自动重试某些错误,然后再向用户公开这些错误。一般来说,以下类型的错误会被透明地重试:

  • 由 HTTP 503(服务不可用)响应导致的所有 API 错误。
  • 某些 API 错误是由 HTTP 500(内部服务器错误)响应导致的。
  • 大多数低级 I/O 错误(连接被拒绝、连接重置等)。

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());
}

Python

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());
  }
}

Python

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());
  }
}

Python

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());
  }
}

Python

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}");
  }
}