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_id

View on GitHub (pinned to 0b6a024f2f)

Solutions

  1. Open the file locally with PIL Image.open(io.BytesIO(bytes)) before uploading to reproduce and fix the decode failure.
  2. Re-export the image as PNG or JPEG and re-upload; avoid HEIC/SVG which stock Pillow cannot decode.
  3. Check the server log for the traceback the endpoint logs — it names the exact PIL error (e.g. UnidentifiedImageError).
  4. 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

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.