Hmbown/CodeWhale · error

image base64 is not canonical

Error message

image {} base64 is not canonical

What it means

The library requires the canonical replay representation for image payloads: standard padded base64 (STANDARD alphabet with '=' padding). prepare_images_with_limit re-encodes the decoded bytes and bails if the result differs from the supplied data_base64, so non-canonical encodings (URL-safe alphabet, missing padding, embedded whitespace/newlines, wrong case) never enter durable history and every replay round-trips identically.

Solutions

  1. Re-encode the payload with STANDARD padded base64 (e.g. base64::engine::general_purpose::STANDARD.encode(bytes)) before attaching.
  2. Normalize the input: decode with an accepting engine, re-encode with STANDARD, and pass the canonical string.
  3. Strip whitespace/newlines and add padding if you must accept foreign base64 at your boundary.
  4. Standardize on one base64 engine across the pipeline so producers emit canonical form directly.

Example fix

// before
let b64 = URL_SAFE_NO_PAD.encode(&bytes);
attach(RuntimeImageInput { data_base64: b64, .. })?;
// after
let b64 = STANDARD.encode(&bytes);
attach(RuntimeImageInput { data_base64: b64, .. })?;
Defensive patterns

Strategy: validation

Validate before calling

fn canonical_b64(input: &str) -> Option<String> {
    use base64::Engine;
    let bytes = base64::engine::general_purpose::STANDARD.decode(input).ok()?;
    Some(base64::engine::general_purpose::STANDARD.encode(bytes))
}

Type guard

fn is_canonical_b64(input: &str) -> bool {
    use base64::Engine;
    base64::engine::general_purpose::STANDARD
        .decode(input)
        .map(|b| base64::engine::general_purpose::STANDARD.encode(b) == input)
        .unwrap_or(false)
}

Try / catch

match prepare_runtime_images(&images) {
    Err(e) if e.to_string().contains("base64 is not canonical") => {
        eprintln!("re-encode with standard padded base64 (no URL-safe, no newlines)"); }
    other => other?,
}

Prevention

When it happens

Trigger: prepare_runtime_images or prepare_stored_images receives data_base64 that was produced with URL_SAFE base64, stripped padding, line-wrapped MIME-style base64 (e.g. from PEM or email), or contains whitespace/newlines — anything where STANDARD.encode(STANDARD.decode(input)) != input.

Common situations: Base64 produced by a URL-safe encoder (JWT-style, '-','_' chars); base64 with newlines inserted every 76 chars from email/PEM tooling; padding stripped to save bytes; base64 copied from JSON with escaped characters mangled.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/c7272c5a1cebc91e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/image_attach.rs:161

            if bytes.len() > per_image_limit {
                bail!(
                    "image {} exceeds the {} MiB limit",
                    index + 1,
                    per_image_limit / (1024 * 1024)
                );
            }
            total = total.saturating_add(bytes.len());
            if total_limit.is_some_and(|limit| total > limit) {
                bail!("images exceed the 5 MiB total limit");
            }
            let attached = encode_image_bytes(&bytes, &format!("image {}", index + 1))?;
            if image.mime != attached.media_type {
                bail!("image {} MIME does not match its content", index + 1);
            }
            decode_and_guard_image(&bytes)?;
            // Standard padded base64 is the one replay representation.
            if STANDARD.encode(&bytes) != image.data_base64 {
                bail!("image {} base64 is not canonical", index + 1);
            }
            Ok(attached.content_block())
        })
        .collect()
}

/// Reuse durable canonical bytes for retry, never reread a path or URL.
pub(crate) fn runtime_images_from_blocks(
    blocks: &[ContentBlock],
) -> Result<Vec<RuntimeImageInput>> {
    let mut images = Vec::new();
    for block in blocks {
        if let ContentBlock::ImageUrl { image_url } = block {
            if image_url.url.len() > MAX_IMAGE_BYTES.div_ceil(3) * 4 + 32 {
                bail!("stored image exceeds the attachment limit");
            }
            let (mime, data) = parse_data_url(&image_url.url)
                .ok_or_else(|| anyhow::anyhow!("stored image requires canonical inline content"))?;

View on GitHub (pinned to 73e0f67d83)