# Nano Banana API障害の切り分け：400・429・503・NO_IMAGE・タイムアウト

> HTTPのrequest error、生成終了理由、呼び出し側timeoutを分けてから、request修正・limit確認・上限付きretryを選ぶ実務手順です。

- Source: https://www.aifreeapi.com/ja/posts/nano-banana2-api-error-codes
- Language: ja
- Published: 2026-08-05
- Updated: 2026-08-05
- Publisher: AI Free API (https://www.aifreeapi.com)

この記事はGeminiアプリ一般ではなく、画像API・SDK・gatewayの障害切り分けを扱います。最初にraw evidenceを保存してください。`400/429/503`はrequest-levelのHTTP結果、`NO_IMAGE`は現行Google API referenceにある公式の生成終了理由、timeoutはclientやgatewayが先に待機をやめた可能性があります。同じretryに流してはいけません。

| 現象 | 最初に保存する証拠 | 最初の対応 | 同じ内容を再試行？ | 復旧の確認 |
|---|---|---|---|---|
| `400 INVALID_ARGUMENT` | 最終URL、API version、model、伏せ字済みbody、details | リクエスト契約を修正 | しない | 最小リクエストで400が消える |
| `429 RESOURCE_EXHAUSTED` | status/details、現在の制限、時刻、attempt | 制限軸を確認して流量を平準化 | 上限付きのみ | 制御した並列数で成功が安定 |
| `503 UNAVAILABLE` | request ID、route、model、発生時間帯 | 稼働状況を確認してbackoff | 上限付きのみ | 同じrouteの最小probeが成功 |
| `finishReason=NO_IMAGE` | candidate、`finishMessage`、parts、response ID | output modalityと最小probeを確認 | 分類後に決める | 新しい応答にimage partがある |
| timeout / `504` | 各層のdeadline、経過時間、body有無、request ID | 最初の終了層と未知の上流結果を確認 | 一時障害だけ | 重複なく同じ経路で完了 |

Googleの[Gemini APIトラブルシューティング](https://ai.google.dev/gemini-api/docs/troubleshooting)は408、429、5xxなどの一時障害に上限付き指数backoffを勧め、400/403の盲目的retryを避けています。[API errors](https://ai.google.dev/gemini-api/docs/api-errors)は`no_image`を生成エラーとして掲載し、[GenerateContent](https://ai.google.dev/api/generate-content)は`NO_IMAGE`を「画像が期待されたが生成されなかった」`FinishReason`として定義しています。公式結果ですがHTTP statusではなく、safety、過負荷、課金の証拠でもありません。

## raw responseを失わない準備

ユーザー向けメッセージに変換する前に、次の項目を記録します。プロンプト、入力画像、認証情報は伏せ字にしてください。

```text
timestamp_utc, provider, base_url, api_version, endpoint, model
http_status, canonical_status, request_id, upstream_request_id
attempt, started_at, latency_ms, client_timeout_ms
candidate_count, prompt_block_reason, finish_reason
part_mime_types, has_text_part, has_image_part, error_body_summary
```

Nano Bananaは通称なので、実際のrouteも残します。Gemini Developer API、Vertex AI、外部gateway、自社reverse proxyでは、同じ画面表示でも責任レイヤーが異なります。以下のprobeはGemini Developer APIのnativeな`models/{model}:generateContent`形式です。Vertexのproject/locationパスと混ぜないでください。

```bash
API_VERSION="${GEMINI_API_VERSION:-v1beta}"
MODEL="${GEMINI_MODEL:?set GEMINI_MODEL}"

curl --fail-with-body --silent --show-error \
  -X POST \
  "https://generativelanguage.googleapis.com/${API_VERSION}/models/${MODEL}:generateContent" \
  -H "x-goog-api-key: ${GEMINI_API_KEY:?set GEMINI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [{"parts": [{"text": "白い背景に青い陶器のマグカップを置いた商品写真を生成してください。"}]}],
    "generationConfig": {"responseModalities": ["IMAGE"]}
  }'
```

`GEMINI_MODEL`には、現在の公式モデル一覧でアカウントから利用できる画像モデルを設定します。この最小probeだけ成功するなら、元の入力、会話コンテキスト、追加パラメータへ調査範囲を絞れます。

![Nano Banana APIのHTTP error、生成終了理由、呼び出し側timeoutの三層構造](https://www.aifreeapi.com/posts/ja/nano-banana2-api-error-codes/img/response-layers.webp)

## 400はリクエスト差分で直す

`400 INVALID_ARGUMENT`に同じbodyを送り直しても回復しません。最終URL、API version、model、`responseModalities`、画像入力のMIMEを、現在の[画像生成ドキュメント](https://ai.google.dev/gemini-api/docs/generate-content/image-generation)と比較します。

オプションを外し、テキストpart 1個と`IMAGE`出力だけにします。成功したら1項目ずつ戻してください。「全部正しそう」より、1項目を追加した瞬間に400が再現する差分の方が強い証拠です。Developer APIのnative endpointにVertexのregionやIAM前提を持ち込まないことも重要です。

## 429と503は原因を分け、retryだけ共有する

`429 RESOURCE_EXHAUSTED`は、適用される利用制限のどれかを超えた状態です。RPM、TPM、RPD、支出、model/tier固有の制限などがあり、数値は変わります。現在値は[Google AI Studioのrate limits](https://ai.google.dev/gemini-api/docs/rate-limits)で確認します。

quota申請より先にburstを確認してください。worker concurrencyを制限し、queueで平準化します。全workerが同じ秒数で再開するとretry stormになります。

`503 UNAVAILABLE`は一時的な過負荷または中断です。prompt不正の証拠でも、一定時間で必ず戻るという保証でもありません。同じaccount/model/routeの最小probe、公式の稼働状況、request IDを揃え、試行回数または総時間の上限に達したら自動retryを止めます。

```python
import random
import time
import requests

TRANSIENT = {408, 429, 500, 502, 503, 504}

def post_with_retry(
    url, headers, payload, *, max_attempts=4, total_budget_s=120
):
    deadline = time.monotonic() + total_budget_s

    for attempt in range(1, max_attempts + 1):
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise TimeoutError("retryの総時間上限に到達")

        try:
            response = requests.post(
                url, headers=headers, json=payload,
                timeout=(10, min(60, remaining))
            )
        except (requests.Timeout, requests.ConnectionError):
            if attempt == max_attempts:
                raise
        else:
            if response.status_code == 400:
                raise ValueError(f"retryせずリクエストを修正: {response.text[:500]}")
            if response.status_code not in TRANSIENT:
                response.raise_for_status()
                return response.json()
            if attempt == max_attempts:
                response.raise_for_status()

        base = min(2 ** (attempt - 1), 30)
        delay = base + random.uniform(0, base * 0.5)
        if delay >= deadline - time.monotonic():
            raise TimeoutError("次のretryが総時間上限に収まらない")
        time.sleep(delay)
```

この値は調整例です。SDKに自動retryがある場合、外側に同じループを重ねると呼び出し回数が乗算されるため、retryレイヤーは一つにします。

## `NO_IMAGE`は公式の生成結果でありHTTP errorではない

公式`finishReason`と、アプリが独自に出す「画像なし」を混同しないでください。現行Google contractでは`NO_IMAGE`はcandidateに属し、gatewayが別途`no_image`へ変換する場合があります。upstream bodyを保存して順に確認します。

1. `candidates`がない：`promptFeedback`を確認します。
2. `finishReason=NO_IMAGE`：`finishMessage`、`responseId`、`modelVersion`を保存し、output modalityと同一routeの最小probeを確認します。根拠なくsafetyや503へ置き換えません。
3. `IMAGE_SAFETY`、`IMAGE_PROHIBITED_CONTENT`など：正当な入力を見直すか境界を受け入れ、retryを回避手段にしません。
4. text partだけ：返却テキストとmodalityを確認します。text-onlyと公式`NO_IMAGE`は別の証拠です。
5. `inlineData`があるのにアプリが画像なし：parser、MIME、base64、storage書き込みを確認します。

```python
def inspect_result(body):
    candidates = body.get("candidates") or []
    if not candidates:
        return {"kind": "no_candidates", "feedback": body.get("promptFeedback")}

    candidate = candidates[0]
    finish_reason = candidate.get("finishReason")
    parts = ((candidate.get("content") or {}).get("parts") or [])
    images = [p["inlineData"] for p in parts if p.get("inlineData")]

    if images:
        kind = "image"
    elif finish_reason == "NO_IMAGE":
        kind = "no_image"
    elif finish_reason in {
        "SAFETY", "IMAGE_SAFETY", "PROHIBITED_CONTENT",
        "IMAGE_PROHIBITED_CONTENT", "IMAGE_RECITATION",
    }:
        kind = "blocked_or_policy"
    else:
        kind = "candidate_without_image"

    return {
        "kind": kind,
        "finishReason": finish_reason,
        "finishMessage": candidate.get("finishMessage"),
        "imageCount": len(images),
        "responseId": body.get("responseId"),
        "modelVersion": body.get("modelVersion"),
    }
```

安全停止やテキストのみの応答を詳しく調べる場合は、[Gemini画像なし・IMAGE_SAFETYガイド](/ja/posts/gemini-image-silent-failure-image-safety-fix)を参照してください。認証など別のHTTPエラーも混在する場合は[Gemini APIエラー総合ガイド](/ja/posts/gemini-api-error-troubleshooting-guide)が適しています。

## タイムアウトは最初に閉じた層を探す

```text
client/SDK → application → reverse proxy/API gateway → provider → model
```

clientが先にキャンセルすればGoogleのHTTP bodyはありません。reverse proxyが独自に504を作る場合もあるため、providerの504はupstream応答で確認できた場合だけ記録します。逆にclient timeoutはserver停止の証拠でもありません。画像や下流書き込みを作る処理は、retry前にrequest/response IDでtaskやartifactを検索し、重複生成を防ぎます。開始、first byte、終了、設定deadline、接続を閉じた層を測定してください。

同じaccount/route/modelで、最小probeと元の複雑な入力を比較します。最小だけ成功し、複雑な入力がclient deadlineで終わるなら、正しい層のtimeoutを見直すかリクエストを小さくします。両方が同じ503/504時間帯で失敗するなら、大きなリクエストを繰り返しても診断情報は増えません。

![Nano Banana APIの上限付きretry、停止、エスカレーション](https://www.aifreeapi.com/posts/ja/nano-banana2-api-error-codes/img/retry-timeline.webp)

## 復旧判定とサポートへの引き継ぎ

400などの非retryable error、最小probeとraw responseで未分類の`NO_IMAGE`、安全/policy `finishReason`、試行回数/総時間の上限、同一fingerprintの継続5xx、または呼び出し側が結果を利用できない時点で自動retryを止めます。

引き継ぎには、UTC時間帯、provider/base URL、API version、endpoint/model、伏せ字済みerror body、request/upstream request ID、各attemptのlatency、全層のtimeout、最小prompt、入力MIME、candidate数、`promptFeedback`、`finishReason`、`finishMessage`、`responseId`、`modelVersion`、part MIME、timeout後のartifact確認結果を含めます。

復旧の証拠は、実運用と同じrouteで最小リクエストがデコード可能なimage partを返し、その後に元の負荷が制御した並列数で完了することです。HTTP 200、taskの`completed`、またはadapterの`NO_IMAGE`表示が消えただけでは十分ではありません。
