Admin SDK 错误分为两类:
- 编程错误:这些是用户应用程序中的编程和配置错误。它们的发生主要是由于 SDK 的错误使用(例如将
null
传递给不接受null
值的方法),以及 Firebase 项目或 SDK 级别的其他配置错误(缺少凭据、不正确的项目 ID 字符串等)在)。 - 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
或空字符串, 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 错误由以下部分组成:
- 错误代码
- 错误信息
- 服务错误代码(可选)
- 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.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()
函数返回的字符串。可以通过调用errorutils.HTTPResponse()
函数来访问可选的 HTTP 响应,该函数返回*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 提供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}");
}
}