From d01015b243c7ea9c8cf2a9f6d783ae71f07dac71 Mon Sep 17 00:00:00 2001 From: Alix-007 Date: Tue, 7 Jul 2026 17:43:45 +0800 Subject: [PATCH] Fix UTF-16-safe audit context truncation (#101298) --- src/media-understanding/shared.test.ts | 17 +++++++++++++++++ src/media-understanding/shared.ts | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/media-understanding/shared.test.ts b/src/media-understanding/shared.test.ts index 92dad93d7f06..03d339dbe121 100644 --- a/src/media-understanding/shared.test.ts +++ b/src/media-understanding/shared.test.ts @@ -637,6 +637,23 @@ describe("fetchWithTimeoutGuarded", () => { expect(call.timeoutMs).toBe(5000); }); + it("truncates auditContext without leaving a lone surrogate at the max boundary", async () => { + fetchWithSsrFGuardMock.mockResolvedValue({ + response: new Response(null, { status: 200 }), + finalUrl: "https://example.com", + release: async () => {}, + }); + + const prefix = "a".repeat(79); + await fetchWithTimeoutGuarded("https://example.com", {}, 5000, fetch, { + auditContext: `${prefix}${String.fromCodePoint(0x1f600)}tail`, + }); + + const call = getFirstGuardedFetchCall(); + expect(call.auditContext).toBe(prefix); + expect(call.auditContext).not.toContain(String.fromCharCode(0xd83d)); + }); + it("passes configured explicit proxy policy through the SSRF guard", async () => { fetchWithSsrFGuardMock.mockResolvedValue({ response: new Response(null, { status: 200 }), diff --git a/src/media-understanding/shared.ts b/src/media-understanding/shared.ts index dd37d956db09..19fff480f9e9 100644 --- a/src/media-understanding/shared.ts +++ b/src/media-understanding/shared.ts @@ -1,6 +1,7 @@ // Shared provider HTTP/audio helpers for media-understanding integrations, // including guarded fetches, deadlines, retries, and multipart upload bodies. import path from "node:path"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { assertOkOrThrowHttpError, createProviderHttpError, @@ -321,7 +322,7 @@ function sanitizeAuditContext(auditContext: string | undefined): string | undefi if (!cleaned) { return undefined; } - return cleaned.slice(0, MAX_AUDIT_CONTEXT_CHARS); + return truncateUtf16Safe(cleaned, MAX_AUDIT_CONTEXT_CHARS); } type ResolvedProviderHttpRequestConfig = {