管理SDKのエラー処理

Admin SDK エラーは 2 つのカテゴリに分類されます。

  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 トークンや期限切れの 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 では、すべての 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モジュール内のすべてのパブリック メソッドは、 auth.UserNotFoundErrorauth.ExpiredIdTokenErrorなどの API 固有のエラー タイプをスローする場合があります。

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()関数によって返される文字列にすぎません。オプションの HTTP 応答には、 *http.Responseを返すerrorutils.HTTPResponse()関数を呼び出すことでアクセスできます。

errorutilsパッケージのエラー チェック関数にnilまたはその他のエラー値を渡しても安全です。入力引数に問題のエラー コードが実際に含まれている場合はtrueを返し、それ以外の場合はfalseを返します。 HTTPResponse()関数も同様の動作をしますが、 falseではなくnilを返す点が異なります。

サービス エラー コードを公開する 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 サービスで共通です。次の表は、考えられるすべてのプラットフォーム エラー コードの概要を示しています。これは安定したリストであり、長期間変更されないことが予想されます。

無効な引数クライアントが無効な引数を指定しました。
FAILED_PRECONDITION空ではないディレクトリの削除など、現在のシステム状態ではリクエストを実行できません。
範囲外ですクライアントが無効な範囲を指定しました。
未認証OAuth トークンが欠落しているか、無効であるか、期限切れであるため、リクエストは認証されませんでした。
許可が拒否されましたクライアントには十分な権限がありません。これは、OAuth トークンに適切なスコープがない、クライアントに権限がない、または API がクライアント プロジェクトに対して有効になっていないために発生する可能性があります。
見つかりません指定されたリソースが見つからないか、ホワイトリストなどの非公開の理由によりリクエストが拒否されました。
対立同時実行性の競合 (読み取り、変更、書き込みの競合など)。少数の従来のサービスでのみ使用されます。ほとんどのサービスは、これの代わりに ABORTED または ALREADY_EXISTS を使用します。コード内でどのサービスを処理するかを確認するには、サービス固有のドキュメントを参照してください。
中止されました同時実行性の競合 (読み取り、変更、書き込みの競合など)。
もう存在しているクライアントが作成しようとしたリソースはすでに存在します。
リソースが枯渇しましたリソース割り当てを超えているか、レート制限に達しています。
キャンセルリクエストはクライアントによってキャンセルされました。
データロス回復不能なデータ損失またはデータ破損。クライアントはユーザーにエラーを報告する必要があります。
未知不明なサーバーエラー。通常はサーバーのバグです。

このエラー コードは、ローカル応答解析 (アンマーシャル) エラーや、簡単に診断できないその他の広範な低レベル 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 に関連します。
ユーザーが見つかりません指定された識別子のユーザー レコードが見つかりませんでした。

Firebase クラウド メッセージング

THIRD_PARTY_AUTH_ERROR APN 証明書または Web プッシュ認証 API キーが無効か欠落していました。
無効な引数リクエストで指定された 1 つ以上の引数が無効でした。
内部内部サーバーエラー。
割り当て超過メッセージターゲットの送信制限を超えました。
SENDER_ID_MISMATCH認証された送信者 ID は、登録トークンの送信者 ID とは異なります。これは通常、送信者とターゲットの登録トークンが同じ Firebase プロジェクト内にないことを意味します。
利用不可クラウド メッセージング サービスは一時的に利用できません。
未登録アプリ インスタンスが 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 エラーに対して他のデフォルトのエラー処理ルーチンを呼び出すだけです。ユーザーは、必ずしもさまざまなエラー コードやエラーの原因となった理由に関心があるわけではありません。

ジャワ

try {
  FirebaseToken token = FirebaseAuth.getInstance().verifyIdToken(idToken);
  performPrivilegedOperation(token.getUid());
} catch (FirebaseAuthException ex) {
  System.err.println("Failed to verify ID token: " + ex.getMessage());
}

パイソン

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

パイソン

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

パイソン

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

パイソン

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