Hmbown/CodeWhale · error

image has invalid base64

Error message

image {} has invalid base64

What it means

One of the runtime image inputs carried a data_base64 string that is not valid standard base64, so STANDARD.decode() failed. The error names the 1-based image index so the caller knows which attachment to fix.

Solutions

  1. Re-encode the bytes with standard base64 (with padding) and pass only the payload, stripping any `data:<mime>;base64,` prefix.
  2. If the source uses URL-safe base64, convert '-_' to '+/' and restore padding before assigning data_base64.
  3. Strip whitespace/newlines from the base64 string.

Example fix

// before
data_base64: "iVBORw0KGgoAAAANSUhEUg..._-" // URL-safe alphabet

// after
data_base64: "iVBORw0KGgoAAAANSUhEUg...+/=" // standard base64 with padding
Defensive patterns

Strategy: validation

Validate before calling

fn valid_std_base64(s: &str) -> bool {
    let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
    base64::engine::general_purpose::STANDARD.decode(&cleaned).is_ok()
}

Try / catch

// Rust
match STANDARD.decode(&image.data_base64) {
    Ok(bytes) => attach(bytes),
    Err(e) => {
        let repaired = image.data_base64.replace(['-', '_'], "+/");
        attach(STANDARD.decode(repaired.trim())?);
    }
}

Prevention

When it happens

Trigger: prepare_images_with_limit (via prepare_runtime_images or prepare_stored_images) is given a RuntimeImageInput whose data_base64 contains characters outside the standard base64 alphabet, wrong padding, or URL-safe '-_' characters where standard '+/' is required.

Common situations: Producers that URL-safe-encode base64; strings with whitespace, newlines, or a `data:` prefix accidentally included in data_base64; copying a base64 string out of JSON tooling that wrapped or escaped it.

Related errors


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

Appendix: source

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

    images: &[RuntimeImageInput],
    per_image_limit: usize,
    total_limit: Option<usize>,
) -> Result<Vec<ContentBlock>> {
    let mut total = 0usize;
    images
        .iter()
        .enumerate()
        .map(|(index, image)| {
            if image.data_base64.len() > per_image_limit.div_ceil(3) * 4 {
                bail!(
                    "image {} exceeds the {} MiB limit",
                    index + 1,
                    per_image_limit / (1024 * 1024)
                );
            }
            let bytes = STANDARD
                .decode(&image.data_base64)
                .map_err(|_| anyhow::anyhow!("image {} has invalid base64", index + 1))?;
            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 {

View on GitHub (pinned to 73e0f67d83)