{"record":{"id":"a89fb8188fdfb78d","repo":"Hmbown/CodeWhale","slug":"image-mime-does-not-match-its-content","errorCode":null,"errorMessage":"image {} MIME does not match its content","messagePattern":"image (.+?) MIME does not match its content","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/image_attach.rs","lineNumber":156,"sourceCode":"                );\n            }\n            let bytes = STANDARD\n                .decode(&image.data_base64)\n                .map_err(|_| anyhow::anyhow!(\"image {} has invalid base64\", index + 1))?;\n            if bytes.len() > per_image_limit {\n                bail!(\n                    \"image {} exceeds the {} MiB limit\",\n                    index + 1,\n                    per_image_limit / (1024 * 1024)\n                );\n            }\n            total = total.saturating_add(bytes.len());\n            if total_limit.is_some_and(|limit| total > limit) {\n                bail!(\"images exceed the 5 MiB total limit\");\n            }\n            let attached = encode_image_bytes(&bytes, &format!(\"image {}\", index + 1))?;\n            if image.mime != attached.media_type {\n                bail!(\"image {} MIME does not match its content\", index + 1);\n            }\n            decode_and_guard_image(&bytes)?;\n            // Standard padded base64 is the one replay representation.\n            if STANDARD.encode(&bytes) != image.data_base64 {\n                bail!(\"image {} base64 is not canonical\", index + 1);\n            }\n            Ok(attached.content_block())\n        })\n        .collect()\n}\n\n/// Reuse durable canonical bytes for retry, never reread a path or URL.\npub(crate) fn runtime_images_from_blocks(\n    blocks: &[ContentBlock],\n) -> Result<Vec<RuntimeImageInput>> {\n    let mut images = Vec::new();\n    for block in blocks {\n        if let ContentBlock::ImageUrl { image_url } = block {","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/image_attach.rs#L138-L174","documentation":"The image's declared MIME type does not match the media type detected from its actual bytes by encode_image_bytes (which sniffs content, e.g. via magic bytes). prepare_images_with_limit rejects the mismatch so the provider-neutral history never carries a mislabeled content block, since downstream consumers trust the declared media_type.","triggerScenarios":"prepare_runtime_images or prepare_stored_images receives a RuntimeImageInput whose mime field says e.g. image/png but whose bytes are actually JPEG/GIF/WebP (or vice versa) — detected when image.mime != attached.media_type after content sniffing.","commonSituations":"Renaming a .jpg to .png and setting mime from the extension; copy-paste code that hardcodes \"image/png\"; HTTP uploads where Content-Type came from the browser's guess; stored records written by an older path that trusted client-supplied MIME.","solutions":["Derive the MIME type from the image bytes (magic-number sniffing) instead of the filename or client header.","Fix the stored record's mime field to match the real content format.","Re-encode the image into the format its declared MIME claims if that format is required.","At ingestion, reject or normalize mismatched MIME before the image reaches this validation path."],"exampleFix":"// before\nRuntimeImageInput { mime: \"image/png\", data_base64: jpeg_b64, .. }\n// after\nRuntimeImageInput { mime: \"image/jpeg\", data_base64: jpeg_b64, .. }","handlingStrategy":"validation","validationCode":"fn mime_matches(data_base64: &str, claimed: &str) -> bool {\n    STANDARD.decode(data_base64)\n        .ok()\n        .and_then(|b| infer_image_mime(&b))\n        .map_or(false, |detected| detected == claimed)\n}","typeGuard":"fn sniffed_mime(bytes: &[u8]) -> Option<&'static str> {\n    match bytes.first()? {\n        0x89 if bytes.start_with(b\"\\x89PNG\") => Some(\"image/png\"),\n        0xFF if bytes.start_with(&[0xFF, 0xD8]) => Some(\"image/jpeg\"),\n        b'G' if bytes.start_with(b\"GIF8\") => Some(\"image/gif\"),\n        _ => None,\n    }\n}","tryCatchPattern":"match prepare_runtime_images(&images) {\n    Err(e) if e.to_string().contains(\"MIME does not match\") => {\n        eprintln!(\"derive mime from magic bytes, not the filename\"); }\n    other => other?,\n}","preventionTips":["Never trust filename extension or client Content-Type for image MIME","Sniff magic bytes at ingestion and set mime from the result","Add a fixture test that attaches a mislabeled JPEG as PNG"],"tags":["image","mime","validation"],"backgroundTag":"type-mismatch","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T16:17:23.217Z"}