◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-05-29Intermediate

Diagnosing invalid_request_error When You Pass an Image URL to the Claude API

When the Claude API rejects an image you passed via `source.type: url`, the root cause almost always lives in one of four buckets: scheme, MIME, size, or reachability. Here is the diagnostic order I use in production.

claude-api83vision8image-inputtroubleshooting92anthropic12

If you are classifying product images or generated artwork through the Claude API as part of an app pipeline, you have probably seen a previously stable code path start returning 400 invalid_request_error: Could not process image the moment you switched from base64 to URL inputs. I get this question often.

In my own case, the first three hours of a wallpaper-classification job — about 800 images — were rejected outright the day I migrated from base64 to URL sources. The cause turned out to be embarrassingly mundane: my CDN was returning a 302 for a subset of regions, and Anthropic's fetcher could not follow the chain to a 200.

These errors converge on four categories: URL scheme, MIME type, file size, or fetch-time reachability. Going through them in order usually clears the failure inside ten minutes. Drawing on lessons from running iOS and Android apps since 2014, here is the diagnostic flow I rely on.

What the error messages typically look like

When you supply an image via URL, the API returns one of three families of errors.

{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "messages.0.content.0.source.url: Could not process image https://example.com/photo.jpg"
  }
}
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "messages.0.content.0.source.url: Image must be a valid HTTPS URL"
  }
}
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "Image exceeds 5 MB size limit"
  }
}

The wording narrows things down quickly. Could not process means "the URL fetched, but the bytes were rejected"; must be a valid HTTPS URL means "the URL itself is malformed or non-HTTPS"; exceeds X MB is obviously a size issue.

Starting point: curl the URL before you trust the API call

Staring at the error string alone never gives me confidence. The first thing I do is reproduce, from my own terminal, what Anthropic's fetcher sees.

curl -sIL -A "Anthropic-Image-Fetcher/1.0" "https://example.com/photo.jpg"

I check four things: did the response chain land on HTTP/2 200, what is the Content-Type, what is the Content-Length, and how long is the redirect chain. Without those four anchors, no API-side fix will reliably work.

Cause 1: URL is still HTTP

The Claude API requires HTTPS for URL-sourced images. When images come from a CMS or self-hosted media server, it is shockingly common to find a hardcoded http:// left over from local development.

# NG
content = [{
    "type": "image",
    "source": {"type": "url", "url": "http://example.com/photo.jpg"},
}]

The fix is mechanical: force https://. I usually wire in a small normalizer so future code paths can never reintroduce the bug.

from urllib.parse import urlparse, urlunparse
 
def to_https(url: str) -> str:
    p = urlparse(url)
    if p.scheme == "https":
        return url
    if p.scheme == "http":
        return urlunparse(p._replace(scheme="https"))
    raise ValueError(f"Unsupported scheme: {p.scheme}")

If you use presigned URLs from S3 or R2, lock the bucket policy down so only HTTPS URLs can be issued.

Cause 2: MIME type is unsupported

Claude accepts image/jpeg, image/png, image/gif, and image/webp for URL-sourced images. HEIC, AVIF, BMP, TIFF, and SVG are rejected.

The sneaky variant of this bug is "the extension is .jpg but the CDN serves application/octet-stream." The API honors the response header, not the extension.

# expected: Content-Type: image/jpeg
curl -sI "https://example.com/photo.jpg" | grep -i content-type

If you see application/octet-stream or binary/octet-stream, fix the upload-side metadata. On Cloudflare R2 and AWS S3 I make sure every upload carries an explicit --content-type image/jpeg.

If your images flow in directly from iPhones, expect HEIC files to slip through unless your pipeline re-encodes to JPEG or PNG using heif-convert or Sharp before publication. Skipping that step guarantees a quarterly incident.

Cause 3: File exceeds the 5 MB limit

URL-sourced images max out at 5 MB per file and 8000 px on the longest side. My wallpaper apps deal with 4K source assets, so a raw image will routinely exceed the limit.

I gate the upload with a HEAD request first.

import httpx
 
async def is_within_limit(url: str, max_mb: int = 5) -> bool:
    async with httpx.AsyncClient(follow_redirects=True, timeout=10) as c:
        r = await c.head(url)
        size = int(r.headers.get("content-length", "0"))
        return 0 < size <= max_mb * 1024 * 1024

If the image is too large, route through an on-the-fly variant that downsizes to 2000 px wide at JPEG quality 85. Cloudflare Images' ?width=2000&quality=85 style URL is convenient for this.

Cause 4: URL requires auth or fails on redirect

If your CDN restricts access by IP, validates Referer/Origin headers, or relies on signed URLs whose expiry has lapsed, Anthropic's fetcher will hit 401/403. Redirect chains that bounce through HTML login pages (Cloudflare Access, for example) cannot be followed.

The cleanest verification is a plain curl with no extra headers, exactly mimicking an unauthenticated client.

curl -sI -L "https://example.com/photo.jpg" | head -5

If you see HTTP/2 401, HTTP/2 403, or a HTTP/2 302 chain that never lands on 200, the URL form simply cannot be used as is. Options:

  • Mirror the image to a public bucket and clean it up 24 hours later via a lifecycle rule.
  • Issue presigned URLs with at least 5 minutes of remaining validity — fetches are not always immediate.
  • Give up on URL sources and download the bytes yourself, then send them as source.type: base64.

The fetcher will follow up to five HTTP redirects, but redirects to HTML login flows are not followed. Pre-resolve to the final URL before passing it in.

Cause 5: On-the-fly transforms are too slow on first hit

A subtle gotcha: when you pass something like https://example.com/photo.jpg?width=2000&quality=85, the CDN's transform worker can take 5–10 seconds the first time, and the Anthropic fetcher gives up.

In my pipelines I warm the cache from my own client before invoking the API.

async def warmup_and_call(url: str):
    async with httpx.AsyncClient(timeout=15) as c:
        await c.get(url)  # prime the CDN cache
    response = await client.messages.create(...)

It is the most boring fix imaginable — "go grab it yourself first" — but in my classification pipeline it dropped the failure rate from roughly 2% to under 0.1%.

Fallback when URL sources truly will not work

When every diagnostic above passes and the API still refuses the URL, I switch to base64. The code grows slightly, but the failure surface moves from the opaque Anthropic side to your own client, where you can log and retry.

import base64
import httpx
import anthropic
 
async def image_block_from_url(url: str) -> dict:
    async with httpx.AsyncClient(follow_redirects=True, timeout=15) as c:
        r = await c.get(url)
        r.raise_for_status()
    return {
        "type": "image",
        "source": {
            "type": "base64",
            "media_type": r.headers["content-type"].split(";")[0].strip(),
            "data": base64.standard_b64encode(r.content).decode("ascii"),
        },
    }

base64 inflates the payload by about 33%, but it removes the fetch-side unknowns entirely. In production I run a two-tier strategy: try the URL form first, fall back to base64 on failure.

Closing thought

Because URL-sourced invalid_request_error has limited visibility on Anthropic's side, the fastest path to a fix is usually to keep a curl -sIL snapshot of the failing URL handy. The moment you find yourself staring at the error string and guessing, run a HEAD from your own machine — within five minutes you will know whether the problem lives in the image origin or in the API call.

If you run into related troubleshooting around invalid_request shapes, you may also find Invalid request error messages for the Claude API and Diagnosing prompt cache misses on the Claude API useful.

Thank you for reading — I hope this saves you a frustrating afternoon.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ API & SDK2026-09-22
Character counts won't predict PDF input tokens — bracket the cost per page before you send
How to estimate the input tokens of a PDF before sending it to the Claude API, by bracketing from page count rather than character count. Includes a measured 6.6x gap on a nine-page A4 document, the two separate increases that come with current models, and the count_tokens limitation that bites when you use file_id.
⬡ API & SDK2026-05-22
Designing a Model-Selection Fallback That Survives this model is currently unavailable on Claude API
When Claude API answers 'this model is currently unavailable', treating it like a 529 Overloaded or a rate limit sends you the long way round. Here is how I tell the rejections apart by SDK exception class, switch model IDs per request, and the assumptions I had to fix along the way.
⬡ API & SDK2026-04-26
How I Cut My Claude API Bill in Half With Prompt Caching
Done right, Anthropic's prompt caching can roughly halve your monthly API spend on workloads with long, repeated system prompts. Here is the design playbook I use after six months in production, including the arithmetic that links hit rate to the bill and the break-even points (about 22% on the 5-minute TTL, 53% on the 1-hour TTL).