From a90bef3970b953d9ee97362e9d033afe21b8f01c Mon Sep 17 00:00:00 2001 From: Leon-SK668 <0668001470@xydigit.com> Date: Tue, 28 Jul 2026 09:55:15 +0800 Subject: [PATCH] fix(usage): guard malformed Copilot payloads (#110795) Co-authored-by: Leon-SK668 <17695126+Leon-SK668@users.noreply.github.com> Co-authored-by: Peter Steinberger --- extensions/github-copilot/models.test.ts | 16 ++++++++++++++++ extensions/github-copilot/usage.ts | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/extensions/github-copilot/models.test.ts b/extensions/github-copilot/models.test.ts index 21ddfc8f028b..1ad6f8605008 100644 --- a/extensions/github-copilot/models.test.ts +++ b/extensions/github-copilot/models.test.ts @@ -301,6 +301,22 @@ describe("fetchCopilotUsage", () => { }); }); + it.each([ + ["null", null], + ["an array", []], + ])("returns an empty window list for a non-object %s payload", async (_label, payload) => { + const mockFetch = createProviderUsageFetch(async () => makeResponse(200, payload)); + + const result = await fetchCopilotUsage("token", 5000, mockFetch); + + expect(result).toEqual({ + provider: "github-copilot", + displayName: "Copilot", + windows: [], + plan: undefined, + }); + }); + it("bounds the usage read and cancels the stream when the body exceeds the JSON byte cap", async () => { // Larger than the shared 16 MiB readProviderJsonResponse cap so the bounded reader cancels the // stream mid-flight; if the cap were removed the unbounded res.json() would buffer the whole body. diff --git a/extensions/github-copilot/usage.ts b/extensions/github-copilot/usage.ts index e968d3b4019f..f8e817f3b82c 100644 --- a/extensions/github-copilot/usage.ts +++ b/extensions/github-copilot/usage.ts @@ -9,6 +9,7 @@ import { type ProviderUsageSnapshot, type UsageWindow, } from "openclaw/plugin-sdk/provider-usage"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { PUBLIC_GITHUB_COPILOT_DOMAIN } from "./domain.js"; type CopilotUsageResponse = { @@ -45,7 +46,8 @@ export async function fetchCopilotUsage( }); } - const data = await readProviderJsonResponse(res, "github-copilot-usage"); + const payload = await readProviderJsonResponse(res, "github-copilot-usage"); + const data = isRecord(payload) ? (payload as CopilotUsageResponse) : {}; const windows: UsageWindow[] = []; if (data.quota_snapshots?.premium_interactions) {