mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(gateway): retain avatar content length for HEAD requests (#117124)
Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
committed by
GitHub
parent
7dd74db836
commit
afa8fffab8
@@ -225,7 +225,7 @@ describe("handleControlUiHttpRequest", () => {
|
||||
trustedProxies?: string[];
|
||||
remoteAddress?: string;
|
||||
}) {
|
||||
const { res, end } = makeMockHttpResponse();
|
||||
const { res, end, setHeader } = makeMockHttpResponse();
|
||||
const handled = await handleControlUiAvatarRequest(
|
||||
{
|
||||
url: params.url,
|
||||
@@ -241,7 +241,7 @@ describe("handleControlUiHttpRequest", () => {
|
||||
config: params.config,
|
||||
},
|
||||
);
|
||||
return { res, end, handled };
|
||||
return { res, end, setHeader, handled };
|
||||
}
|
||||
|
||||
async function runAssistantMediaRequest(params: {
|
||||
@@ -2233,6 +2233,92 @@ describe("handleControlUiHttpRequest", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "PNG", filename: "avatar.png", contentType: "image/png" },
|
||||
{ name: "JPEG", filename: "avatar.jpg", contentType: "image/jpeg" },
|
||||
{ name: "GIF", filename: "avatar.gif", contentType: "image/gif" },
|
||||
{ name: "WebP", filename: "avatar.webp", contentType: "image/webp" },
|
||||
{ name: "SVG", filename: "avatar.svg", contentType: "image/svg+xml" },
|
||||
])(
|
||||
"preserves the pinned $name avatar byte length and metadata on HEAD",
|
||||
async ({ contentType, filename }) => {
|
||||
const tmp = testTempDirs.make("openclaw-avatar-head-metadata-");
|
||||
const body = Buffer.from(`avatar 東京 ${filename}\n`, "utf8");
|
||||
const read = vi.spyOn(fsSync, "read");
|
||||
const closeSync = vi.spyOn(fsSync, "closeSync");
|
||||
try {
|
||||
await fs.writeFile(path.join(tmp, filename), body);
|
||||
const config = createAvatarConfig(tmp, filename);
|
||||
const head = await runAvatarRequest({ url: "/avatar/main", method: "HEAD", config });
|
||||
|
||||
expect(head.handled).toBe(true);
|
||||
expect(head.res.statusCode).toBe(200);
|
||||
expect(head.setHeader).toHaveBeenCalledWith("Content-Length", String(body.byteLength));
|
||||
expect(head.setHeader).toHaveBeenCalledWith("Content-Type", contentType);
|
||||
expect(head.setHeader).toHaveBeenCalledWith("Cache-Control", "no-cache");
|
||||
expect(head.end).toHaveBeenCalledWith();
|
||||
expect(read).not.toHaveBeenCalled();
|
||||
expect(closeSync).toHaveBeenCalledOnce();
|
||||
|
||||
const get = await runAvatarRequest({ url: "/avatar/main", method: "GET", config });
|
||||
expect(get.res.statusCode).toBe(200);
|
||||
expect(get.end).toHaveBeenCalledWith(body);
|
||||
expect(get.setHeader).toHaveBeenCalledWith("Content-Type", contentType);
|
||||
expect(get.setHeader).toHaveBeenCalledWith("Cache-Control", "no-cache");
|
||||
expect(get.setHeader).not.toHaveBeenCalledWith("Content-Length", expect.anything());
|
||||
} finally {
|
||||
read.mockRestore();
|
||||
closeSync.mockRestore();
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["", "/openclaw"])(
|
||||
"preserves authenticated avatar HEAD length under the %s Control UI base path",
|
||||
async (basePath) => {
|
||||
const tmp = testTempDirs.make("openclaw-avatar-head-base-");
|
||||
const body = Buffer.from("authenticated avatar 東京", "utf8");
|
||||
try {
|
||||
await fs.writeFile(path.join(tmp, "main.png"), body);
|
||||
const response = await runAvatarRequest({
|
||||
url: `${basePath}/avatar/main`,
|
||||
method: "HEAD",
|
||||
config: createAvatarConfig(tmp, "main.png"),
|
||||
...(basePath ? { basePath } : {}),
|
||||
auth: { mode: "token", token: "test-token", allowTailscale: false },
|
||||
headers: { authorization: "Bearer test-token" },
|
||||
});
|
||||
|
||||
expect(response.handled).toBe(true);
|
||||
expect(response.res.statusCode).toBe(200);
|
||||
expect(response.setHeader).toHaveBeenCalledWith("Content-Length", String(body.byteLength));
|
||||
expect(response.end).toHaveBeenCalledWith();
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("does not expose avatar HEAD representation length before authentication", async () => {
|
||||
const tmp = testTempDirs.make("openclaw-avatar-head-unauthorized-");
|
||||
try {
|
||||
await fs.writeFile(path.join(tmp, "main.png"), REAL_PNG);
|
||||
const response = await runAvatarRequest({
|
||||
url: "/avatar/main",
|
||||
method: "HEAD",
|
||||
config: createAvatarConfig(tmp, "main.png"),
|
||||
auth: { mode: "token", token: "test-token", allowTailscale: false },
|
||||
});
|
||||
|
||||
expect(response.handled).toBe(true);
|
||||
expect(response.res.statusCode).toBe(401);
|
||||
expect(response.setHeader).not.toHaveBeenCalledWith("Content-Length", expect.anything());
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["metadata", "/avatar/main?meta=1", "GET"],
|
||||
["HEAD", "/avatar/main", "HEAD"],
|
||||
|
||||
@@ -831,6 +831,8 @@ export async function handleControlUiAvatarRequest(
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
if (req.method === "HEAD") {
|
||||
res.statusCode = 200;
|
||||
// The pinned descriptor exposes GET's exact byte count without reading the avatar.
|
||||
res.setHeader("Content-Length", String(projection.openedFile.stat.size));
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user