Skip to main content

Overview

gemini-3-pro-image-preview (i.e., Nano Banana Pro) enforces strict content safety controls and will reject non-compliant requests at multiple layers. A simple “generation failed” message doesn’t help users understand the problem. Good error handling needs to:
  • Accurately identify the rejection reason — distinguish between content violations, knowledge-base limits, and technical errors
  • Provide friendly user messaging — turn technical errors into understandable explanations
  • Offer actionable suggestions — tell users how to adjust their request to succeed
  • Retain complete technical details — for developer debugging
When a request returns HTTP 200 but no image, this is usually a safety judgment made on Google’s side. APIYI’s transparent proxy simply forwards the result as-is — we want our customers to generate images successfully too. The detection and messaging logic must be implemented on your application side.

Google Content Moderation Policy (2026 Update)

Google’s image generation uses a two-layer safety mechanism:
  1. Configurable filters: cover four categories — harassment, hate speech, sexually explicit content, and dangerous content — adjustable via safetySettings
  2. Built-in protections: always active for core harms (such as child safety) and cannot be disabled via parameters
Explicitly prohibited content includes: child sexual abuse and exploitation (CSAE), violent extremism/terrorism, non-consensual intimate imagery (NCII), self-harm, sexually explicit content, hate speech, and harassment and bullying.
In February 2026, after Nano Banana 2 launched, Google significantly tightened its policies around people and copyright, adding/strengthening the following frequent rejection scenarios (data as of May 2026 (UTC+8)):
  • Public figures / celebrities: photorealistic, recognizable real people
  • Face swap (faceswap)
  • Re-dressing / face-altering real people
  • Tampering with financial or order information
  • Well-known IP (such as Disney, since January 23, 2026)
  • Watermark removal and minor-related content
Still allowed: fictional characters, stylized portraits, and illustrated figures.
Google’s official policy documents (please copy and visit them yourself):
  • Generative AI Prohibited Use Policy: policies.google.com/terms/generative-ai/use-policy
  • Generative content common errors reference: ai.google.dev/api/generate-content

Three Core Diagnostic Indicators

Check in order of priority, from highest to lowest:

1. candidatesTokenCount (highest priority) ⭐

  • Location: response.usageMetadata.candidatesTokenCount
  • Meaning: the token count of candidate content generated by the API
  • Rule: a value of 0 means the request was rejected outright at the content moderation stage — no candidate content was even generated. This is the strictest rejection.
{
  "candidates": null,
  "usageMetadata": {
    "promptTokenCount": 271,
    "candidatesTokenCount": 0,
    "totalTokenCount": 271
  }
}

2. finishReason (second priority)

  • Location: response.candidates[0].finishReason
  • Rule: any value other than STOP indicates an abnormal completion that requires special handling
The latest image-related finishReason values (note that the Nano Banana series added image-specific values with the IMAGE_ prefix):
finishReasonMeaningUser-friendly message
STOPNormal completion-
IMAGE_SAFETYOutput-side image safety filterContent triggered the image safety policy
PROHIBITED_CONTENT / IMAGE_PROHIBITED_CONTENTProhibited contentContent violates the safety policy and was rejected
SAFETYSafety filterContent triggered the safety filter
RECITATION / IMAGE_RECITATIONCitation/copyright restrictionContent may involve a copyright issue
IMAGE_OTHER / NO_IMAGENo image/otherNo image could be generated; please adjust your prompt and retry
MAX_TOKENSLength exceededContent length exceeds the limit

3. Text rejection explanation (important)

  • Location: response.candidates[0].content.parts[].text
  • Rule: when finishReason is STOP but parts contains only text and no image data, the API has returned a rejection explanation rather than an image. The text may be in Chinese or English, for example:
我不能为你创建带有色情、不雅或冒犯性内容的图像。这违反了我们的安全政策。
I can't generate images that are sexually explicit.

Error Scenario Quick Reference

ScenarioDetection conditionTypical cause
Content moderation rejectioncandidatesTokenCount === 0Prompt/reference image contains sensitive content; earliest-stage rejection
Rejection during generationfinishReason !== 'STOP' and parts is emptyProhibited content, safety filter
Text rejection explanationfinishReason === 'STOP', has text but no imageSexually explicit content, non-compliant request
Knowledge-base limittext mentions a future year (2026+) or an unreleased productKnowledge base updated through January 2025
Prohibited featuretext contains keywords like watermark, faceswap, re-dressing, celebrity, etc.Prohibited features such as watermark removal/face swap/re-dressing/celebrities

Handling Flow (Decision Order)

Receive API response
  ├─ ① candidatesTokenCount === 0 ─→ Content moderation rejection
  ├─ ② candidates is empty ─────────→ API format error (system issue)
  ├─ ③ finishReason !== 'STOP' ────→ Rejection during generation (check mapping table)
  ├─ ④ content.parts is empty ─────→ Empty content (handle like finishReason)
  ├─ ⑤ Iterate parts to collect text and images
  ├─ ⑥ Has image ──────────────────→ ✅ Return success
  └─ ⑦ No image but has text ──────→ Show rejection explanation (optional keyword detection)
        └─ No text ───────────────→ Generic error + keep full response

Code Implementation (Core)

Combine the checks above into a single parsing function:
async function processGeminiResponse(data) {
  // ① Highest priority: rejected outright at the content moderation stage
  if (data.usageMetadata?.candidatesTokenCount === 0) {
    return {
      success: false,
      errorType: 'ZERO_CANDIDATES_TOKEN',
      userMessage: 'Your request was rejected during content moderation. Please revise it and try again.',
      devMessage: 'candidatesTokenCount: 0 - rejected by Google content moderation',
      rawResponse: data,
    };
  }

  // ② candidates is empty — usually a system/format issue
  if (!data.candidates || !data.candidates.length) {
    return {
      success: false,
      errorType: 'NO_CANDIDATES',
      userMessage: 'A system error occurred. Please try again later.',
      devMessage: 'candidates is null or an empty array',
      rawResponse: data,
    };
  }

  const candidate = data.candidates[0];

  // ③ finishReason is not STOP — rejected during generation
  if (candidate.finishReason && candidate.finishReason !== 'STOP') {
    const reasonMessages = {
      PROHIBITED_CONTENT: 'Content violates the safety policy and was rejected.',
      IMAGE_PROHIBITED_CONTENT: 'Content violates the safety policy and was rejected.',
      SAFETY: 'Content triggered the safety filter.',
      IMAGE_SAFETY: 'Content triggered the image safety policy.',
      RECITATION: 'Content may involve a copyright issue.',
      IMAGE_RECITATION: 'Content may involve a copyright issue.',
      NO_IMAGE: 'No image could be generated. Please adjust your prompt and try again.',
      IMAGE_OTHER: 'No image could be generated. Please adjust your prompt and try again.',
      MAX_TOKENS: 'Content length exceeds the limit.',
    };
    return {
      success: false,
      errorType: 'FINISH_REASON',
      finishReason: candidate.finishReason,
      userMessage: reasonMessages[candidate.finishReason] || `Request rejected: ${candidate.finishReason}`,
      devMessage: `finishReason: ${candidate.finishReason}`,
      rawResponse: data,
    };
  }

  // ④ content.parts is empty
  if (!candidate.content?.parts) {
    return {
      success: false,
      errorType: 'NO_PARTS',
      userMessage: 'Generation failed. Please try again.',
      devMessage: 'candidate.content.parts is empty',
      rawResponse: data,
    };
  }

  // ⑤ Iterate parts: ⚠️ always collect text first, then check thoughtSignature
  const images = [];
  const texts = [];
  for (const part of candidate.content.parts) {
    if (part.text && !part.text.startsWith('data:image/')) {
      texts.push(part.text);
    }
    if (part.inlineData?.data) {
      images.push(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
    }
  }

  // ⑥ Having an image means success
  if (images.length > 0) {
    return { success: true, images, texts };
  }

  // ⑦ No image but has text — show the rejection explanation
  if (texts.length > 0) {
    const textContent = texts.join('\n');
    return {
      success: false,
      errorType: 'TEXT_RESPONSE',
      userMessage: textContent,          // Use the text returned by the API directly
      detectedType: detectContentType(textContent),
      apiText: textContent,
      rawResponse: data,
    };
  }

  // ⑧ Fallback: never just say "unknown error"
  return {
    success: false,
    errorType: 'UNKNOWN',
    userMessage: 'Generation failed. Please check your prompt and try again.',
    devMessage: 'No image data or text response found',
    rawResponse: data,
  };
}
Smart keyword detection (optional, for more specific messaging):
function detectContentType(text) {
  const t = text.toLowerCase();
  const isRejection =
    t.includes("i can't generate") || t.includes('i cannot create') ||
    t.includes("i'm just a language model") || t.includes('我不能') || t.includes('无法生成');
  if (!isRejection) return null;

  if (t.includes('watermark')) return 'watermark_removal';
  if (t.includes('faceswap') || t.includes('face swap')) return 'faceswap';
  if (t.includes('sexually') || t.includes('explicit') || t.includes('色情') || t.includes('不雅')) return 'nsfw';
  return 'general_rejection';
}
The most common pitfall: a part carrying thoughtSignature may still contain important text. Always collect the text first, then decide whether to skip — otherwise the rejection explanation is lost and users only see “generation failed.”

Consumer-Friendly Messaging

Design principles: clear and concise, positive guidance, actionable, no blame. Recommended templates:
❌ Content does not meet requirements
Your request contains inappropriate content, so an image cannot be generated.
💡 Suggestion: Use healthy, positive descriptions; avoid sensitive topics; revise your prompt and try again.

❌ Feature not yet supported
This feature (e.g., watermark removal/face swap) is not supported. Please try a different editing approach.

❌ Content out of scope
The content you mentioned may be beyond the AI's knowledge range (updated through January 2025).
💡 Suggestion: Use common objects/concepts and avoid referencing future products.
Tiered display recommendations:
  • Consumer users: by default show only the friendly explanation + revision suggestion
  • Business / tool providers: by default expand technical details (finishReason, candidatesTokenCount, etc.)
  • Developers: provide an “expand/collapse” toggle to view the full JSON response

Best Practices

  1. Check strictly by priority: candidatesTokenCountfinishReasonparts → extract data → keyword detection
  2. Collect text before checking thoughtSignature to avoid losing the rejection explanation
  3. Keep the full response: development/testing tools should always save the raw JSON for troubleshooting
  4. Support Chinese and English rejection text: Google may return Chinese or English, so keyword matching must cover both
  5. Graceful degradation: give a specific message when smart detection succeeds; otherwise show the API text directly; otherwise use the friendly finishReason name; and only then fall back to a generic message
  6. Never show “unknown error”: always include an actionable suggestion or the full response

FAQ

Google’s safety filtering has randomness and context dependence: reference image content and how the prompt is combined all affect the judgment. Try adjusting the wording or using more indirect phrasing.
candidatesTokenCount: 0 or finishReason: PROHIBITED_CONTENT → content problem; Failed to fetch or an HTTP error → technical problem; an API text explanation → usually a content problem.
Tiered display: by default show the friendly explanation + revision suggestion; optionally expand technical details; in development mode show the full JSON response.
No. A mapping table plus a generic fallback is enough: reasonMessages[finishReason] || then display the raw value.