2014年からアプリ事業を続けている個人開発者として、Firebase AI Logic を自分の iOS アプリに組み込んだとき、シミュレータでは動くのに TestFlight に上げた瞬間すべての Gemini 呼び出しが 403 で落ちる、という現象に丸一日悩まされました。
エラーメッセージ自体は短く、The request is missing a valid API key や PERMISSION_DENIED としか出ないため、原因がコードなのか設定なのか判別しづらいのが厄介です。以下では、累計5,000万ダウンロードの自社アプリに Gemini を組み込む過程で私が実際にぶつかった3つの原因と、その切り分け手順を、実機検証の経験からまとめます。
まず出ている 403 の中身を確認する
Firebase AI Logic(Swift では FirebaseAI モジュール)で Gemini を呼ぶと、失敗時には次のような形でエラーが返ります。
import FirebaseAI
let model = FirebaseAI.firebaseAI(backend: .googleAI())
.generativeModel(modelName: "gemini-2.5-flash")
do {
let response = try await model.generateContent("こんにちは")
print(response.text ?? "")
} catch {
// ここで握りつぶさず、必ず error をそのまま出力する
print("Gemini call failed:", error)
}catch 節で error をそのまま print すると、ログに httpStatusCode = 403 と一緒に status: PERMISSION_DENIED や App Check token is invalid といった詳細が出ます。まずこの全文を読むことが切り分けの起点です。response.text が nil だから、と曖昧に判断して進めると遠回りになります。
403 の裏には、大きく分けて次の3つの原因があります。上から順に確認していくのが早道です。
原因1: App Check が強制適用されているのにトークンが出ていない(最頻出)
私のケースで犯人だったのがこれです。Firebase コンソールで App Check を「強制(Enforce)」に切り替えると、有効な App Check トークンを持たないリクエストはすべて 403 で拒否されます。シミュレータで動いて実機やTestFlightで落ちるのは、シミュレータでは debug provider のトークンが通っていたのに、実機では App Attest プロバイダを初期化していなかったためでした。
対処は、FirebaseApp.configure() の 前 に App Check プロバイダを設定することです。順序を間違えると、Firebase が初期化された後にプロバイダが差し替わり、最初の数リクエストがトークンなしで飛びます。
import FirebaseCore
import FirebaseAppCheck
class AppCheckSetup: NSObject, AppCheckProviderFactory {
func createProvider(with app: FirebaseApp) -> AppCheckProvider? {
// iOS 14 以降の実機は App Attest、それ以前は DeviceCheck
if #available(iOS 14.0, *) {
return AppAttestProvider(app: app)
}
return DeviceCheckProvider(app: app)
}
}
// AppDelegate / App 起動時、configure より前に呼ぶ
AppCheck.setAppCheckProviderFactory(AppCheckSetup())
FirebaseApp.configure()開発中にシミュレータで検証したいときは、debug provider を使い、コンソールに出力されるデバッグトークンを Firebase コンソールの「App Check → アプリ → デバッグトークンを管理」に登録します。
#if DEBUG
AppCheck.setAppCheckProviderFactory(AppCheckDebugProviderFactory())
#else
AppCheck.setAppCheckProviderFactory(AppCheckSetup())
#endif
FirebaseApp.configure()ここで一つ注意点があります。App Attest はサーバー側で実機の正当性を検証するため、TestFlight ビルドであっても初回の検証に数秒かかることがあります。アプリ起動直後に即座に Gemini を呼ぶ設計だと、トークンがまだ発行されておらず 403 になることがあるので、最初の呼び出しはユーザー操作を待つか、軽いリトライを挟むと安定します。
原因2: 必要な Google API が有効化されていない
Firebase コンソールの「AI Logic」から初回セットアップをウィザード経由で行うと、必要な API は自動で有効化されます。しかし、別の Firebase プロジェクトを流用したり、GoogleService-Info.plist だけ差し替えて使い回したりすると、API が無効のままで 403 になります。
使用するバックエンドによって必要な API が異なります。
- Gemini Developer API バックエンド(
.googleAI()):firebasevertexai.googleapis.comと Generative Language API - Vertex AI バックエンド(
.vertexAI()):aiplatform.googleapis.com
Google Cloud Console の「API とサービス → 有効な API」で、対象プロジェクトに上記が並んでいるか確認します。コマンドで確認・有効化する場合は次の通りです。
# 有効化済みか確認
gcloud services list --enabled --project=YOUR_PROJECT_ID | grep -E "aiplatform|firebasevertexai"
# 足りなければ有効化(Vertex AI バックエンドの例)
gcloud services enable aiplatform.googleapis.com --project=YOUR_PROJECT_ID私の経験上、既存アプリに後から AI 機能を足すときにこの罠を踏みやすいです。新規ウィザードを通っていないプロジェクトは、まずここを疑ってください。
原因3: Blaze プラン(従量課金)が未設定
Vertex AI バックエンドを使う場合、プロジェクトが無料の Spark プランのままだと、課金が必要なため 403 もしくは課金関連のエラーになります。Firebase コンソールの「使用量と請求」から Blaze プランへアップグレードすると解消します。
Gemini Developer API バックエンドは無料枠の範囲なら Spark プランでも動きますが、本番アプリでレート制限を避けたいなら、いずれにせよ Blaze への移行を検討することになります。コストが不安な場合は、Google Cloud の予算アラートを設定しておくと安心です。
切り分けの順序をまとめると
403 に当たったら、次の順で確認すると最短で原因にたどり着けます。
catchでエラー全文を出力し、App Check token is invalidの文字列があるか確認する → あれば原因1- なければ Google Cloud Console で対象 API が有効か確認する → 無効なら原因2
- API も有効なら、課金プランが Blaze かを確認する → Spark なら原因3
- すべて満たしているのに 403 が続く場合は、
GoogleService-Info.plistが呼び出し元プロジェクトのものと一致しているかを確認する(別環境の plist を混入させていると、コンソールで見ている設定と実際の宛先がずれます)
特に1と4は、コンソール上の設定だけ見ていても気づけません。実機ログのエラー全文と、ビルドに含まれている plist の PROJECT_ID を突き合わせるのが確実です。
再発を防ぐために
私は今、AI 機能を持つアプリのリリースチェックリストに「App Check を強制適用する前に、実機(TestFlight)で App Attest トークンが発行されることを確認する」という項目を入れています。強制適用は最後に切り替える設定であって、開発初期から有効にすると、原因の切り分けが一気に難しくなるからです。
開発中は App Check を「強制」ではなく「未強制(モニタリングのみ)」にしておき、トークンの発行状況をコンソールで観察してから本番で強制に切り替える。この順番にしてから、403 に振り回されることがなくなりました。
同じところでつまずいている方の参考になれば幸いです。