From bf09878da1e4d1dc8f6584d8960c406bb1b2fc86 Mon Sep 17 00:00:00 2001 From: Suliman Abdulrazzaq Date: Sun, 9 Aug 2026 15:13:51 +0300 Subject: [PATCH] fix: add charset to text file responses --- src/api/routes/surface.ts | 10 +++++++++- test/agent-files-route.test.ts | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/api/routes/surface.ts b/src/api/routes/surface.ts index 3fbe270a..f66b65c3 100644 --- a/src/api/routes/surface.ts +++ b/src/api/routes/surface.ts @@ -29,6 +29,14 @@ import { SHARED_SKILL_TRIGGER_REFUSAL, } from "../artifact-share.ts"; +function contentTypeForFile(mimetype?: string): string { + const contentType = mimetype || "application/octet-stream"; + if (/^(?:text\/|application\/(?:json|xml)(?:;|$))/i.test(contentType) && !/;\s*charset\s*=/i.test(contentType)) { + return `${contentType}; charset=utf-8`; + } + return contentType; +} + function isGrant(b: unknown): b is Grant { return ( isObj(b) && @@ -252,7 +260,7 @@ async function getFileContent(ctx: ApiCtx): Promise { const opened = await app.openFileForViewer(id, viewer); if (!opened) return sendJson(res, 404, { error: "not_found" }); res.writeHead(200, { - "content-type": opened.mimetype || "application/octet-stream", + "content-type": contentTypeForFile(opened.mimetype), "content-length": String(opened.sizeBytes), "content-disposition": `inline; filename*=UTF-8''${encodeURIComponent(opened.name)}`, }); diff --git a/test/agent-files-route.test.ts b/test/agent-files-route.test.ts index a60fc371..c942c113 100644 --- a/test/agent-files-route.test.ts +++ b/test/agent-files-route.test.ts @@ -82,6 +82,27 @@ describe("agent files self-API", async () => { assert.deepEqual(page.shared, []); }); + it("adds UTF-8 to text file content responses", async () => { + const res = await get(`/v1/files/${mineId}/content`, await capFor("U1")); + assert.equal(res.status, 200); + assert.equal(res.headers.get("content-type"), "text/plain; charset=utf-8"); + assert.equal(Buffer.from(await res.arrayBuffer()).toString("utf8"), "mine"); + + await built.files.put({ + id: "parameterized-text", + ownerScopeId: scopeId("personal", "U1"), + createdBy: "U1", + name: "parameterized.txt", + path: "parameterized.txt", + mimetype: "text/plain; charset=iso-8859-1", + data: Buffer.from("bytes"), + direction: "out", + }); + const parameterized = await get("/v1/files/parameterized-text/content", await capFor("U1")); + assert.equal(parameterized.status, 200); + assert.equal(parameterized.headers.get("content-type"), "text/plain; charset=iso-8859-1"); + }); + it("returns 404 for another principal's file content", async () => { const res = await get(`/v1/files/${theirsId}/content`, await capFor("U1")); assert.equal(res.status, 404);