mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(media): drain ignored download responses
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user