fix(slack): cancel followed redirect bodies

This commit is contained in:
Vincent Koc
2026-06-19 10:12:29 +02:00
parent 6aa85dfaa1
commit 44b0644e88
2 changed files with 13 additions and 2 deletions

View File

@@ -291,10 +291,11 @@ describe("fetchWithSlackAuth", () => {
it("strips Authorization header on cross-origin redirects", async () => {
// First call: redirect response from Slack
const redirectResponse = new Response(null, {
const redirectResponse = new Response("redirect body", {
status: 302,
headers: { location: "https://cdn.slack-edge.com/presigned-url?sig=abc123" },
});
const cancel = vi.spyOn(redirectResponse.body!, "cancel").mockResolvedValue(undefined);
// Second call: actual file content from CDN
const fileResponse = new Response(Buffer.from("actual image data"), {
@@ -321,13 +322,15 @@ describe("fetchWithSlackAuth", () => {
"https://cdn.slack-edge.com/presigned-url?sig=abc123",
{ redirect: "follow" },
);
expect(cancel).toHaveBeenCalledOnce();
});
it("preserves Authorization header on same-origin redirects", async () => {
const redirectResponse = new Response(null, {
const redirectResponse = new Response("redirect body", {
status: 302,
headers: { location: "/files/redirect-target" },
});
const cancel = vi.spyOn(redirectResponse.body!, "cancel").mockResolvedValue(undefined);
const fileResponse = new Response(Buffer.from("image data"), {
status: 200,
@@ -342,6 +345,7 @@ describe("fetchWithSlackAuth", () => {
headers: { Authorization: "Bearer xoxb-test-token" },
redirect: "follow",
});
expect(cancel).toHaveBeenCalledOnce();
});
it("returns redirect response when no location header is provided", async () => {

View File

@@ -104,6 +104,12 @@ function resolveSlackFetchForRuntime(): typeof fetch {
return isMockedFetch(globalThis.fetch) ? globalThis.fetch : fetchWithRuntimeDispatcher;
}
async function cancelUnreadResponseBody(response: Response): Promise<void> {
if (!response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
/**
* Fetches a URL with Authorization header while keeping same-origin redirects
* authenticated and dropping auth once the redirect crosses origins.
@@ -136,6 +142,7 @@ export async function fetchWithSlackAuth(url: string, token: string): Promise<Re
if (resolvedUrl.protocol !== "https:") {
return initialRes;
}
await cancelUnreadResponseBody(initialRes);
if (resolvedUrl.origin === parsed.origin) {
return fetchImpl(resolvedUrl.toString(), {
headers: authHeaders,