fix(media): drain ignored download responses

This commit is contained in:
Vincent Koc
2026-05-28 05:53:09 +02:00
parent 6a324f6400
commit 736e04cb90
2 changed files with 91 additions and 5 deletions
+83 -5
View File
@@ -17,6 +17,9 @@ function createMockHttpExchange() {
statusCode: 0,
headers: {} as Record<string, string>,
});
const originalResume = res.resume.bind(res);
const resume = vi.fn(() => originalResume());
res.resume = resume as typeof res.resume;
const req = {
on: (event: string, handler: (...args: unknown[]) => void) => {
if (event === "error") {
@@ -27,18 +30,40 @@ function createMockHttpExchange() {
end: () => undefined,
destroy: () => res.destroy(),
} as const;
return { req, res };
return { req, res, resume };
}
function mockRedirectExchange(params: { location?: string }) {
const { req, res } = createMockHttpExchange();
function mockRedirectExchange(params: { body?: string; location?: string }) {
const { req, res, resume } = createMockHttpExchange();
res.statusCode = 302;
res.headers = params.location ? { location: params.location } : {};
return {
req,
resume,
send(cb: (value: unknown) => void) {
setImmediate(() => {
cb(res as unknown);
if (params.body) {
res.write(params.body);
}
res.end();
});
},
};
}
function mockHttpStatusExchange(params: { body?: string; statusCode: number }) {
const { req, res, resume } = createMockHttpExchange();
res.statusCode = params.statusCode;
return {
req,
resume,
send(cb: (value: unknown) => void) {
setImmediate(() => {
cb(res as unknown);
if (params.body) {
res.write(params.body);
}
res.end();
});
},
@@ -209,21 +234,74 @@ describe("media store redirects", () => {
expect(getRequestHeaders(1).get("authorization")).toBe("Bearer secret");
});
it("drains ignored redirect response bodies before following redirects", async () => {
let redirectResume: ReturnType<typeof vi.fn> | undefined;
let call = 0;
mockRequest.mockImplementation((_url, _opts, cb) => {
call += 1;
if (call === 1) {
const exchange = mockRedirectExchange({
body: "ignored redirect response body",
location: "https://example.com/final",
});
redirectResume = exchange.resume;
exchange.send(cb);
return exchange.req;
}
const exchange = mockSuccessfulTextExchange({
text: "redirected",
contentType: "text/plain",
});
exchange.send(cb);
return exchange.req;
});
await saveMediaSource("https://example.com/start");
expect(redirectResume).toHaveBeenCalledOnce();
});
it("fails when redirect response omits location header", async () => {
let redirectResume: ReturnType<typeof vi.fn> | undefined;
mockRequest.mockImplementationOnce((_url, _opts, cb) => {
const exchange = mockRedirectExchange({});
const exchange = mockRedirectExchange({ body: "ignored redirect response body" });
redirectResume = exchange.resume;
exchange.send(cb);
return exchange.req;
});
await expectRedirectSaveFailure("Redirect loop or missing Location header");
expect(redirectResume).toHaveBeenCalledOnce();
});
it("fails when redirect location is malformed", async () => {
let redirectResume: ReturnType<typeof vi.fn> | undefined;
mockRequest.mockImplementationOnce((_url, _opts, cb) => {
const exchange = mockRedirectExchange({ location: "http://[" });
const exchange = mockRedirectExchange({
body: "ignored redirect response body",
location: "http://[",
});
redirectResume = exchange.resume;
exchange.send(cb);
return exchange.req;
});
await expectRedirectSaveFailure("Invalid redirect Location header");
expect(redirectResume).toHaveBeenCalledOnce();
});
it("drains ignored HTTP error response bodies before failing", async () => {
let errorResume: ReturnType<typeof vi.fn> | undefined;
mockRequest.mockImplementationOnce((_url, _opts, cb) => {
const exchange = mockHttpStatusExchange({
body: "ignored error response body",
statusCode: 500,
});
errorResume = exchange.resume;
exchange.send(cb);
return exchange.req;
});
await expectRedirectSaveFailure("HTTP 500 downloading media");
expect(errorResume).toHaveBeenCalledOnce();
});
});
+8
View File
@@ -198,6 +198,10 @@ function looksLikeUrl(src: string) {
return /^https?:\/\//i.test(src);
}
function discardIgnoredHttpResponse(res: NodeJS.ReadableStream): void {
res.resume();
}
/**
* Download media to disk while capturing the first few KB for mime sniffing.
*/
@@ -227,6 +231,7 @@ async function downloadToFile(
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400) {
const location = res.headers.location;
if (!location || maxRedirects <= 0) {
discardIgnoredHttpResponse(res);
reject(new Error(`Redirect loop or missing Location header`));
return;
}
@@ -234,6 +239,7 @@ async function downloadToFile(
try {
redirectUrl = new URL(location, url);
} catch {
discardIgnoredHttpResponse(res);
reject(new Error("Invalid redirect Location header"));
return;
}
@@ -241,12 +247,14 @@ async function downloadToFile(
redirectUrl.origin === parsedUrl.origin
? headers
: retainSafeHeadersForCrossOriginRedirect(headers);
discardIgnoredHttpResponse(res);
resolve(
downloadToFile(redirectUrl.href, dest, redirectHeaders, maxRedirects - 1, maxBytes),
);
return;
}
if (!res.statusCode || res.statusCode >= 400) {
discardIgnoredHttpResponse(res);
reject(new Error(`HTTP ${res.statusCode ?? "?"} downloading media`));
return;
}