diff --git a/extensions/inworld/tts.test.ts b/extensions/inworld/tts.test.ts index bfb0b21ba47..b7e8bbcac09 100644 --- a/extensions/inworld/tts.test.ts +++ b/extensions/inworld/tts.test.ts @@ -228,6 +228,14 @@ describe("inworldTTS", () => { ); }); + it("keeps truncated HTTP error bodies UTF-16 safe", async () => { + queueGuardedResponse(new Response(`${"e".repeat(399)}😀tail`, { status: 400 })); + + await expect(inworldTTS({ text: "test", apiKey: "test-key" })).rejects.toMatchObject({ + message: `Inworld TTS API error (400): ${"e".repeat(399)}…`, + }); + }); + it("throws on in-stream errors", async () => { const body = JSON.stringify({ error: { code: 3, message: "Invalid voice ID" }, @@ -249,11 +257,11 @@ describe("inworldTTS", () => { }); it("throws descriptive error on non-JSON line in stream", async () => { - queueGuardedResponse(new Response("Rate limited", { status: 200 })); + queueGuardedResponse(new Response(`${"p".repeat(79)}😀tail`, { status: 200 })); - await expect(inworldTTS({ text: "test", apiKey: "test-key" })).rejects.toThrow( - "Inworld TTS stream parse error: unexpected non-JSON line:", - ); + await expect(inworldTTS({ text: "test", apiKey: "test-key" })).rejects.toMatchObject({ + message: `Inworld TTS stream parse error: unexpected non-JSON line: ${"p".repeat(79)}`, + }); }); it("sends correct request body with defaults", async () => { diff --git a/extensions/inworld/tts.ts b/extensions/inworld/tts.ts index 80a5762ecd2..2748442ace2 100644 --- a/extensions/inworld/tts.ts +++ b/extensions/inworld/tts.ts @@ -3,6 +3,7 @@ import { MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/media-runtime"; import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech-core"; import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; const DEFAULT_INWORLD_BASE_URL = "https://api.inworld.ai"; export const DEFAULT_INWORLD_VOICE_ID = "Sarah"; @@ -57,7 +58,7 @@ async function readInworldErrorBodySnippet(response: Response): Promise const collapsed = buffer.toString("utf8").replace(/\s+/g, " ").trim(); if (collapsed.length > INWORLD_ERROR_BODY_MAX_CHARS) { - return `${collapsed.slice(0, INWORLD_ERROR_BODY_MAX_CHARS)}…`; + return `${truncateUtf16Safe(collapsed, INWORLD_ERROR_BODY_MAX_CHARS)}…`; } return collapsed; } @@ -176,7 +177,7 @@ export async function inworldTTS(params: { parsed = JSON.parse(trimmed) as typeof parsed; } catch { throw new Error( - `Inworld TTS stream parse error: unexpected non-JSON line: ${trimmed.slice(0, 80)}`, + `Inworld TTS stream parse error: unexpected non-JSON line: ${truncateUtf16Safe(trimmed, 80)}`, ); }