fix(msteams): wrap malformed api json

This commit is contained in:
Vincent Koc
2026-05-15 08:52:15 +08:00
parent 376a792100
commit c98459d9da
5 changed files with 58 additions and 2 deletions

View File

@@ -85,6 +85,7 @@ Docs: https://docs.openclaw.ai
- Brave Search: report malformed web and LLM-context API JSON with provider-owned errors instead of leaking raw parser failures.
- xAI tools: report malformed web search, X search, and code execution JSON with provider-owned errors instead of leaking raw parser failures.
- Nextcloud Talk: report malformed room-info and bot-admin JSON with channel-owned errors instead of leaking raw parser failures.
- Microsoft Teams: report malformed Graph and delegated OAuth JSON with channel-owned errors 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

@@ -193,6 +193,19 @@ describe("msteams graph helpers", () => {
}),
"Graph /teams/team-1/channels failed (403): forbidden",
);
mockTextFetchResponse("{ nope", {
status: 200,
headers: { "content-type": "application/json" },
});
await expectRejectsToThrow(
fetchGraphJson({
token: graphToken,
path: "/teams/team-1/channels",
}),
"Graph /teams/team-1/channels failed: malformed JSON response",
);
});
it("posts Graph JSON to v1 and beta roots and treats empty mutation responses as undefined", async () => {

View File

@@ -1,3 +1,4 @@
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import { fetchWithSsrFGuard, type MSTeamsConfig } from "../runtime-api.js";
import { GRAPH_ROOT } from "./attachments/shared.js";
@@ -118,7 +119,7 @@ export async function fetchGraphAbsoluteUrl<T>(params: {
`Graph ${params.url} failed (${response.status}): ${text || "unknown error"}`,
);
}
return (await response.json()) as T;
return await readProviderJsonResponse<T>(response, `Graph ${params.url} failed`);
} finally {
await release();
}

View File

@@ -228,6 +228,25 @@ describe("exchangeMSTeamsCodeForTokens", () => {
}),
).rejects.toThrow(/MSTeams token exchange failed \(400\)/);
});
it("reports malformed token exchange JSON with a stable OAuth error", async () => {
fetchSpy.mockResolvedValueOnce(
new Response("{ nope", {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
await expect(
exchangeMSTeamsCodeForTokens({
tenantId: "t",
clientId: "c",
clientSecret: "s", // pragma: allowlist secret
code: "bad-json",
verifier: "v",
}),
).rejects.toThrow("MSTeams token exchange failed: malformed JSON response");
});
});
describe("refreshMSTeamsDelegatedTokens", () => {
@@ -310,4 +329,22 @@ describe("refreshMSTeamsDelegatedTokens", () => {
}),
).rejects.toThrow(/MSTeams token refresh failed \(401\)/);
});
it("reports malformed token refresh JSON with a stable OAuth error", async () => {
fetchSpy.mockResolvedValueOnce(
new Response("{ nope", {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
await expect(
refreshMSTeamsDelegatedTokens({
tenantId: "t",
clientId: "c",
clientSecret: "s", // pragma: allowlist secret
refreshToken: "bad-json",
}),
).rejects.toThrow("MSTeams token refresh failed: malformed JSON response");
});
});

View File

@@ -1,3 +1,4 @@
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import {
MSTEAMS_DEFAULT_DELEGATED_SCOPES,
@@ -65,7 +66,10 @@ async function fetchMSTeamsTokens(params: {
const errorText = await response.text();
throw new Error(`MSTeams ${params.failureLabel} failed (${response.status}): ${errorText}`);
}
return (await response.json()) as MSTeamsTokenResponse;
return await readProviderJsonResponse<MSTeamsTokenResponse>(
response,
`MSTeams ${params.failureLabel} failed`,
);
} finally {
await release();
}