mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-22 11:28:55 +08:00
fix(image-downloader): detect image type from magic bytes (#1396)
GitHub serves pasted attachments from /user-attachments/assets/<uuid>
with no file extension, so getImageExtension() silently defaulted to
".png". When the bytes are actually JPEG/GIF/WebP the downloaded file is
mislabeled, and the Read tool then sends a base64 image whose declared
media_type doesn't match its magic bytes — which the Anthropic API
rejects with `400 invalid_request_error` ("image was specified using the
image/png media type, but the image appears to be a image/jpeg image").
Sniff the real format from the buffer's magic bytes after download and
only fall back to the URL-based extension when the signature is
unrecognized. Adds a regression test for a JPEG at an extensionless URL.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
eba921ff6f
commit
8551f4b0aa
@@ -192,10 +192,6 @@ export async function downloadCommentImages(
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileExtension = getImageExtension(originalUrl);
|
||||
const filename = `image-${Date.now()}-${i}${fileExtension}`;
|
||||
const localPath = path.join(downloadsDir, filename);
|
||||
|
||||
try {
|
||||
console.log(`Downloading ${originalUrl}...`);
|
||||
|
||||
@@ -209,6 +205,19 @@ export async function downloadCommentImages(
|
||||
const arrayBuffer = await imageResponse.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// GitHub user-attachment URLs (/user-attachments/assets/<uuid>) carry
|
||||
// no file extension, so the URL-based guess silently falls back to
|
||||
// ".png". When the bytes are actually JPEG/GIF/WebP, the saved file is
|
||||
// mislabeled and the Read tool sends a base64 image with the wrong
|
||||
// media_type, which the Anthropic API rejects (400 invalid_request).
|
||||
// Detect the real type from the magic bytes and only fall back to the
|
||||
// URL extension when the signature is unrecognized.
|
||||
const fileExtension =
|
||||
detectImageExtensionFromBuffer(buffer) ??
|
||||
getImageExtension(originalUrl);
|
||||
const filename = `image-${Date.now()}-${i}${fileExtension}`;
|
||||
const localPath = path.join(downloadsDir, filename);
|
||||
|
||||
await fs.writeFile(localPath, buffer);
|
||||
console.log(`✓ Saved: ${localPath}`);
|
||||
|
||||
@@ -244,3 +253,56 @@ function getImageExtension(url: string): string {
|
||||
const match = filename.match(/\.(png|jpg|jpeg|gif|webp|svg)$/i);
|
||||
return match ? match[0] : ".png";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine an image's file extension from its magic bytes, independent of the
|
||||
* (often extensionless) source URL. Returns undefined when the signature is not
|
||||
* a format we can confidently identify, so the caller can fall back to the
|
||||
* URL-based extension. Covers the raster formats the Anthropic API accepts.
|
||||
*/
|
||||
function detectImageExtensionFromBuffer(buffer: Buffer): string | undefined {
|
||||
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
||||
if (
|
||||
buffer.length >= 8 &&
|
||||
buffer[0] === 0x89 &&
|
||||
buffer[1] === 0x50 &&
|
||||
buffer[2] === 0x4e &&
|
||||
buffer[3] === 0x47
|
||||
) {
|
||||
return ".png";
|
||||
}
|
||||
// JPEG: FF D8 FF
|
||||
if (
|
||||
buffer.length >= 3 &&
|
||||
buffer[0] === 0xff &&
|
||||
buffer[1] === 0xd8 &&
|
||||
buffer[2] === 0xff
|
||||
) {
|
||||
return ".jpg";
|
||||
}
|
||||
// GIF: "GIF8" (47 49 46 38)
|
||||
if (
|
||||
buffer.length >= 6 &&
|
||||
buffer[0] === 0x47 &&
|
||||
buffer[1] === 0x49 &&
|
||||
buffer[2] === 0x46 &&
|
||||
buffer[3] === 0x38
|
||||
) {
|
||||
return ".gif";
|
||||
}
|
||||
// WebP: "RIFF" (52 49 46 46) .... "WEBP" (57 45 42 50) at offset 8
|
||||
if (
|
||||
buffer.length >= 12 &&
|
||||
buffer[0] === 0x52 &&
|
||||
buffer[1] === 0x49 &&
|
||||
buffer[2] === 0x46 &&
|
||||
buffer[3] === 0x46 &&
|
||||
buffer[8] === 0x57 &&
|
||||
buffer[9] === 0x45 &&
|
||||
buffer[10] === 0x42 &&
|
||||
buffer[11] === 0x50
|
||||
) {
|
||||
return ".webp";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user