Skip to content

Commit 736e04c

Browse files
committed
fix(media): drain ignored download responses
1 parent 6a324f6 commit 736e04c

2 files changed

Lines changed: 91 additions & 5 deletions

File tree

src/media/store.redirect.test.ts

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ function createMockHttpExchange() {
1717
statusCode: 0,
1818
headers: {} as Record<string, string>,
1919
});
20+
const originalResume = res.resume.bind(res);
21+
const resume = vi.fn(() => originalResume());
22+
res.resume = resume as typeof res.resume;
2023
const req = {
2124
on: (event: string, handler: (...args: unknown[]) => void) => {
2225
if (event === "error") {
@@ -27,18 +30,40 @@ function createMockHttpExchange() {
2730
end: () => undefined,
2831
destroy: () => res.destroy(),
2932
} as const;
30-
return { req, res };
33+
return { req, res, resume };
3134
}
3235

33-
function mockRedirectExchange(params: { location?: string }) {
34-
const { req, res } = createMockHttpExchange();
36+
function mockRedirectExchange(params: { body?: string; location?: string }) {
37+
const { req, res, resume } = createMockHttpExchange();
3538
res.statusCode = 302;
3639
res.headers = params.location ? { location: params.location } : {};
3740
return {
3841
req,
42+
resume,
43+
send(cb: (value: unknown) => void) {
44+
setImmediate(() => {
45+
cb(res as unknown);
46+
if (params.body) {
47+
res.write(params.body);
48+
}
49+
res.end();
50+
});
51+
},
52+
};
53+
}
54+
55+
function mockHttpStatusExchange(params: { body?: string; statusCode: number }) {
56+
const { req, res, resume } = createMockHttpExchange();
57+
res.statusCode = params.statusCode;
58+
return {
59+
req,
60+
resume,
3961
send(cb: (value: unknown) => void) {
4062
setImmediate(() => {
4163
cb(res as unknown);
64+
if (params.body) {
65+
res.write(params.body);
66+
}
4267
res.end();
4368
});
4469
},
@@ -209,21 +234,74 @@ describe("media store redirects", () => {
209234
expect(getRequestHeaders(1).get("authorization")).toBe("Bearer secret");
210235
});
211236

237+
it("drains ignored redirect response bodies before following redirects", async () => {
238+
let redirectResume: ReturnType<typeof vi.fn> | undefined;
239+
let call = 0;
240+
mockRequest.mockImplementation((_url, _opts, cb) => {
241+
call += 1;
242+
if (call === 1) {
243+
const exchange = mockRedirectExchange({
244+
body: "ignored redirect response body",
245+
location: "https://example.com/final",
246+
});
247+
redirectResume = exchange.resume;
248+
exchange.send(cb);
249+
return exchange.req;
250+
}
251+
252+
const exchange = mockSuccessfulTextExchange({
253+
text: "redirected",
254+
contentType: "text/plain",
255+
});
256+
exchange.send(cb);
257+
return exchange.req;
258+
});
259+
260+
await saveMediaSource("https://example.com/start");
261+
262+
expect(redirectResume).toHaveBeenCalledOnce();
263+
});
264+
212265
it("fails when redirect response omits location header", async () => {
266+
let redirectResume: ReturnType<typeof vi.fn> | undefined;
213267
mockRequest.mockImplementationOnce((_url, _opts, cb) => {
214-
const exchange = mockRedirectExchange({});
268+
const exchange = mockRedirectExchange({ body: "ignored redirect response body" });
269+
redirectResume = exchange.resume;
215270
exchange.send(cb);
216271
return exchange.req;
217272
});
218273
await expectRedirectSaveFailure("Redirect loop or missing Location header");
274+
expect(redirectResume).toHaveBeenCalledOnce();
219275
});
220276

221277
it("fails when redirect location is malformed", async () => {
278+
let redirectResume: ReturnType<typeof vi.fn> | undefined;
222279
mockRequest.mockImplementationOnce((_url, _opts, cb) => {
223-
const exchange = mockRedirectExchange({ location: "http://[" });
280+
const exchange = mockRedirectExchange({
281+
body: "ignored redirect response body",
282+
location: "http://[",
283+
});
284+
redirectResume = exchange.resume;
224285
exchange.send(cb);
225286
return exchange.req;
226287
});
227288
await expectRedirectSaveFailure("Invalid redirect Location header");
289+
expect(redirectResume).toHaveBeenCalledOnce();
290+
});
291+
292+
it("drains ignored HTTP error response bodies before failing", async () => {
293+
let errorResume: ReturnType<typeof vi.fn> | undefined;
294+
mockRequest.mockImplementationOnce((_url, _opts, cb) => {
295+
const exchange = mockHttpStatusExchange({
296+
body: "ignored error response body",
297+
statusCode: 500,
298+
});
299+
errorResume = exchange.resume;
300+
exchange.send(cb);
301+
return exchange.req;
302+
});
303+
304+
await expectRedirectSaveFailure("HTTP 500 downloading media");
305+
expect(errorResume).toHaveBeenCalledOnce();
228306
});
229307
});

src/media/store.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ function looksLikeUrl(src: string) {
198198
return /^https?:\/\//i.test(src);
199199
}
200200

201+
function discardIgnoredHttpResponse(res: NodeJS.ReadableStream): void {
202+
res.resume();
203+
}
204+
201205
/**
202206
* Download media to disk while capturing the first few KB for mime sniffing.
203207
*/
@@ -227,26 +231,30 @@ async function downloadToFile(
227231
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400) {
228232
const location = res.headers.location;
229233
if (!location || maxRedirects <= 0) {
234+
discardIgnoredHttpResponse(res);
230235
reject(new Error(`Redirect loop or missing Location header`));
231236
return;
232237
}
233238
let redirectUrl: URL;
234239
try {
235240
redirectUrl = new URL(location, url);
236241
} catch {
242+
discardIgnoredHttpResponse(res);
237243
reject(new Error("Invalid redirect Location header"));
238244
return;
239245
}
240246
const redirectHeaders =
241247
redirectUrl.origin === parsedUrl.origin
242248
? headers
243249
: retainSafeHeadersForCrossOriginRedirect(headers);
250+
discardIgnoredHttpResponse(res);
244251
resolve(
245252
downloadToFile(redirectUrl.href, dest, redirectHeaders, maxRedirects - 1, maxBytes),
246253
);
247254
return;
248255
}
249256
if (!res.statusCode || res.statusCode >= 400) {
257+
discardIgnoredHttpResponse(res);
250258
reject(new Error(`HTTP ${res.statusCode ?? "?"} downloading media`));
251259
return;
252260
}

0 commit comments

Comments
 (0)