fix(memory): wrap malformed remote json

This commit is contained in:
Vincent Koc
2026-05-15 09:06:24 +08:00
parent 31bfe7f084
commit f0803c576f
5 changed files with 72 additions and 2 deletions

View File

@@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai
- Tavily: report malformed search and extract API JSON with provider-owned errors instead of leaking raw parser failures.
- Perplexity: report malformed Search API and chat completion JSON with provider-owned errors instead of leaking raw parser failures.
- Exa: report malformed search API JSON with a provider-owned error instead of leaking raw parser failures.
- Memory host SDK: report malformed remote JSON with caller-scoped errors for POST and batch file upload responses instead of leaking raw parser failures.
- Twilio voice-call: report malformed successful API JSON responses with provider-owned errors instead of leaking raw parser failures.
- Voice-call provider APIs: report malformed successful guarded JSON responses with provider-prefixed errors instead of leaking raw parser failures.
- Realtime transcription: report malformed provider websocket JSON frames with owned parser errors instead of leaking raw `SyntaxError` objects.

View File

@@ -0,0 +1,41 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { uploadBatchJsonlFile } from "./batch-upload.js";
import { withRemoteHttpResponse } from "./remote-http.js";
vi.mock("./remote-http.js", () => ({
withRemoteHttpResponse: vi.fn(),
}));
const remoteHttpMock = vi.mocked(withRemoteHttpResponse);
function textResponse(body: string, status: number): Response {
return {
ok: status >= 200 && status < 300,
status,
json: async () => JSON.parse(body) as unknown,
text: async () => body,
} as Response;
}
describe("uploadBatchJsonlFile", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("wraps malformed file-upload JSON with the request error prefix", async () => {
remoteHttpMock.mockImplementationOnce(async (params) => {
return await params.onResponse(textResponse("{ nope", 200));
});
await expect(
uploadBatchJsonlFile({
client: {
baseUrl: "https://memory.example/v1",
headers: { Authorization: "Bearer test" },
},
requests: [{ input: "one" }],
errorPrefix: "file upload failed",
}),
).rejects.toThrow("file upload failed: malformed JSON response");
});
});

View File

@@ -34,7 +34,11 @@ export async function uploadBatchJsonlFile(params: {
const text = await fileRes.text();
throw new Error(`${params.errorPrefix}: ${fileRes.status} ${text}`);
}
return (await fileRes.json()) as { id?: string };
try {
return (await fileRes.json()) as { id?: string };
} catch (cause) {
throw new Error(`${params.errorPrefix}: malformed JSON response`, { cause });
}
},
});
if (!filePayload.id) {

View File

@@ -70,4 +70,20 @@ describe("postJson", () => {
expect((error as Error).message).toBe("post failed: 502 bad gateway");
expect((error as { status?: unknown }).status).toBe(502);
});
it("wraps malformed success JSON with the request error prefix", async () => {
remoteHttpMock.mockImplementationOnce(async (params) => {
return await params.onResponse(textResponse("{ nope", 200));
});
await expect(
postJson({
url: "https://memory.example/v1/post",
headers: {},
body: {},
errorPrefix: "post failed",
parse: () => ({}),
}),
).rejects.toThrow("post failed: malformed JSON response");
});
});

View File

@@ -31,7 +31,15 @@ export async function postJson<T>(params: {
}
throw err;
}
return await params.parse(await res.json());
return await params.parse(await readJsonResponse(res, params.errorPrefix));
},
});
}
async function readJsonResponse(res: Response, errorPrefix: string): Promise<unknown> {
try {
return await res.json();
} catch (cause) {
throw new Error(`${errorPrefix}: malformed JSON response`, { cause });
}
}