Nano Banana 2.1 returned no image? Inspect the response before retrying

Diagnose Nano Banana 2.1 responses with no image. Inspect image parts, finish reasons, HTTP errors and uncertain timeouts before making another request.

Black ink gear on a pale card with geometric accents and a Nano Banana 2.1 title.

A successful HTTP request is not the same thing as an image-bearing response. With Gemini image generation, the image is carried in a candidate’s content.parts[].inlineData (or inline_data), alongside possible text parts. An application that reads only response.text can report “no image” even when the response contains one.

This guide is for the Ofox model ID google/gemini-nano-banana-2.1. Ofox publishes this article and operates the gateway used in the test described below. The instructions distinguish what the gateway returned in that test from what Google documents generally; they do not assume every failed request has the same cause.

First, establish whether the response contains an image

For a native Gemini request, inspect each candidate’s parts and look for an inlineData object with an image MIME type and nonempty data. Do not treat an empty or absent text field as proof that generation failed. Also record the HTTP status, candidate finishReason, and promptFeedback before deciding what to do next.

Here is a redacted structural excerpt, not a copy of the image bytes, from a successful Ofox gateway test on October 13, 2026:

{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "inlineData": {
              "mimeType": "image/png",
              "data": "[base64 image bytes omitted]"
            }
          }
        ]
      },
      "finishReason": "STOP"
    }
  ]
}

The tested request returned HTTP 200 and a 1024 × 1024 PNG. The excerpt omits other response material; the test’s reported usage included 54 input tokens, 80 reported text-output tokens, 1,120 image-output tokens, and 639 thought tokens. Those figures describe that request, not a required response shape or a fixed charge. Google’s image-generation documentation describes image-bearing parts, while its model documentation describes the model’s capabilities.

Make one inspectable request

Set OFOX_API_KEY in your environment, then send the exact poster prompt used in the test. The model ID matters: google/gemini-nano-banana-2.1 is not the older google/gemini-3.1-flash-image, nor a “Lite” variant.

cat > request.json <<'JSON'
{
  "contents": [
    {
      "role": "user",
      "parts": [
        {
          "text": "Create a square minimalist event poster on warm cream paper. Large exact headline: ORCHARD MARKET. Smaller exact line: SATURDAY 10 AM. Draw three red apples in a blue ceramic bowl below the text. No other text, no logos, no border."
        }
      ]
    }
  ],
  "generationConfig": {
    "responseModalities": ["TEXT", "IMAGE"],
    "imageConfig": {
      "aspectRatio": "1:1",
      "imageSize": "1K"
    }
  }
}
JSON

curl --silent --show-error \
  --output response.json \
  --write-out 'HTTP %{http_code}\n' \
  -H "x-goog-api-key: ${OFOX_API_KEY:?Set OFOX_API_KEY first}" \
  -H 'Content-Type: application/json' \
  --data-binary @request.json \
  'https://api.ofox.io/gemini/v1beta/models/google/gemini-nano-banana-2.1:generateContent'

The HTTP code printed by curl is important: save it with the response rather than assuming that the presence of response.json means the call succeeded. Keep request and response files containing confidential prompts or image data out of public logs and repositories. For endpoint setup beyond this example, see the Nano Banana 2.1 API guide.

Run an offline diagnostic before changing the prompt

Save the following as diagnose_gemini_image.py. It reads a saved JSON response; it does not call an API. Supply the HTTP code recorded by your client with --http-status. If a client timed out without a usable response, use --timeout instead. The output is deliberately limited to diagnostic fields: it never prints a key, raw error message, prompt, or base64 image data.

#!/usr/bin/env python3
import argparse
import json
import sys
from pathlib import Path

def field(obj, camel, snake):
    if not isinstance(obj, dict):
        return None
    return obj.get(camel, obj.get(snake))

def safe_feedback(obj):
    if not isinstance(obj, dict):
        return None
    ratings = field(obj, "safetyRatings", "safety_ratings")
    result = {"blockReason": field(obj, "blockReason", "block_reason")}
    if isinstance(ratings, list):
        result["safetyRatings"] = [
            {
                "category": r.get("category"),
                "probability": r.get("probability"),
            }
            for r in ratings if isinstance(r, dict)
        ]
    return result

def diagnose(body, http_status=None, timed_out=False):
    # Accept either a native response or {"body": <native response>}.
    if isinstance(body, dict) and isinstance(body.get("body"), dict):
        if http_status is None:
            http_status = body.get("status_code", body.get("httpStatus"))
        body = body["body"]

    result = {
        "status": "no_image",
        "httpStatus": http_status,
        "imagesCount": 0,
        "finishReasons": [],
        "promptFeedback": safe_feedback(
            field(body, "promptFeedback", "prompt_feedback")
        ),
    }

    if timed_out:
        result["status"] = "transport_uncertain"
        return result

    if http_status is not None and not 200 <= http_status < 300:
        result["status"] = (
            "http_error_check_retryability"
            if http_status == 429 or 500 <= http_status <= 599
            else "http_error"
        )
        return result

    candidates = body.get("candidates", []) if isinstance(body, dict) else []
    if not isinstance(candidates, list):
        candidates = []

    for candidate in candidates:
        if not isinstance(candidate, dict):
            continue
        reason = field(candidate, "finishReason", "finish_reason")
        if reason is not None:
            result["finishReasons"].append(reason)
        content = candidate.get("content")
        parts = content.get("parts", []) if isinstance(content, dict) else []
        if not isinstance(parts, list):
            continue
        for part in parts:
            inline = field(part, "inlineData", "inline_data")
            if not isinstance(inline, dict):
                continue
            mime = field(inline, "mimeType", "mime_type")
            if (isinstance(mime, str) and mime.startswith("image/")
                    and isinstance(inline.get("data"), str)
                    and inline["data"]):
                result["imagesCount"] += 1

    if result["imagesCount"]:
        result["status"] = "image_present"
    elif (result["promptFeedback"] or {}).get("blockReason"):
        result["status"] = "blocked"
    elif any(
        r in {"SAFETY", "PROHIBITED_CONTENT", "SPII", "RECITATION"}
        for r in result["finishReasons"]
    ):
        result["status"] = "blocked"
    elif "MAX_TOKENS" in result["finishReasons"]:
        result["status"] = "max_tokens_no_image"
    return result

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("response_json", type=Path, nargs="?")
    parser.add_argument("--http-status", type=int)
    parser.add_argument("--timeout", action="store_true")
    args = parser.parse_args()
    if args.timeout:
        print(json.dumps(diagnose({}, args.http_status, timed_out=True)))
        return 0
    if args.response_json is None:
        parser.error("Provide response_json unless using --timeout")
    try:
        body = json.loads(args.response_json.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError):
        print("Cannot read a valid JSON response", file=sys.stderr)
        return 2
    print(json.dumps(
        diagnose(body, args.http_status, args.timeout),
        separators=(",", ":")
    ))
    return 0

if __name__ == "__main__":
    sys.exit(main())

For example, python3 diagnose_gemini_image.py response.json --http-status 200 reports whether image parts were found. imagesCount counts declared, nonempty image parts; it does not decode the data or certify that an image file is valid. Preserve the original response for closer local inspection if extraction fails.

You can check the diagnostic’s branching without spending API credit. These are synthetic fixtures, not simulated model tests:

printf '%s\n' \
  '{"candidates":[{"content":{"parts":[{"inline_data":{"mime_type":"image/png","data":"aGVsbG8="}}]},"finish_reason":"STOP"}]}' \
  > fixture-image.json
python3 diagnose_gemini_image.py fixture-image.json --http-status 200
# {"status":"image_present","httpStatus":200,"imagesCount":1,"finishReasons":["STOP"],"promptFeedback":null}

printf '%s\n' \
  '{"candidates":[{"finishReason":"MAX_TOKENS","content":{"parts":[]}}]}' \
  > fixture-limit.json
python3 diagnose_gemini_image.py fixture-limit.json --http-status 200
# {"status":"max_tokens_no_image","httpStatus":200,"imagesCount":0,"finishReasons":["MAX_TOKENS"],"promptFeedback":null}

The fixture’s aGVsbG8= is merely nonempty base64 text, not a PNG. That distinction is useful: detecting an image part is the first check; decoding and validating the file is the next.

If the diagnostic finds an image, extract it rather than retrying generation. This small local extractor saves all image parts and does not print their contents:

import base64
import json
from pathlib import Path

response = json.loads(Path("response.json").read_text(encoding="utf-8"))
saved = 0

for candidate in response.get("candidates", []):
    for part in candidate.get("content", {}).get("parts", []):
        inline = part.get("inlineData", part.get("inline_data", {}))
        mime = inline.get("mimeType", inline.get("mime_type", ""))
        data = inline.get("data")
        if not (mime.startswith("image/") and data):
            continue
        extension = {"image/png": ".png", "image/jpeg": ".jpg"}.get(mime)
        if extension is None:
            raise ValueError(f"Inspect unsupported image MIME type: {mime}")
        image = base64.b64decode(data, validate=True)
        saved += 1
        Path(f"image-{saved}{extension}").write_bytes(image)

print(f"Saved {saved} image(s)")

This example supports PNG and JPEG explicitly rather than guessing a filename for every MIME type. If decoding fails, retain the HTTP status and response metadata, but do not paste the base64 into a support ticket or public issue.

Interpret a response with no image

The appropriate next action depends on where the request failed. Google’s API troubleshooting guide provides general error guidance; the table below applies it as a practical triage sequence for this gateway call, without treating every error as retryable.

ObservationInspect firstAppropriate action
400Request JSON, responseModalities, imageConfig, and field namesCorrect an invalid parameter or payload. Repeating the identical request is unlikely to help.
401Whether the gateway key was supplied to x-goog-api-key and is validFix authentication without exposing the key in logs.
403Account or model permissions and any returned error classificationResolve access or policy restrictions; do not relabel it as a transient image failure.
404The exact endpoint and model IDCheck that the path uses google/gemini-nano-banana-2.1. An old 404 report for another ID or date does not establish present availability.
429Rate-limit versus credit/quota information in the error and account stateBack off only if it is a retryable rate limit. More retries will not fix exhausted credit or an account limit requiring action.
5xxGateway/server status and whether any request record or result existsA bounded retry may be reasonable, but first consider whether the request could already have been processed.
Client timeout or lost connectionLocal request ID, gateway request record if available, and any saved responseOutcome is uncertain. Do not assume “no image generated” or “not billed” merely because the client did not receive it.

A 200 with STOP and no image part deserves inspection of all candidates and parts, the requested modalities, and the saved JSON. A 200 with MAX_TOKENS and no image is a different clue: output may have ended before an image was delivered. A user report on Google’s developer forum describes this symptom; it is an unverified user report, not a failure we reproduced in the six successful requests here. A reported token count below a configured limit is not, on its own, proof of a model bug; limits, response accounting, and the meaning of the particular counter need checking before drawing that conclusion.

If promptFeedback has a block reason or a candidate ends for a safety-related reason, treat that as a policy result. Review whether the request is appropriate under the applicable Gemini API terms; do not use retries or prompt reformulations as a way to evade a restriction. For setup details, use the API guide.

Retry narrowly, and preserve context on follow-up turns

Do not put every non-image result into an automatic retry loop. Correct 400, 401, 403, and 404 causes first. For an identified retryable 429 or 5xx, use a small, bounded number of attempts with increasing delay and jitter, and honor any server-supplied retry guidance your client supports. After a timeout, inspect the request record before resubmitting where possible: a lost response leaves both generation and billing status uncertain. The pricing guide explains why an additional generated image is not cost-free simply because the first was unwanted.

For editing, “retry” and “follow-up turn” are also different operations. A follow-up must include the prior conversation content, including the model’s returned content parts. Do not rebuild the previous model message from visible text alone: that can drop the image and any thought signatures. The tested three-step chain sent the previous candidates[0].content intact before each new user turn. Its next prompts were:

Change only the headline from ORCHARD MARKET to ORCHARD FAIR. Keep SATURDAY 10 AM exactly unchanged. Preserve the three red apples, blue bowl, cream background, composition and all other pixels as closely as possible.

Now change only the bowl from blue to dark green. Preserve ORCHARD FAIR, SATURDAY 10 AM, the three red apples and the layout.

In Python, the essential history operation is:

# previous_response is the complete parsed response from the preceding turn.
model_content = previous_response["candidates"][0]["content"]
contents.append(model_content)  # Keep its parts and signatures intact.
contents.append({
    "role": "user",
    "parts": [{"text": next_prompt}]
})

Save the updated response before adding another turn. If the prior response was blocked, incomplete, or lacks the content needed for an edit, do not pretend this is an equivalent continuation. For editing expectations, including what “preserve” does and does not guarantee, see the poster-editing walkthrough and the older product-preservation guide. Multi-reference workflows have their own trade-offs in single versus two product references.

What the successful test establishes—and what it does not

On October 13, 2026, the native Gemini route through Ofox returned HTTP 200, STOP, and a 1024 × 1024 PNG for each of six actual requests: a poster, headline edit, and bowl-color follow-up on Nano Banana 2.1, and the same three prompts on the older Nano Banana 2. The 2.1 outputs visibly retained the requested English wording and objects across the edits. These are useful success-response examples, not a failure reproduction, a reliability rate, or a production-quality ranking; there were no repeated trials, and the models may use different defaults. The 2.1 versus 2 comparison discusses that limited comparison.

The public Ofox Nano Banana 2.1 model page listed the model when checked. Its public interface is availability evidence, not evidence that its login-required Playground generated a result, and no authenticated Agent model selection was tested. Conversely, a historical 404 for the 2.1 route before it was available does not invalidate the observed 2.1 responses. Check the current model ID and your own access rather than substituting one observation for the other.

A final cost caution applies when debugging: the tested 1K 2.1 poster reported 1,120 image-output tokens. At the observed catalog rate of $30 per million image-output tokens, that image component would be $0.0336, not a verified invoice total; input, text, thinking, and other applicable charges can add to it. Neither a timed-out call nor a response your application failed to parse establishes zero billable work. Save the status and diagnostic fields first, then decide whether a new request is warranted.