invoke-ai/InvokeAI · error · HTTPException
Failed to read image
Error message
Failed to read image
What it means
update_style_preset reads the uploaded image bytes and decodes them with PIL Image.open inside a thread. Any exception during decode (corrupt data, unsupported format, truncated file) is logged server-side and surfaced as HTTP 415 'Failed to read image'. The Content-Type check passed, but the bytes are not a decodable image.
Source
Thrown at invokeai/app/api/routers/style_presets.py:135
is_public = validated_data.is_public
except (json.JSONDecodeError, pydantic.ValidationError):
raise HTTPException(status_code=400, detail="Invalid preset data")
record = await asyncio.to_thread(_load_record_or_404, style_preset_id)
_assert_preset_write(record, current_user)
if image is not None:
if not image.content_type or not image.content_type.startswith("image"):
raise HTTPException(status_code=415, detail="Not an image")
contents = await image.read()
try:
pil_image = await asyncio.to_thread(Image.open, io.BytesIO(contents))
except Exception:
ApiDependencies.invoker.services.logger.error(traceback.format_exc())
raise HTTPException(status_code=415, detail="Failed to read image")
try:
await asyncio.to_thread(
ApiDependencies.invoker.services.style_preset_image_files.save, style_preset_id, pil_image
)
except ValueError as e:
raise HTTPException(status_code=409, detail=str(e))
else:
try:
await asyncio.to_thread(ApiDependencies.invoker.services.style_preset_image_files.delete, style_preset_id)
except StylePresetImageFileNotFoundException:
pass
preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt)
changes = StylePresetChanges(name=name, preset_data=preset_data, type=type, is_public=is_public)
style_preset_image = await asyncio.to_thread(
ApiDependencies.invoker.services.style_preset_image_files.get_url, style_preset_idView on GitHub (pinned to 0b6a024f2f)
Solutions
- Open the file locally with PIL Image.open(io.BytesIO(bytes)) before uploading to reproduce and fix the decode failure.
- Re-export the image as PNG or JPEG and re-upload; avoid HEIC/SVG which stock Pillow cannot decode.
- Check the server log for the traceback the endpoint logs — it names the exact PIL error (e.g. UnidentifiedImageError).
- Verify the file is fully transferred (compare byte sizes / checksums) and that you are uploading binary, not base64 text.
Example fix
// before
const res = await fetch(url);
await res.text(); // HTML error page with image/jpeg header
// after
const buf = await res.arrayBuffer();
if (!isPngOrJpeg(new Uint8Array(buf))) throw new Error("not a decodable image");
formData.append("image", new Blob([buf], { type: "image/png" })); Defensive patterns
Strategy: validation
Validate before calling
const MAGIC = {
png: [0x89, 0x50, 0x4e, 0x47],
jpeg: [0xff, 0xd8, 0xff],
};
function hasImageMagic(bytes) {
return Object.values(MAGIC).some((sig) => sig.every((b, i) => bytes[i] === b));
}
const bytes = new Uint8Array(await image.arrayBuffer());
if (!hasImageMagic(bytes)) throw new Error("file is not a PNG/JPEG payload"); Type guard
function isDecodableImage(bytes: Uint8Array): boolean {
const isPng = bytes[0] === 0x89 && bytes[1] === 0x50;
const isJpeg = bytes[0] === 0xff && bytes[1] === 0xd8;
return isPng || isJpeg;
} Try / catch
try {
await api.updateStylePreset({ stylePresetId, data, image });
} catch (e) {
if (e.status === 415 && e.detail === "Failed to read image") {
// server logged the PIL traceback; convert to PNG client-side and retry once
const png = await convertToPng(image);
await api.updateStylePreset({ stylePresetId, data, image: png });
} else throw e;
} Prevention
- Magic-byte-check files before upload; don't trust extensions or Content-Type.
- Convert HEIC/SVG/AVIF to PNG/JPEG client-side first.
- Verify uploads complete (byte-size or checksum comparison).
- Keep server Pillow build codec support in mind when accepting user files.
When it happens
Trigger: PATCH /style_presets/i/{id} with an image/* Content-Type but bytes PIL cannot open: truncated download, zero-byte file, wrong extension/actual format mismatch, unsupported format (HEIC on a Pillow build without heif), or text/HTML masquerading with an image Content-Type.
Common situations: Pillow without HEIF/WEBP plugin support; aborted uploads yielding partial files; base64 string uploaded instead of decoded binary; CDN error pages served with image/jpeg headers; SVG uploads (PIL cannot open SVG).
Related errors
AI-assisted analysis of invoke-ai/InvokeAI@0b6a024f2f (2026-08-29).
Data as JSON: /api/errors/77ebf0a76bd6db1b.
Report an issue: GitHub.