Fix: gracefully error out for unsupported images (#7478)

Fix for #7459 
## What
Since codex errors out for unsupported images, stop attempting to
base64/attach them and instead emit a clear placeholder when the file
isn’t a supported image MIME.

## Why
Local uploads for unsupported formats (e.g., SVG/GIF/etc.) were
dead-ending after decode failures because of the 400 retry loop. Users
now get an explicit “cannot attach … unsupported image format …”
response.

## How
Replace the fallback read/encode path with MIME detection that bails out
for non-image or unsupported image types, returning a consistent
placeholder. Unreadable and invalid images still produce their existing
error placeholders.
This commit is contained in:
Job Chong
2025-12-10 02:28:41 +08:00
committed by GitHub
parent 9df70a0772
commit ac3237721e
3 changed files with 55 additions and 33 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1491,7 +1491,6 @@ name = "codex-protocol"
version = "0.0.0"
dependencies = [
"anyhow",
"base64",
"codex-git",
"codex-utils-image",
"icu_decimal",

View File

@@ -14,7 +14,6 @@ workspace = true
[dependencies]
codex-git = { workspace = true }
base64 = { workspace = true }
codex-utils-image = { workspace = true }
icu_decimal = { workspace = true }
icu_locale_core = { workspace = true }

View File

@@ -1,6 +1,5 @@
use std::collections::HashMap;
use base64::Engine;
use codex_utils_image::load_and_resize_to_fit;
use mcp_types::CallToolResult;
use mcp_types::ContentBlock;
@@ -175,6 +174,16 @@ fn invalid_image_error_placeholder(
}
}
fn unsupported_image_error_placeholder(path: &std::path::Path, mime: &str) -> ContentItem {
ContentItem::InputText {
text: format!(
"Codex cannot attach image at `{}`: unsupported image format `{}`.",
path.display(),
mime
),
}
}
impl From<ResponseInputItem> for ResponseItem {
fn from(item: ResponseInputItem) -> Self {
match item {
@@ -285,37 +294,20 @@ impl From<Vec<UserInput>> for ResponseInputItem {
} else if err.is_invalid_image() {
invalid_image_error_placeholder(&path, &err)
} else {
match std::fs::read(&path) {
Ok(bytes) => {
let Some(mime_guess) = mime_guess::from_path(&path).first()
else {
return local_image_error_placeholder(
&path,
"unsupported MIME type (unknown)",
);
};
let mime = mime_guess.essence_str().to_owned();
if !mime.starts_with("image/") {
return local_image_error_placeholder(
&path,
format!("unsupported MIME type `{mime}`"),
);
}
let encoded =
base64::engine::general_purpose::STANDARD.encode(bytes);
ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
}
}
Err(read_err) => {
tracing::warn!(
"Skipping image {} could not read file: {}",
path.display(),
read_err
);
local_image_error_placeholder(&path, &read_err)
}
let Some(mime_guess) = mime_guess::from_path(&path).first() else {
return local_image_error_placeholder(
&path,
"unsupported MIME type (unknown)",
);
};
let mime = mime_guess.essence_str().to_owned();
if !mime.starts_with("image/") {
return local_image_error_placeholder(
&path,
format!("unsupported MIME type `{mime}`"),
);
}
unsupported_image_error_placeholder(&path, &mime)
}
}
},
@@ -823,4 +815,36 @@ mod tests {
Ok(())
}
#[test]
fn local_image_unsupported_image_format_adds_placeholder() -> Result<()> {
let dir = tempdir()?;
let svg_path = dir.path().join("example.svg");
std::fs::write(
&svg_path,
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>"#,
)?;
let item = ResponseInputItem::from(vec![UserInput::LocalImage {
path: svg_path.clone(),
}]);
match item {
ResponseInputItem::Message { content, .. } => {
assert_eq!(content.len(), 1);
let expected = format!(
"Codex cannot attach image at `{}`: unsupported image format `image/svg+xml`.",
svg_path.display()
);
match &content[0] {
ContentItem::InputText { text } => assert_eq!(text, &expected),
other => panic!("expected placeholder text but found {other:?}"),
}
}
other => panic!("expected message response but got {other:?}"),
}
Ok(())
}
}