diff --git a/extensions/canvas/src/host/a2ui.ts b/extensions/canvas/src/host/a2ui.ts index aa555ebd40a1..21779707a2cc 100644 --- a/extensions/canvas/src/host/a2ui.ts +++ b/extensions/canvas/src/host/a2ui.ts @@ -136,20 +136,25 @@ async function handleA2uiHttpRequestWithRootResolver( : ((await detectMime({ filePath: result.realPath })) ?? "application/octet-stream"); res.setHeader("Cache-Control", "no-store"); - if (req.method === "HEAD") { - res.setHeader("Content-Type", mime === "text/html" ? "text/html; charset=utf-8" : mime); - res.end(); - return true; - } - if (mime === "text/html") { const buf = await result.handle.readFile({ encoding: "utf8" }); + const body = injectCanvasRuntime(buf, options); res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.end(injectCanvasRuntime(buf, options)); + if (req.method === "HEAD") { + res.setHeader("Content-Length", String(Buffer.byteLength(body))); + res.end(); + return true; + } + res.end(body); return true; } res.setHeader("Content-Type", mime); + if (req.method === "HEAD") { + res.setHeader("Content-Length", String(result.stat.size)); + res.end(); + return true; + } res.end(await result.handle.readFile()); return true; } finally { diff --git a/extensions/canvas/src/host/server.test.ts b/extensions/canvas/src/host/server.test.ts index 6ec1ce481307..ace8919395fd 100644 --- a/extensions/canvas/src/host/server.test.ts +++ b/extensions/canvas/src/host/server.test.ts @@ -672,4 +672,43 @@ describe("canvas host", () => { await fs.rm(linkPath, { force: true }); } }); + + it("serves Content-Length on A2UI HEAD responses", async () => { + const fixtureEntryDir = await createCaseDir(); + const a2uiRoot = path.join(fixtureEntryDir, "a2ui"); + const { setA2uiRootRealForTest } = await import("../../test-api.js"); + + try { + await fs.mkdir(a2uiRoot, { recursive: true }); + await fs.writeFile( + path.join(a2uiRoot, "index.html"), + ` +`, + "utf8", + ); + await fs.writeFile( + path.join(a2uiRoot, "a2ui.bundle.js"), + "window.openclawA2UI = {};", + "utf8", + ); + setA2uiRootRealForTest(await fs.realpath(a2uiRoot)); + + // HTML: HEAD Content-Length must match the injected GET body bytes. + const getHtml = await captureA2uiFixtureResponse(`${A2UI_PATH}/`); + const headHtml = await captureA2uiFixtureResponse(`${A2UI_PATH}/`, "HEAD"); + expect(headHtml.status).toBe(200); + expect(headHtml.headers["content-length"]).toBe(String(getHtml.bodyBytes.byteLength)); + expect(headHtml.bodyBytes.byteLength).toBe(0); + expect(headHtml.headers["content-type"]).toBe("text/html; charset=utf-8"); + + // Static asset: HEAD Content-Length must match the file bytes. + const getBundle = await captureA2uiFixtureResponse(`${A2UI_PATH}/a2ui.bundle.js`); + const headBundle = await captureA2uiFixtureResponse(`${A2UI_PATH}/a2ui.bundle.js`, "HEAD"); + expect(headBundle.status).toBe(200); + expect(headBundle.headers["content-length"]).toBe(String(getBundle.bodyBytes.byteLength)); + expect(headBundle.bodyBytes.byteLength).toBe(0); + } finally { + setA2uiRootRealForTest(undefined); + } + }); });