Skip to content

Commit 0866391

Browse files
lzyyzznlsteipete
andauthored
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 <[email protected]> --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 1e377e3 commit 0866391

2 files changed

Lines changed: 15 additions & 6 deletions

File tree

extensions/inworld/tts.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,14 @@ describe("inworldTTS", () => {
228228
);
229229
});
230230

231+
it("keeps truncated HTTP error bodies UTF-16 safe", async () => {
232+
queueGuardedResponse(new Response(`${"e".repeat(399)}😀tail`, { status: 400 }));
233+
234+
await expect(inworldTTS({ text: "test", apiKey: "test-key" })).rejects.toMatchObject({
235+
message: `Inworld TTS API error (400): ${"e".repeat(399)}…`,
236+
});
237+
});
238+
231239
it("throws on in-stream errors", async () => {
232240
const body = JSON.stringify({
233241
error: { code: 3, message: "Invalid voice ID" },
@@ -249,11 +257,11 @@ describe("inworldTTS", () => {
249257
});
250258

251259
it("throws descriptive error on non-JSON line in stream", async () => {
252-
queueGuardedResponse(new Response("<html>Rate limited</html>", { status: 200 }));
260+
queueGuardedResponse(new Response(`${"p".repeat(79)}😀tail`, { status: 200 }));
253261

254-
await expect(inworldTTS({ text: "test", apiKey: "test-key" })).rejects.toThrow(
255-
"Inworld TTS stream parse error: unexpected non-JSON line:",
256-
);
262+
await expect(inworldTTS({ text: "test", apiKey: "test-key" })).rejects.toMatchObject({
263+
message: `Inworld TTS stream parse error: unexpected non-JSON line: ${"p".repeat(79)}`,
264+
});
257265
});
258266

259267
it("sends correct request body with defaults", async () => {

extensions/inworld/tts.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/media-runtime";
33
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
44
import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech-core";
55
import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
6+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
67

78
const DEFAULT_INWORLD_BASE_URL = "https://api.inworld.ai";
89
export const DEFAULT_INWORLD_VOICE_ID = "Sarah";
@@ -57,7 +58,7 @@ async function readInworldErrorBodySnippet(response: Response): Promise<string>
5758

5859
const collapsed = buffer.toString("utf8").replace(/\s+/g, " ").trim();
5960
if (collapsed.length > INWORLD_ERROR_BODY_MAX_CHARS) {
60-
return `${collapsed.slice(0, INWORLD_ERROR_BODY_MAX_CHARS)}…`;
61+
return `${truncateUtf16Safe(collapsed, INWORLD_ERROR_BODY_MAX_CHARS)}…`;
6162
}
6263
return collapsed;
6364
}
@@ -176,7 +177,7 @@ export async function inworldTTS(params: {
176177
parsed = JSON.parse(trimmed) as typeof parsed;
177178
} catch {
178179
throw new Error(
179-
`Inworld TTS stream parse error: unexpected non-JSON line: ${trimmed.slice(0, 80)}`,
180+
`Inworld TTS stream parse error: unexpected non-JSON line: ${truncateUtf16Safe(trimmed, 80)}`,
180181
);
181182
}
182183

0 commit comments

Comments
 (0)