fix(agents): keep provider error detail truncation UTF-16 safe (#102496)

* fix(agents): keep provider error detail truncation UTF-16 safe

Replace the raw detail.slice(0, limit - 1) in truncateErrorDetail with
truncateUtf16Safe so provider error previews do not emit lone surrogates
when an emoji falls on the truncation boundary.

Adds a regression test that places an emoji at the default truncation
boundary and asserts the formatted detail contains no lone surrogates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(agents): prove provider error UTF-16 boundary

---------

Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
chengzhichao-xydt
2026-07-09 15:44:29 +08:00
committed by GitHub
parent bacf048ab9
commit 51bb5f6847
2 changed files with 17 additions and 1 deletions
+15
View File
@@ -136,6 +136,21 @@ describe("provider error utils", () => {
);
});
it("does not split UTF-16 surrogate pairs when truncating provider error details", async () => {
const safePrefix = "a".repeat(218);
const message = `${safePrefix}😀suffix`;
const response = new Response(
JSON.stringify({
error: { message, code: "utf16_test" },
}),
{ status: 400 },
);
await expect(assertOkOrThrowProviderError(response, "Provider API error")).rejects.toThrow(
`Provider API error (400): ${safePrefix}… [code=utf16_test]`,
);
});
it("keeps HTTP status metadata when error body reads fail", async () => {
const response = {
ok: false,
+2 -1
View File
@@ -4,6 +4,7 @@
* Transport adapters use this module to turn provider-specific response bodies,
* request ids, and binary payload guardrails into stable OpenClaw error shapes.
*/
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
export { asFiniteNumber } from "../../packages/normalization-core/src/number-coercion.js";
import { normalizeOptionalString as trimToUndefined } from "../../packages/normalization-core/src/string-coerce.js";
import { readResponseWithLimit } from "../infra/http-body.js";
@@ -25,7 +26,7 @@ export function asObject(value: unknown): Record<string, unknown> | undefined {
/** Trims provider error details to a log- and prompt-safe preview length. */
export function truncateErrorDetail(detail: string, limit = 220): string {
return detail.length <= limit ? detail : `${detail.slice(0, limit - 1)}`;
return detail.length <= limit ? detail : `${truncateUtf16Safe(detail, limit - 1)}`;
}
/** Redacts secrets before preserving a bounded provider error body preview. */