mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-22 03:18:54 +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
@@ -158,6 +158,55 @@ describe("downloadCommentImages", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("should save a JPEG from an extensionless URL with a .jpg extension", async () => {
|
||||
// Regression for the case where a JPEG screenshot is pasted into an issue.
|
||||
// GitHub serves it from /user-attachments/assets/<uuid> (no extension), so
|
||||
// the URL-based guess used to default to ".png" while the bytes are JPEG —
|
||||
// producing a mislabeled file that the Anthropic API rejected with a 400.
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl =
|
||||
"https://github.com/user-attachments/assets/f871c23e-a84d-4f1f-b9a0-86626c63f161";
|
||||
const signedUrl =
|
||||
"https://private-user-images.githubusercontent.com/screenshot?jwt=token";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.get = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl}">`,
|
||||
},
|
||||
});
|
||||
|
||||
// JPEG magic bytes: FF D8 FF, then arbitrary padding.
|
||||
const jpegBytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]);
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => jpegBytes.buffer,
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_body",
|
||||
issueNumber: "143",
|
||||
body: ``,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(fsWriteFileSpy).toHaveBeenCalledWith(
|
||||
"/tmp/github-images/image-1704067200000-0.jpg",
|
||||
Buffer.from(jpegBytes.buffer),
|
||||
);
|
||||
expect(result.get(imageUrl)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.jpg",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle review comments", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl =
|
||||
|
||||
Reference in New Issue
Block a user