Skip to content

Commit a42a1af

Browse files
committed
fix(openrouter): bound oauth error bodies
1 parent b470b1e commit a42a1af

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

extensions/openrouter/oauth.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,42 @@ function jsonResponse(value: unknown, init?: ResponseInit): Response {
2424
});
2525
}
2626

27+
function boundedTextErrorResponse(body: string, status = 502): {
28+
response: Response;
29+
cancel: ReturnType<typeof vi.fn>;
30+
releaseLock: ReturnType<typeof vi.fn>;
31+
text: ReturnType<typeof vi.fn>;
32+
} {
33+
const encoded = new TextEncoder().encode(body);
34+
let read = false;
35+
const cancel = vi.fn(async () => undefined);
36+
const releaseLock = vi.fn();
37+
const text = vi.fn(async () => {
38+
throw new Error("response.text() should not be called");
39+
});
40+
const response = {
41+
ok: false,
42+
status,
43+
headers: new Headers(),
44+
body: {
45+
getReader: () => ({
46+
read: async () => {
47+
if (read) {
48+
return { done: true, value: undefined };
49+
}
50+
read = true;
51+
return { done: false, value: encoded };
52+
},
53+
cancel,
54+
releaseLock,
55+
}),
56+
},
57+
text,
58+
} as unknown as Response;
59+
60+
return { response, cancel, releaseLock, text };
61+
}
62+
2763
function requestUrl(input: RequestInfo | URL): string {
2864
if (typeof input === "string") {
2965
return input;
@@ -180,6 +216,33 @@ describe("OpenRouter OAuth", () => {
180216
).rejects.toThrow("OpenRouter OAuth key exchange failed (400): Invalid code");
181217
});
182218

219+
it("bounds OpenRouter OAuth exchange error bodies without requiring response.text()", async () => {
220+
const errorResponse = boundedTextErrorResponse(
221+
`${"openrouter denied ".repeat(1024)}tail-marker`,
222+
502,
223+
);
224+
const fetchImpl = vi.fn<typeof fetch>(async () => errorResponse.response);
225+
226+
let error: unknown;
227+
try {
228+
await exchangeOpenRouterOAuthCode({
229+
code: "bad-code",
230+
codeVerifier: "bad-verifier",
231+
fetchImpl,
232+
});
233+
} catch (caught) {
234+
error = caught;
235+
}
236+
237+
expect(error).toBeInstanceOf(Error);
238+
const message = (error as Error).message;
239+
expect(message).toContain("OpenRouter OAuth key exchange failed (502): openrouter denied");
240+
expect(message).not.toContain("tail-marker");
241+
expect(errorResponse.text).not.toHaveBeenCalled();
242+
expect(errorResponse.cancel).toHaveBeenCalledTimes(1);
243+
expect(errorResponse.releaseLock).toHaveBeenCalledTimes(1);
244+
});
245+
183246
it("stores a browser OAuth result as the default OpenRouter API-key profile", async () => {
184247
const fetchImpl = vi.fn<typeof fetch>(async () =>
185248
jsonResponse({ key: "sk-or-v1-test", user_id: "user-1" }),

extensions/openrouter/oauth.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type ProviderAuthResult,
99
} from "openclaw/plugin-sdk/provider-auth";
1010
import { generateOAuthState } from "openclaw/plugin-sdk/provider-auth-runtime";
11+
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
1112
import { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
1213

1314
const PROVIDER_ID = "openrouter";
@@ -23,6 +24,7 @@ export const OPENROUTER_OAUTH_CODE_CHALLENGE_METHOD = "S256";
2324

2425
const OPENROUTER_OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
2526
const OPENROUTER_OAUTH_FETCH_TIMEOUT_MS = 30 * 1000;
27+
const OPENROUTER_OAUTH_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
2628
const OPENROUTER_OAUTH_PROFILE_ID = "openrouter:default";
2729

2830
type OpenRouterOAuthCallbackResult = {
@@ -72,7 +74,11 @@ function extractOpenRouterError(value: unknown): string | undefined {
7274
}
7375

7476
async function readResponseBody(response: Response): Promise<unknown> {
75-
const text = await response.text();
77+
const text = response.ok
78+
? await response.text()
79+
: await readResponseTextLimited(response, OPENROUTER_OAUTH_ERROR_BODY_LIMIT_BYTES).catch(
80+
() => "",
81+
);
7682
if (!text.trim()) {
7783
return null;
7884
}

0 commit comments

Comments
 (0)