{"record":{"id":"a032222eab9d3e3c","repo":"BerriAI/litellm","slug":"url-does-not-point-to-a-valid-image-content-type","errorCode":null,"errorMessage":"URL does not point to a valid image (content-type: {content_type})","messagePattern":"URL does not point to a valid image \\(content-type: (.+?)\\)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/prompt_templates/factory.py","lineNumber":3190,"sourceCode":"\n\ndef _load_image_from_url(image_url):\n    try:\n        from PIL import Image\n    except Exception:\n        raise Exception(\"image conversion failed please run `pip install Pillow`\")\n    from io import BytesIO\n\n    try:\n        # Send a GET request to the image URL\n        client: Final = HTTPHandler(concurrent_limit=1)\n        response: Final[httpx.Response] = safe_get(client, image_url)\n        response.raise_for_status()  # Raise an exception for HTTP errors\n\n        # Check the response's content type to ensure it is an image\n        content_type: Final = response.headers.get(\"content-type\")\n        if not content_type or \"image\" not in content_type:\n            raise ValueError(f\"URL does not point to a valid image (content-type: {content_type})\")\n\n        # Load the image from the response content\n        return Image.open(BytesIO(response.content))\n\n    except Exception as e:\n        raise e\n\n\ndef _gemini_vision_convert_messages(messages: list):\n    \"\"\"\n    Converts given messages for GPT-4 Vision to Gemini format.\n\n    Args:\n        messages (list): The messages to convert. Each message can be a dictionary with a \"content\" key. The content can be a string or a list of elements. If it is a string, it will be concatenated to the prompt. If it is a list, each element will be processed based on its type:\n            - If the element is a dictionary with a \"type\" key equal to \"text\", its \"text\" value will be concatenated to the prompt.\n            - If the element is a dictionary with a \"type\" key equal to \"image_url\", its \"image_url\" value will be added to the list of images.\n\n    Returns:","sourceCodeStart":3172,"sourceCodeEnd":3208,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/prompt_templates/factory.py#L3172-L3208","documentation":"Raised by _load_image_from_url after it fetched the URL successfully (or the response lacked headers): the HTTP response's content-type header is missing or does not contain 'image', so the bytes are not an image. This protects the Pillow path from trying to open HTML error pages and the like.","triggerScenarios":"Image URLs that return text/html (login pages, S3 XML errors, 404 pages served as 200), endpoints with no content-type header, or presigned URLs that expired and redirect to an error document; reached from Gemini vision conversion of https:// URLs.","commonSituations":"Expired presigned S3/GCS URLs; intranet URLs behind auth proxies that return HTML; CDN URLs requiring headers your request lacks; typos in domains that resolve to parked pages.","solutions":["curl -I <url> and confirm content-type starts with image/; if not, fix the URL or its auth","Regenerate expired presigned URLs before each request","Download the image yourself (with proper auth headers) and pass it as a base64 data URI instead"],"exampleFix":"# before\nimage_url = \"https://my-bucket.s3.amazonaws.com/cat.png?X-Amz-Signature=...\"  # expired -> XML error page\nmsg = {\"type\": \"image_url\", \"image_url\": {\"url\": image_url}}\n\n# after\nimport boto3, base64\ns3 = boto3.client(\"s3\")\nobj = s3.get_object(Bucket=\"my-bucket\", Key=\"cat.png\")\nb64 = base64.b64encode(obj[\"Body\"].read()).decode()\nmsg = {\"type\": \"image_url\", \"image_url\": {\"url\": f\"data:image/png;base64,{b64}\"}}","handlingStrategy":"validation","validationCode":"import httpx\n\ndef url_is_image(url: str) -> bool:\n    r = httpx.head(url, follow_redirects=True, timeout=10)\n    if r.status_code == 405:\n        r = httpx.get(url, headers={\"Range\": \"bytes=0-0\"}, follow_redirects=True, timeout=10)\n    return r.headers.get(\"content-type\", \"\").startswith(\"image/\")","typeGuard":null,"tryCatchPattern":"try:\n    litellm.completion(model=model, messages=messages)\nexcept Exception as e:\n    if \"URL does not point to a valid image\" in str(e):\n        data = httpx.get(local_fetch(url), timeout=30).content  # fetch with auth locally\n        messages = inject_data_uri(messages, \"data:image/png;base64,\" + base64.b64encode(data).decode())\n        litellm.completion(model=model, messages=messages)\n    else:\n        raise","preventionTips":["Verify content-type with a HEAD request before passing URLs","Regenerate presigned URLs close to request time","Fetch auth-protected images yourself and inline base64"],"tags":["vision","http","content-type","url"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}