# Nano Banana API 장애 분류: 400·429·503·NO_IMAGE·시간 초과

> HTTP 요청 오류, 생성 종료 사유, 호출자 시간 초과를 나눈 뒤 요청 수정·limit 확인·제한 retry를 선택하는 한국어 실무 절차입니다.

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

이 문서는 Gemini 앱 일반 팁이 아니라 Nano Banana 이미지 API·SDK·gateway 장애 대응용입니다. 재시도 전에 원본 증거를 보관하세요. `400/429/503`은 요청 단계의 HTTP 결과이고, `NO_IMAGE`는 현재 Google API 문서의 공식 생성 종료 사유이며, 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 | 출력 모달리티와 최소 probe 확인 | 분류 후 결정 | 새 응답에 image part 존재 |
| timeout / `504` | 계층별 deadline, 경과 시간, body 수신 여부, request ID | 최초 종료 계층과 미확정 결과 확인 | 일시 오류만 | 중복 없이 같은 경로에서 완료 |

Google의 [Gemini API 문제 해결 문서](https://ai.google.dev/gemini-api/docs/troubleshooting)는 408, 429, 5xx 같은 일시 오류에 제한된 exponential backoff를 권장하고 400/403의 맹목적 retry를 피하라고 설명합니다. [API errors](https://ai.google.dev/gemini-api/docs/api-errors)는 `no_image`를 generation error로, [GenerateContent](https://ai.google.dev/api/generate-content)는 `NO_IMAGE`를 “이미지를 기대했지만 생성되지 않음”이라는 `FinishReason`으로 정의합니다. 공식 결과이지만 HTTP 상태가 아니며 safety, 과부하, 과금의 단독 증거도 아닙니다.

## 원본 응답을 한 번에 보관하세요

adapter가 모든 오류를 “생성 실패”로 바꾸기 전에 다음 필드를 기록합니다. 프롬프트, 입력 이미지, 인증 정보는 마스킹합니다.

```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만 성공하면 원래 입력, 대화 문맥, 선택 필드로 범위를 좁힐 수 있습니다. 둘 다 실패하면 route, 계정, 용량 증거를 봅니다.

![Nano Banana API의 HTTP 오류, 생성 종료 사유, 호출자 시간 초과 3계층](https://www.aifreeapi.com/posts/ko/nano-banana2-api-error-codes/img/response-layers.webp)

## 400은 재시도가 아니라 요청 차이로 해결합니다

`400 INVALID_ARGUMENT`는 API가 형식 또는 파라미터를 거절했다는 뜻입니다. 같은 body를 다시 보내도 회복되지 않습니다. 최종 URL, API version, model, `responseModalities`, 입력 이미지 MIME을 현재 [이미지 생성 문서](https://ai.google.dev/gemini-api/docs/generate-content/image-generation)와 비교합니다.

선택 필드를 제거하고 text part 하나와 `IMAGE` 출력만 남기세요. 성공하면 필드를 하나씩 다시 넣습니다. 큰 payload가 “맞아 보인다”는 판단보다 특정 필드를 넣는 순간 400이 재현되는 차이가 훨씬 유용합니다. Developer API 문제에 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)에서 확인합니다.

먼저 burst를 줄이세요. worker concurrency를 제한하고 queue로 요청을 분산합니다. 모든 worker가 같은 시간에 깨어나면 retry storm이 생기므로 jitter가 필요합니다.

`503 UNAVAILABLE`는 일시적 과부하 또는 중단입니다. 잘못된 프롬프트의 증거도 아니고 일정 시간 안에 복구된다는 보장도 아닙니다. 동일 account/model/route의 최소 probe와 공식 상태를 비교하고 request ID를 남깁니다. attempt 수 또는 총 경과 시간 상한에 도달하면 자동 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"재시도 전에 요청 수정: {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를 한다면 외부에 같은 루프를 겹치지 마세요. 두 계층의 시도 횟수가 곱해질 수 있습니다.

## `NO_IMAGE`는 공식 생성 결과이지 HTTP 오류가 아닙니다

공식 `finishReason`과 앱의 일반 “이미지 없음” 메시지를 섞지 마세요. 현재 Google contract에서 `NO_IMAGE`는 candidate에 속하고, gateway가 별도로 `no_image`로 변환할 수 있습니다. upstream body를 보관하고 다음 순서로 봅니다.

1. `candidates` 없음: `promptFeedback`를 확인합니다.
2. `finishReason=NO_IMAGE`: `finishMessage`, `responseId`, `modelVersion`을 저장하고 출력 모달리티와 같은 route의 최소 probe를 확인합니다. 근거 없이 safety나 503으로 바꾸지 않습니다.
3. `IMAGE_SAFETY`, `IMAGE_PROHIBITED_CONTENT` 등: 정당한 입력을 검토하거나 정책 경계를 수용하며 retry를 우회 수단으로 쓰지 않습니다.
4. text part만 있음: 반환 텍스트와 모달리티를 확인합니다. text-only는 공식 `NO_IMAGE`와 다른 증거입니다.
5. `inlineData`가 있는데도 앱은 이미지 없음: parser, MIME, base64, 저장소 쓰기를 점검합니다.

```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 가이드](/ko/posts/gemini-image-silent-failure-image-safety-fix)에서 더 자세히 다룹니다. 인증 등 다른 HTTP 오류가 섞였다면 [Gemini API 오류 종합 가이드](/ko/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 중단을 증명하지 않습니다. 이미지나 downstream write를 만드는 작업은 retry 전에 request/response ID로 task 또는 artifact를 조회해 중복 생성을 막으세요. 제어하는 각 구간에서 시작, first byte, 종료, deadline, 종료 주체를 측정합니다.

같은 account/route/model에서 최소 probe와 원래 복잡한 입력을 비교합니다. 최소 요청만 성공하고 복잡한 요청이 client deadline에서 끝나면 올바른 계층의 timeout을 조정하거나 입력을 줄입니다. 둘 다 같은 503/504 구간에서 실패하면 큰 요청을 반복해도 진단 정보가 거의 늘지 않습니다.

![Nano Banana API의 제한된 retry, 중단, escalation 타임라인](https://www.aifreeapi.com/posts/ko/nano-banana2-api-error-codes/img/retry-timeline.webp)

## 중단 조건과 지원팀 전달 항목

400 등 non-retryable client error, 최소 probe와 raw response로 아직 분류하지 않은 `NO_IMAGE`, safety/policy `finishReason`, attempt/총시간 상한, 동일 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` 표시가 사라진 것만으로는 충분하지 않습니다.
