Admin SDK 错误处理

Admin SDK 错误分为两类:

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

对于编程错误,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 身份验证生成的一些错误 API 包含 Firebase Auth 特有的服务错误代码。如果错误 是来自后端服务的 HTTP 错误响应的结果, 还包含相应的 HTTP 响应。这可用于检查 原始响应的准确标头和内容,这对于 调试、记录或实现更复杂的错误处理逻辑。

除 Node.js 之外的所有 Admin SDK 实现都提供了允许访问 以上 API 错误的组成部分。

错误类型和 API(按语言)

Java

在 Java 中,所有 API 错误都会扩展 FirebaseException 类。您可以访问 错误代码、错误消息和来自此基类的可选 HTTP 响应。

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

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

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

公开服务错误代码的 API 提供特定于 API 的子类, FirebaseException。例如,FirebaseAuth API 中的所有公共方法 声明抛出 FirebaseAuthException 的实例。您可以访问 服务错误代码。

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

Python

在 Python 中,所有 API 错误都会扩展 exceptions.FirebaseError 类。您可以查看错误代码、错误消息和可选的 响应。

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

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

在 .NET 中,所有 API 错误都会扩展 FirebaseException 类。您可以访问 平台错误代码、错误消息以及来自此基础服务的可选 HTTP 响应 类。

public class FirebaseException : Exception {

    public ErrorCode ErrorCode { get; }

    public String Message { get; }

    public HttpResponseMessage HttpResponse { get; }
}

公开服务错误代码的 API 提供特定于 API 的子类, FirebaseException。例如,FirebaseAuth API 中的所有公共方法 声明抛出 FirebaseAuthException 实例。 您可以访问 服务错误代码。

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

平台错误代码

错误代码在所有 Firebase 和 Google Cloud Platform 服务中都是通用的。 下表列出了所有可能的平台错误代码。这是一个 稳定版,并且应该会在很长一段时间内保持不变。

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

此错误代码也被指定给本地响应解析(解组)错误,以及不易诊断的各种低级 I/O 错误。

INTERNAL 内部服务器错误。通常是服务器错误。
无法观看 服务不可用。通常,服务器暂时关闭。

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

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

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

大多数 API 只能导致上述错误代码的一部分。无论如何 执行上述操作时,不应明确处理所有这些错误代码 实现错误处理程序大多数应用只关心 1-2 个特定的错误代码,并将其他所有内容视为不可恢复的通用 失败。

特定服务的错误代码

Firebase Auth

CERTIFICATE_FETCH_FAILED 份 未能提取验证 JWT(ID 令牌或会话 Cookie)所需的公钥证书。
EMAIL_ALREADY_EXISTS 已存在使用所提供的电子邮件地址的用户。
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_网址 接续网址的网域未列入白名单。与电子邮件操作链接 API 相关。
USER_NOT_FOUND 未找到与指定标识符对应的用户记录。

Firebase Cloud Messaging

THIRD_PARTY_AUTH_ERROR APNs 证书或 Web 推送身份验证 API 密钥无效或缺失。
INVALID_ARGUMENT 请求中指定的一个或多个参数无效。
INTERNAL 内部服务器错误。
超出 QUOTA_EXCEEDED 消息目标的发送次数已超出上限。
SENDER_ID_MISMATCH 经过身份验证的发送者 ID 与注册令牌的发送者 ID 不同。这通常意味着发送者和目标注册令牌不在同一个 Firebase 项目中。
无法观看 Cloud Messaging服务暂时无法使用。
未报名 应用实例已从 FCM 中取消注册。这通常意味着使用的设备注册令牌已失效,必须使用新的令牌。

自动重试

在显示某些错误之前,Admin SDK 会自动重试这些错误 。一般情况下,系统会以透明的方式重试以下类型的错误:

  • 由 HTTP 503(服务不可用)响应导致的所有 API 错误。
  • 由 HTTP 500(内部服务器错误)响应导致的某些 API 错误。
  • 大多数低级 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 调用其他默认的错误处理例程 错误。他们不一定关注不同的错误代码或 以及可能导致该错误的原因。

Java

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}')

Go

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

performPrivilegedOperation(token)

.Net

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

检查错误代码

在某些情况下,您可能希望检查确切的错误代码,并调用 不同的情境感知错误处理例程。在以下示例中,我们 具有一个错误处理程序,可根据 服务错误代码。

Java

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}')

Go

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)

.Net

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

下面是我们同时检查顶级错误代码和服务错误代码的另一个示例:

Java

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}')

Go

_, 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
}

.Net

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 响应

在极少数情况下,您可能需要检查 对后端服务执行一些错误处理操作。Admin SDK 公开这些错误响应的标头和内容。响应 内容通常以字符串或原始字节序列的形式返回,且可以是 解析为任何必要的目标格式。

Java

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}')

Go

_, 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
}

.Net

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