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 <li.zeyu@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
lzyyzznl 2026-07-09 19:09:34 +08:00 committed by GitHub
parent 1e377e3660
commit 08663917fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 15 additions and 6 deletions

View file

@ -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("<html>Rate limited</html>", { 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 () => {

View file

@ -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<string>
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)}`,
);
}