Skip to content

Commit 3161c88

Browse files
committed
fix(mattermost): reject oversized success text bodies
1 parent 404cfee commit 3161c88

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

extensions/mattermost/src/mattermost/client.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,25 @@ describe("createMattermostClient", () => {
244244
expect(release).toHaveBeenCalledTimes(1);
245245
});
246246

247+
it("rejects oversized guarded Mattermost success text bodies instead of truncating", async () => {
248+
const release = vi.fn(async () => {});
249+
const tracked = cancelTrackedResponse(`${"plain success ".repeat(7000)}tail`, {
250+
status: 200,
251+
headers: { "content-type": "text/plain" },
252+
});
253+
fetchWithSsrFGuardMock.mockResolvedValueOnce({ response: tracked.response, release });
254+
const client = createMattermostClient({
255+
baseUrl: "https://chat.example.com",
256+
botToken: "test-token",
257+
});
258+
259+
await expect(client.request("/users/me")).rejects.toThrow(
260+
"Mattermost API /users/me: text response exceeds 65536 bytes",
261+
);
262+
expect(tracked.wasCanceled()).toBe(true);
263+
expect(release).toHaveBeenCalledTimes(1);
264+
});
265+
247266
it("releases guarded Mattermost responses when upstream body reads fail", async () => {
248267
const release = vi.fn(async () => {});
249268
const stream = new ReadableStream<Uint8Array>({

extensions/mattermost/src/mattermost/client.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
readProviderJsonResponse,
55
readResponseTextLimited,
66
} from "openclaw/plugin-sdk/provider-http";
7+
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
78
import { sleep } from "openclaw/plugin-sdk/runtime-env";
89
import {
910
fetchWithSsrFGuard,
@@ -94,6 +95,14 @@ function buildMattermostApiUrl(baseUrl: string, path: string): string {
9495
return `${normalized}/api/v4${suffix}`;
9596
}
9697

98+
async function readMattermostSuccessText(res: Response, path: string): Promise<string> {
99+
const bytes = await readResponseWithLimit(res, MATTERMOST_TEXT_RESPONSE_LIMIT_BYTES, {
100+
onOverflow: ({ maxBytes }) =>
101+
new Error(`Mattermost API ${path}: text response exceeds ${maxBytes} bytes`),
102+
});
103+
return new TextDecoder().decode(bytes);
104+
}
105+
97106
export async function readMattermostError(res: Response): Promise<string> {
98107
const contentType = res.headers.get("content-type") ?? "";
99108
if (!res.body) {
@@ -226,7 +235,7 @@ export function createMattermostClient(params: {
226235
if (contentType.includes("application/json")) {
227236
return await readProviderJsonResponse<T>(res, `Mattermost API ${path}`);
228237
}
229-
return (await readResponseTextLimited(res, MATTERMOST_TEXT_RESPONSE_LIMIT_BYTES)) as T;
238+
return (await readMattermostSuccessText(res, path)) as T;
230239
};
231240

232241
return { baseUrl, apiBaseUrl, token, request, fetchImpl };

0 commit comments

Comments
 (0)