From 08663917fa3732bb6192da78dae6c093730a3a26 Mon Sep 17 00:00:00 2001
From: lzyyzznl
Date: Thu, 9 Jul 2026 19:09:34 +0800
Subject: [PATCH] fix(inworld): use truncateUtf16Safe for error body and parse
error truncation (#102608)
* fix(inworld): use truncateUtf16Safe for error body and parse error truncation
Two .slice(0, N) truncation sites in the Inworld TTS extension may
cut UTF-16 surrogate pairs in half when the error body or parse error
message contains multi-byte characters such as emoji. Replace both
with truncateUtf16Safe to keep the truncated output valid Unicode.
* test(inworld): cover UTF-16-safe TTS errors
Co-authored-by: lizeyu-xydt
---------
Co-authored-by: Peter Steinberger
---
extensions/inworld/tts.test.ts | 16 ++++++++++++----
extensions/inworld/tts.ts | 5 +++--
2 files changed, 15 insertions(+), 6 deletions(-)
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)}`,
);
}