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