エラー処理は、たいてい想像で書かれている
APIを叩くコードを書くとき、try-except は書きます。ただ、そこで実際にどんな例外が飛んでくるのかを確かめてから書いている人は、あまり多くないと思います。私も普段は「たぶん4xxが返るだろう」くらいの想像でハンドリングを書いています。
なので、確かめました。Gemini APIに対して、思いつく限りの壊れたリクエストを投げて、返ってきたものを全部記録します。
試したのは27ケースです。存在しないモデル名、範囲外のパラメータ、壊れた画像、不正なJSON構造、キーの間違いなど。ただしレート制限(429)を狙った連打はしていません。サービス側に無駄な負荷をかけるので、そこは踏み込まずに済ませました。
結果、思っていたのと違うところが4つありました。
返ってきたのは、ほぼ400だった
まずSDK経由の20ケースです。14件が例外になり、6件は壊れませんでした。
例外になった14件のHTTPステータスは次のとおりです。
| ステータス | 件数 | 主な原因 |
|---|---|---|
| 400 INVALID_ARGUMENT | 9 | パラメータの範囲外、壊れた画像、不正なスキーマ、関数名の規則違反、キーの誤り |
| 404 NOT_FOUND | 4 | 存在しないモデル名、停止済みモデル、非対応メソッド |
| 429 RESOURCE_EXHAUSTED | 1 | 巨大な入力 |
ほとんどが400です。そして例外の型は、SDKではまとめて ClientError になります。つまり except で型を分けても仕方がなく、中身のステータスを見に行くことになります。
except ClientError as e:
# 型では区別できない。e の中の code / status を見る
404には3つの意味があり、見分けられない
一番はまりそうなのがここでした。404が返る状況を3つ用意して、メッセージを比べました。
存在しないモデル名を指定した場合。
404 NOT_FOUND
models/gemini-9.9-ultra is not found for API version v1beta,
or is not supported for generateContent.
モデル名を打ち間違えた場合。
404 NOT_FOUND
models/gemini-3.6-flsah is not found for API version v1beta,
or is not supported for generateContent.
埋め込み専用モデルにテキスト生成を頼んだ場合。
404 NOT_FOUND
models/gemini-embedding-2 is not found for API version v1beta,
or is not supported for generateContent.
3つとも同じ文面です。モデル名が間違っているのか、モデルは実在するけれどそのメソッドに対応していないのか、メッセージからは区別できません。英文が or で繋がっているとおり、API側も「どちらかです」としか言っていません。
例外が1つだけあります。廃止されたモデルを呼んだときです。
404 NOT_FOUND
This model models/gemini-2.0-flash is no longer available.
Please update your code to use models/gemini-3.6-flash
for the latest features and improvements.
これだけは、もう提供していないことと、移行先のモデル名まで教えてくれます。廃止のときだけ丁寧なのは、移行させたい意図があるからだと思います。
裏を返すと、404を受け取ったときに no longer available という文字列が入っているかどうかで、廃止なのかタイプミスなのかを判定できます。メッセージ本文で分岐するのは行儀の良い実装ではありませんが、現状これ以外に区別する手がかりがありません。
キーが間違っていると400、キーが無いと403
認証まわりは、直感と違う結果でした。HTTPで直接叩いて確かめています。
| 状況 | HTTP | status | reason |
|---|---|---|---|
| APIキーが不正 | 400 | INVALID_ARGUMENT | API_KEY_INVALID |
| APIキーを送らない | 403 | PERMISSION_DENIED | - |
| APIキーが空文字 | 403 | PERMISSION_DENIED | - |
キーが間違っているときは401でも403でもなく、400が返ります。「リクエストの引数が不正」という扱いです。メッセージは API key not valid. Please pass a valid API key. で、詳細部に API_KEY_INVALID という理由コードが入ります。
一方、キーを付け忘れると403で、こちらは文面が変わります。
Method doesn't allow unregistered callers (callers without established identity).
Please use API Key or other form of API consumer identity to call this API.
400を受け取ったら全部「自分のリクエストの作り方が悪い」と判断してリトライしない実装にしていると、キーの設定ミスもその他大勢に混ざります。気付きにくい部類のエラーなので、API_KEY_INVALID は個別に拾ったほうが親切です。
巨大な入力は「長すぎる」ではなく「使いすぎ」
400万文字のテキストを送ってみました。コンテキスト長を超えているので400が返ると予想していたのですが、実際は違いました。
429 RESOURCE_EXHAUSTED
You exceeded your current quota, please check your plan and billing details.
* Quota exceeded for metric: generativelanguage.googleapis.com/
generate_content_paid_tier_input_token_count, limit: 2000000,
model: gemini-3.6-flash
Please retry in 11.378735399s.
入力トークンの割り当てを使い切った、という扱いです。しかも「11.378735399秒後に再試行してください」と秒数まで返ってきます。
これは実務で効きます。429は普通「混んでいるので待って再送すれば通る」エラーですが、このケースは待って再送しても、リクエストが巨大なままなら永遠に通りません。指数バックオフで自動リトライする実装だと、同じ巨大リクエストを投げ続けることになります。
429を受けたときは、待って再送する前に、自分が投げたトークン数を確認したほうがよさそうです。メッセージ中の limit の値と自分の入力トークン数を比べれば、混雑なのか自分が大きすぎるのかは分かります。
壊れると思ったのに、壊れなかったもの
20ケース中6件は、エラーになりませんでした。個人的にはこちらのほうが驚きです。
空文字のプロンプトを送ると、普通に応答が返ってきます。何も聞いていないのに、何か答えてくれます。バリデーションで弾かれると思っていました。
topK=0 も通ります。範囲外に見えますが、エラーにはなりません。ちなみに temperature のほうは厳格で、範囲外だと弾かれます。
400 INVALID_ARGUMENT
* GenerateContentRequest.generation_config.temperature:
temperature must be in the range [0.0, 2.0].
有効な範囲まで書いてあるので、こちらは親切な部類です。
画像生成モデルにテキストの質問をしたらどうなるか。gemini-3.1-flash-image に「1+1は?」と聞いたところ、「2 です。」と返ってきました。画像専用だと思い込んでいましたが、テキストにも答えます。
関数宣言が空のツール定義も素通りします。中身のないツールを渡しても、モデルは何事もなかったように答えます。
response_mime_type に application/json だけ指定してスキーマを渡さない場合も成功し、それらしいJSONが返ります。
{
"fruits": ["りんご", "バナナ", "みかん"]
}
キー名はモデルが勝手に決めます。スキーマを指定していないので当然ですが、これに依存したコードを書くと、キー名が変わった瞬間に壊れます。エラーにならないぶん、こちらのほうが危ないと言えます。
maxOutputTokens=1 も例外にはなりません。代わりに finish_reason が MAX_TOKENS になり、テキストは空で返ります。例外を投げずに空文字を返してくるので、response.text が空のときの分岐を書いていないと、静かに壊れます。
つまり、壊れたリクエストが必ず例外になるとは限りません。成功扱いで返ってきた中身が空だったり、期待と違う構造だったりするケースを、例外処理とは別に拾う必要があります。
未知のフィールドは無視してくれない
リクエストのJSONに、APIが知らないフィールドを混ぜたらどうなるか。無視されるかと思ったら、はっきり拒否されました。
400 INVALID_ARGUMENT
Invalid JSON payload received. Unknown name "totallyUnknownField": Cannot find field.
キー名のタイプミスも同様です。text を txt と書き間違えると、場所まで指定して教えてくれます。
Invalid JSON payload received. Unknown name "txt" at 'contents[0].parts[0]': Cannot find field.
role に変な値を入れた場合は、使える値を教えてくれます。
Role 'wizard' is not supported. Please use a valid role: MODEL, USER.
このあたりは、エラーメッセージだけで直せる良さがあります。一方で、未知のフィールドを送ると必ず落ちるということは、新しいパラメータを試すコードを古いエンドポイントに投げると動かない、という意味でもあります。
実験そのものでしくじった話
最後に、自分のミスも書いておきます。
不正なAPIキーを試したとき、最初はこんなメッセージが返ってきました。
Cannot send a request, as the client has been closed.
キーが不正だとは一言も書いていません。しばらく「キーの問題ではなく接続の問題なのか」と考えました。
原因は実験スクリプトの書き方でした。テストケースを lambda の中に押し込んでいて、その中で使い捨てのクライアントを生成していたためです。書き方を変えて普通にクライアントを作ってから呼んだら、素直な答えが返ってきました。
400 INVALID_ARGUMENT
API key not valid. Please pass a valid API key.
もし client has been closed を見かけたら、それはAPIからの返答ではなく、クライアントオブジェクトの寿命の問題です。キーやネットワークを疑う前に、そのクライアントがまだ生きているかを見たほうが早い、という教訓を得ました。
分岐の指針
集めた結果から、エラー処理で押さえるべき点をまとめます。
- 例外の型では区別できない。SDKはまとめて
ClientErrorを投げるので、中のcodeとstatusを見る -
リトライして意味があるのは429だけ。ただし429でも、原因が巨大な入力なら再送しても通らない。
limitと自分のトークン数を比べる - 400は再送しても無駄。ただし
API_KEY_INVALIDだけは設定ミスなので、他の400と分けてログに出す - 404は、メッセージに
no longer availableが含まれるかどうかで、モデル廃止とタイプミスを見分ける - 例外が飛ばないケースを別に拾う。
finish_reasonがMAX_TOKENSのときや、response.textが空のとき
エラー処理を想像で書いていたことが、数字で確認できました。特に「巨大な入力が429で返る」ところは、実際に投げてみるまで想像もしていませんでした。