Skip to content

Commit dfee6bd

Browse files
committed
fix(msteams): bound graph upload JSON reads
1 parent 2913d3a commit dfee6bd

2 files changed

Lines changed: 70 additions & 14 deletions

File tree

extensions/msteams/src/graph-upload.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,41 @@ function bodyOnlyErrorResponse(body: string, status = 500): Response {
3232
} as unknown as Response;
3333
}
3434

35+
function oversizedJsonSuccessResponse(): {
36+
response: Response;
37+
getReadCount: () => number;
38+
wasCanceled: () => boolean;
39+
} {
40+
const chunkSize = 1024 * 1024;
41+
const chunkCount = 20;
42+
let readCount = 0;
43+
let canceled = false;
44+
const response = new Response(
45+
new ReadableStream<Uint8Array>({
46+
pull(controller) {
47+
if (readCount >= chunkCount) {
48+
controller.close();
49+
return;
50+
}
51+
readCount += 1;
52+
controller.enqueue(new Uint8Array(chunkSize));
53+
},
54+
cancel() {
55+
canceled = true;
56+
},
57+
}),
58+
{ status: 200, headers: { "content-type": "application/json" } },
59+
);
60+
response.json = async () => {
61+
throw new Error("unbounded response.json should not be called");
62+
};
63+
return {
64+
response,
65+
getReadCount: () => readCount,
66+
wasCanceled: () => canceled,
67+
};
68+
}
69+
3570
describe("graph upload helpers", () => {
3671
const tokenProvider = {
3772
getAccessToken: vi.fn(async () => "graph-token"),
@@ -142,6 +177,26 @@ describe("graph upload helpers", () => {
142177
expect(message).not.toContain("tail-marker");
143178
expect(message.length).toBeLessThan(700);
144179
});
180+
181+
it("bounds successful SharePoint upload JSON bodies without using response.json()", async () => {
182+
const streamed = oversizedJsonSuccessResponse();
183+
const fetchFn = vi.fn(async () => streamed.response);
184+
185+
const error = await uploadToSharePoint({
186+
buffer: Buffer.from("world"),
187+
filename: "large.txt",
188+
siteId: "site-123",
189+
tokenProvider,
190+
fetchFn: fetchFn as unknown as typeof fetch,
191+
}).catch((caught: unknown) => caught);
192+
193+
expect(error).toBeInstanceOf(Error);
194+
expect((error as Error).message).toBe(
195+
"SharePoint upload failed: JSON response exceeds 16777216 bytes",
196+
);
197+
expect(streamed.wasCanceled()).toBe(true);
198+
expect(streamed.getReadCount()).toBeLessThan(20);
199+
});
145200
});
146201

147202
describe("resolveGraphChatId", () => {

extensions/msteams/src/graph-upload.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* - Getting chat members for per-user sharing
1010
*/
1111

12+
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
1213
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
1314
import { createMSTeamsHttpError } from "./http-error.js";
1415
import { buildUserAgent } from "./user-agent.js";
@@ -54,11 +55,11 @@ export async function uploadToOneDrive(params: {
5455
throw await createMSTeamsHttpError(res, "OneDrive upload failed");
5556
}
5657

57-
const data = (await res.json()) as {
58+
const data = await readProviderJsonResponse<{
5859
id?: string;
5960
webUrl?: string;
6061
name?: string;
61-
};
62+
}>(res, "OneDrive upload failed");
6263

6364
if (!data.id || !data.webUrl || !data.name) {
6465
throw new Error("OneDrive upload response missing required fields");
@@ -106,9 +107,9 @@ async function createSharingLink(params: {
106107
throw await createMSTeamsHttpError(res, "Create sharing link failed");
107108
}
108109

109-
const data = (await res.json()) as {
110+
const data = await readProviderJsonResponse<{
110111
link?: { webUrl?: string };
111-
};
112+
}>(res, "Create sharing link failed");
112113

113114
if (!data.link?.webUrl) {
114115
throw new Error("Create sharing link response missing webUrl");
@@ -200,11 +201,11 @@ export async function uploadToSharePoint(params: {
200201
throw await createMSTeamsHttpError(res, "SharePoint upload failed");
201202
}
202203

203-
const data = (await res.json()) as {
204+
const data = await readProviderJsonResponse<{
204205
id?: string;
205206
webUrl?: string;
206207
name?: string;
207-
};
208+
}>(res, "SharePoint upload failed");
208209

209210
if (!data.id || !data.webUrl || !data.name) {
210211
throw new Error("SharePoint upload response missing required fields");
@@ -260,11 +261,11 @@ export async function getDriveItemProperties(params: {
260261
throw await createMSTeamsHttpError(res, "Get driveItem properties failed");
261262
}
262263

263-
const data = (await res.json()) as {
264+
const data = await readProviderJsonResponse<{
264265
eTag?: string;
265266
webDavUrl?: string;
266267
name?: string;
267-
};
268+
}>(res, "Get driveItem properties failed");
268269

269270
if (!data.eTag || !data.webDavUrl || !data.name) {
270271
throw new Error("DriveItem response missing required properties (eTag, webDavUrl, or name)");
@@ -331,9 +332,9 @@ export async function resolveGraphChatId(params: {
331332
return null;
332333
}
333334

334-
const data = (await res.json()) as {
335+
const data = await readProviderJsonResponse<{
335336
value?: Array<{ id?: string }>;
336-
};
337+
}>(res, "Resolve Graph chat failed");
337338

338339
const chats = data.value ?? [];
339340

@@ -371,12 +372,12 @@ async function getChatMembers(params: {
371372
throw await createMSTeamsHttpError(res, "Get chat members failed");
372373
}
373374

374-
const data = (await res.json()) as {
375+
const data = await readProviderJsonResponse<{
375376
value?: Array<{
376377
userId?: string;
377378
displayName?: string;
378379
}>;
379-
};
380+
}>(res, "Get chat members failed");
380381

381382
return (data.value ?? [])
382383
.map((m) => ({
@@ -435,9 +436,9 @@ async function createSharePointSharingLink(params: {
435436
throw await createMSTeamsHttpError(res, "Create SharePoint sharing link failed");
436437
}
437438

438-
const data = (await res.json()) as {
439+
const data = await readProviderJsonResponse<{
439440
link?: { webUrl?: string };
440-
};
441+
}>(res, "Create SharePoint sharing link failed");
441442

442443
if (!data.link?.webUrl) {
443444
throw new Error("Create SharePoint sharing link response missing webUrl");

0 commit comments

Comments
 (0)