can1357/oh-my-pi · error · InvalidImageDataError

${options.resolvedPath} is not a decodable ${options.image.m

Error message

${options.resolvedPath} is not a decodable ${options.image.mimeType} image: ${decodeFailure}

What it means

loadInMemoryImageInput proactively decodes the image via imageDecodeFailureReason before it can enter the transcript, because an undecodable payload would be rejected by the LLM provider and fail the entire request. If decoding fails it throws InvalidImageDataError naming the path, mime type, and the decoder's reason.

Source

Thrown at packages/coding-agent/src/utils/image-loading.ts:300

	resolvedPath: string;
	textNotePrefix: string;
	autoResize: boolean;
	maxBytes: number;
	excludeWebP: boolean | undefined;
}

async function loadInMemoryImageInput(options: LoadInMemoryImageInputOptions): Promise<LoadedImageInput> {
	const inputBytes = Buffer.byteLength(options.image.data, "base64");
	if (inputBytes > options.maxBytes) {
		throw new ImageInputTooLargeError(inputBytes, options.maxBytes);
	}

	// Decode before anything else: a payload that cannot be decoded is rejected
	// by the provider for the whole request, so it must fail here — where the
	// caller still has a path to act on — instead of entering the transcript.
	const decodeFailure = await imageDecodeFailureReason(options.image);
	if (decodeFailure !== null) {
		throw new InvalidImageDataError(options.resolvedPath, options.image.mimeType, decodeFailure);
	}

	let outputData = options.image.data;
	let outputMimeType = options.image.mimeType;
	let outputBytes = inputBytes;
	let dimensionNote: string | undefined;

	const shouldReencodeWebP = options.excludeWebP === true && options.image.mimeType === "image/webp";
	if (options.autoResize || shouldReencodeWebP) {
		try {
			const resized = await resizeImage(options.image, { excludeWebP: options.excludeWebP });
			outputData = resized.data;
			outputMimeType = resized.mimeType;
			outputBytes = resized.buffer.byteLength;
			dimensionNote = formatDimensionNote(resized);
		} catch {
			// Keep the original image when resize fails.
		}

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-export or convert the file to PNG or JPEG and retry.
  2. Verify the file opens in an image viewer; if not, re-download or re-capture it.
  3. Fix the extension/mimeType so it matches the actual encoding.
  4. Read the decoder's reason in the error message to distinguish truncation from unsupported format.

Example fix

// before: await loadImageInput("/tmp/photo.heic");  // after: convert to JPEG first (e.g. via sips/ffmpeg/sharp), then await loadImageInput("/tmp/photo.jpg");
Defensive patterns

Strategy: validation

Validate before calling

const failure = await imageDecodeFailureReason({ type: "image", data: base64, mimeType }); if (failure !== null) throw new Error(`cannot send image: ${failure}`);

Type guard

function looksPng(buf: Uint8Array): boolean { return buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47; } function looksJpeg(buf: Uint8Array): boolean { return buf[0] === 0xff && buf[1] === 0xd8; }

Try / catch

try { const img = await loadImageInput(path); } catch (err) { if (err instanceof InvalidImageDataError) { logger.warn("skipping undecodable image", { path: path }); return null; } throw err; }

Prevention

When it happens

Trigger: Calling loadInMemoryImageInput (via loadImageInput, loadSvgImageInput, or loadImageAttachmentInput) with bytes Bun.Image cannot decode: truncated files, corrupt downloads, zero-byte files, or files whose actual format mismatches the declared mimeType (e.g. a BMP renamed .png).

Common situations: Screenshot tool wrote a partial file; user attached a renamed or unsupported format; download interrupted; image is an exotic codec (HEIC, AVIF) the decoder lacks.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/1df69189947e4f2c. Report an issue: GitHub.