fix(slack): reject HTML responses when downloading media (#4665)

* fix(slack): reject HTML responses when downloading media

Slack sometimes returns HTML login pages instead of binary media when
authentication fails or URLs expire. This change detects HTML responses
by checking content-type header and buffer content, then skips to the
next available file URL.

* fix: format import order and add braces to continue statement

* chore: format Slack media tests

* chore: apply formatter to Slack media tests

* fix(slack): merge auth-header forwarding and html media guard

---------

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
tumf
2026-03-02 02:20:25 +09:00
committed by GitHub
parent 6dbbc58a8d
commit e0571399ac
3 changed files with 73 additions and 0 deletions

View File

@@ -245,6 +245,52 @@ describe("resolveSlackMedia", () => {
expect(mockFetch).not.toHaveBeenCalled();
});
it("rejects HTML auth pages for non-HTML files", async () => {
const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer");
mockFetch.mockResolvedValueOnce(
new Response("<!DOCTYPE html><html><body>login</body></html>", {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
}),
);
const result = await resolveSlackMedia({
files: [{ url_private: "https://files.slack.com/test.jpg", name: "test.jpg" }],
token: "xoxb-test-token",
maxBytes: 1024 * 1024,
});
expect(result).toBeNull();
expect(saveMediaBufferMock).not.toHaveBeenCalled();
});
it("allows expected HTML uploads", async () => {
vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
createSavedMedia("/tmp/page.html", "text/html"),
);
mockFetch.mockResolvedValueOnce(
new Response("<!doctype html><html><body>ok</body></html>", {
status: 200,
headers: { "content-type": "text/html" },
}),
);
const result = await resolveSlackMedia({
files: [
{
url_private: "https://files.slack.com/page.html",
name: "page.html",
mimetype: "text/html",
},
],
token: "xoxb-test-token",
maxBytes: 1024 * 1024,
});
expect(result).not.toBeNull();
expect(result?.[0]?.path).toBe("/tmp/page.html");
});
it("overrides video/* MIME to audio/* for slack_audio voice messages", async () => {
// saveMediaBuffer re-detects MIME from buffer bytes, so it may return
// video/mp4 for MP4 containers. Verify resolveSlackMedia preserves
@@ -525,6 +571,11 @@ describe("resolveSlackAttachmentContent", () => {
},
],
});
const firstCall = mockFetch.mock.calls[0];
expect(firstCall?.[0]).toBe("https://files.slack.com/forwarded.jpg");
const firstInit = firstCall?.[1];
expect(firstInit?.redirect).toBe("manual");
expect(new Headers(firstInit?.headers).get("Authorization")).toBe("Bearer xoxb-test-token");
});
});