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