While building the automated wallpaper categorization feature for Beautiful HD Wallpapers (iOS/Android, 50 million+ downloads cumulative), I kept running into INVALID_ARGUMENT errors from the Gemini API when processing images in batches. My first instinct was to double-check the API key—but the real cause was somewhere else entirely.
Having been a solo app developer since 2014, I've spent more hours than I'd like to admit chasing down cryptic API errors. Here's the diagnostic process I wish I'd had from the start.
Why INVALID_ARGUMENT Is So Vague
INVALID_ARGUMENT (HTTP 400) follows Google Cloud's convention: something in the request is wrong, but the top-level message won't tell you exactly what. For image-related requests, the root cause almost always falls into one of three buckets:
- MIME type mismatch — for example, sending a PNG file with
image/jpegdeclared - Inline data size exceeded — base64-encoded data over 20 MB per request
- Malformed
contentsarray — wrong structure for multi-part requests
Let's walk through each one with the diagnostic and fix.
Cause 1: MIME Type Mismatch
Hardcoding MIME types based on file extension is the most common trap. A file named .jpg may actually be a WebP or PNG on the inside—especially when dealing with wallpaper images downloaded from various sources, which is exactly the situation I ran into.
import imghdr
from pathlib import Path
def get_mime_type(image_path: str) -> str:
"""Detect MIME type from file content, not extension."""
detected = imghdr.what(image_path)
mime_map = {
"jpeg": "image/jpeg",
"png": "image/png",
"webp": "image/webp",
"gif": "image/gif",
}
if detected and detected in mime_map:
return mime_map[detected]
# Fallback to extension-based detection
path = Path(image_path)
ext = path.suffix.lower()
fallback = {
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".webp": "image/webp"
}
return fallback.get(ext, "image/jpeg")
# Usage
mime = get_mime_type("/path/to/image.jpg")
print(f"Detected MIME: {mime}")
# → Detected MIME: image/jpegimghdr is part of the Python standard library, so no extra install needed. Note that Python 3.13+ deprecates imghdr—if you're on a recent Python version, python-magic is a solid drop-in replacement.
After switching to content-based detection for the wallpaper app, the INVALID_ARGUMENT rate dropped noticeably. Images that looked like JPEGs by filename turned out to be WebP files—a mismatch that the API rejected silently until I caught it in the exception message.
Cause 2: Inline Data Size Limit
The Gemini API enforces a 20 MB limit on inline data per request. Base64 encoding inflates file size by roughly 33%, so the practical ceiling for image files is around 15 MB.
4K wallpaper images can easily exceed 10–20 MB each. Trying to send 10 of them in a single batch request means 100+ MB of payload—well beyond what the API will accept.
import os
def check_image_size(image_path: str) -> dict:
"""Check whether an image should be sent inline or via File API."""
file_size = os.path.getsize(image_path)
base64_size = file_size * 4 / 3 # estimated after base64 encoding
return {
"file_path": image_path,
"file_size_mb": round(file_size / (1024 * 1024), 2),
"estimated_base64_mb": round(base64_size / (1024 * 1024), 2),
"use_file_api": base64_size > 15 * 1024 * 1024,
}
# Example output
result = check_image_size("/path/to/large_wallpaper.jpg")
print(result)
# → {'file_size_mb': 12.4, 'estimated_base64_mb': 16.5, 'use_file_api': True}Running this check before the batch starts prevents the frustrating situation where the API processes 8 out of 10 images before failing on the 9th. Any image where use_file_api is True should go through the File API instead.
Cause 3: Malformed contents Structure
When sending multiple images, the structure of the contents array matters. This mistake is especially common when calling the REST API directly rather than through the Python SDK.
# Wrong: text and image_data as separate top-level dicts
contents = [
{"text": "Classify each image by category"},
{"inline_data": {"mime_type": "image/jpeg", "data": encoded_img}},
]
# Correct: parts list inside a single content dict
contents = [
{
"parts": [
{"text": "Classify each image by category"},
{"inline_data": {"mime_type": "image/jpeg", "data": encoded_img}},
]
}
]The Python SDK handles this automatically when you pass a list of strings and image objects, but calling the REST API directly requires the correct nested structure. I hit this issue when I temporarily bypassed the SDK for a performance experiment—cost me about two hours before I spotted it.
Choosing Between File API and Inline Data
Here's a complete implementation that selects the right approach based on file size, combining all three fixes:
import google.generativeai as genai
import base64
import time
import os
def upload_or_encode_image(image_path: str, mime_type: str) -> dict:
"""
Route to File API for large images, inline data for small ones.
- 15 MB or smaller: inline_data (lower latency)
- Larger than 15 MB: File API (async upload, referenced by URI)
"""
file_size = os.path.getsize(image_path)
if file_size <= 15 * 1024 * 1024:
with open(image_path, "rb") as f:
data = base64.b64encode(f.read()).decode("utf-8")
return {"inline_data": {"mime_type": mime_type, "data": data}}
else:
uploaded = genai.upload_file(image_path, mime_type=mime_type)
# Wait until the file is active (usually a few seconds to a minute)
while uploaded.state.name == "PROCESSING":
time.sleep(5)
uploaded = genai.get_file(uploaded.name)
if uploaded.state.name == "FAILED":
raise ValueError(f"File upload failed: {uploaded.name}")
return {"file_data": {"mime_type": mime_type, "file_uri": uploaded.uri}}
def analyze_images_batch(image_paths: list, prompt: str) -> str:
"""Safely analyze multiple images in a single request."""
model = genai.GenerativeModel("gemini-2.5-flash")
parts = [{"text": prompt}]
for path in image_paths:
mime = get_mime_type(path)
part = upload_or_encode_image(path, mime)
parts.append(part)
response = model.generate_content({"parts": parts})
return response.text
# Example
images = [
"/wallpapers/nature_4k.jpg",
"/wallpapers/city_night.webp",
]
result = analyze_images_batch(
images,
"Return the category for each image as JSON: [{category, subcategory}]"
)
print(result)
# → [{"category": "nature", "subcategory": "landscape"}, {"category": "city", "subcategory": "night"}]For the wallpaper app, this pattern handled around 30,000 images: 4K originals went through File API, thumbnails stayed inline. INVALID_ARGUMENT errors essentially disappeared.
Reading the Error Message
When the exception occurs, catching and printing the message gives a strong hint about which of the three causes is at play:
import google.api_core.exceptions
try:
response = model.generate_content(contents)
except google.api_core.exceptions.InvalidArgument as e:
print(f"Message: {e.message}")
# "Request payload size exceeds the limit" → size problem
# "Invalid MIME type" → MIME type problem
# "Invalid value at 'contents[0].parts'" → structure error
# "Could not process Image:" → image read failureIn most cases, one of these four messages points directly to the fix needed.
Diagnostic Checklist
When INVALID_ARGUMENT appears on a batch image request, work through these steps in order:
- Run
check_image_size()on all images upfront and identify anything over 15 MB - Verify MIME type is detected from file content, not just the file extension
- If calling the REST API directly, confirm the
partslist is nested inside a singlecontentsentry - Catch the exception and print
e.messageto read the specific cause
The safest approach when setting this up for the first time: start with a single small image (under 1 MB), confirm it works, then gradually increase batch size and image dimensions. Trying to debug a batch of 50 high-resolution images all at once makes it much harder to isolate the cause.
I hope this saves someone the hours I spent tracking down these errors.