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
- Re-encode the payload with STANDARD padded base64 (e.g. base64::engine::general_purpose::STANDARD.encode(bytes)) before attaching.
- Normalize the input: decode with an accepting engine, re-encode with STANDARD, and pass the canonical string.
- Strip whitespace/newlines and add padding if you must accept foreign base64 at your boundary.
- 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
- Use one STANDARD base64 engine everywhere in the pipeline
- Reject URL-safe or unpadded base64 at system boundaries
- Never wrap base64 in newlines when storing or transmitting it
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
- image exceeds the MiB limit
- image has invalid base64
- invalid-base64
- invalid base64
- invalid tool image evidence
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)