mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
fix(chutes): bound OAuth token error response reads (#97808)
* fix(chutes): bound OAuth token error response reads * ci: re-trigger checks (fs-safe unhandled-rejection flake on prior run) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Pick-cat <266665499+Pick-cat@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,28 @@ function createStoredCredential(
|
||||
} as unknown as Parameters<typeof refreshChutesTokens>[0]["credential"];
|
||||
}
|
||||
|
||||
function cancelTrackedResponse(
|
||||
text: string,
|
||||
init: ResponseInit,
|
||||
): {
|
||||
response: Response;
|
||||
wasCanceled: () => boolean;
|
||||
} {
|
||||
let canceled = false;
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(text));
|
||||
},
|
||||
cancel() {
|
||||
canceled = true;
|
||||
},
|
||||
});
|
||||
return {
|
||||
response: new Response(stream, init),
|
||||
wasCanceled: () => canceled,
|
||||
};
|
||||
}
|
||||
|
||||
function expectRefreshedCredential(
|
||||
refreshed: Awaited<ReturnType<typeof refreshChutesTokens>>,
|
||||
now: number,
|
||||
@@ -212,6 +234,62 @@ describe("chutes-oauth", () => {
|
||||
expectRefreshedCredential(refreshed, now);
|
||||
});
|
||||
|
||||
it("bounds token exchange error bodies without using response.text()", async () => {
|
||||
const tracked = cancelTrackedResponse(`${"chutes exchange failure ".repeat(1024)}tail`, {
|
||||
status: 401,
|
||||
headers: { "content-type": "text/plain" },
|
||||
});
|
||||
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
|
||||
const fetchFn = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url === CHUTES_TOKEN_ENDPOINT) {
|
||||
return tracked.response;
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
await expect(
|
||||
exchangeChutesCodeForTokens({
|
||||
app: {
|
||||
clientId: "cid_test",
|
||||
redirectUri: "http://127.0.0.1:1456/oauth-callback",
|
||||
scopes: ["openid"],
|
||||
},
|
||||
code: "code_401",
|
||||
codeVerifier: "verifier_401",
|
||||
fetchFn,
|
||||
now: 1_000_000,
|
||||
}),
|
||||
).rejects.toThrow("Chutes token exchange failed: chutes exchange failure");
|
||||
expect(textSpy).not.toHaveBeenCalled();
|
||||
expect(tracked.wasCanceled()).toBe(true);
|
||||
});
|
||||
|
||||
it("bounds token refresh error bodies without using response.text()", async () => {
|
||||
const tracked = cancelTrackedResponse(`${"chutes refresh failure ".repeat(1024)}tail`, {
|
||||
status: 401,
|
||||
headers: { "content-type": "text/plain" },
|
||||
});
|
||||
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
|
||||
const fetchFn = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url === CHUTES_TOKEN_ENDPOINT) {
|
||||
return tracked.response;
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
await expect(
|
||||
refreshChutesTokens({
|
||||
credential: createStoredCredential(5_000_000),
|
||||
fetchFn,
|
||||
now: 5_000_000,
|
||||
}),
|
||||
).rejects.toThrow("Chutes token refresh failed: chutes refresh failure");
|
||||
expect(textSpy).not.toHaveBeenCalled();
|
||||
expect(tracked.wasCanceled()).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unsafe refresh token lifetimes", async () => {
|
||||
const fetchFn = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
|
||||
@@ -6,7 +6,9 @@ import { createHash, randomBytes } from "node:crypto";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { resolveExpiresAtMsFromDurationSeconds } from "../infra/parse-finite-number.js";
|
||||
import type { OAuthCredentials } from "../llm/oauth.js";
|
||||
import { readProviderJsonResponse } from "./provider-http-errors.js";
|
||||
import { readProviderJsonResponse, readResponseTextLimited } from "./provider-http-errors.js";
|
||||
|
||||
const CHUTES_OAUTH_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
||||
|
||||
const CHUTES_OAUTH_ISSUER = "https://api.chutes.ai";
|
||||
export const CHUTES_AUTHORIZE_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/authorize`;
|
||||
@@ -152,7 +154,7 @@ export async function exchangeChutesCodeForTokens(params: {
|
||||
body,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
const text = await readResponseTextLimited(response, CHUTES_OAUTH_ERROR_BODY_LIMIT_BYTES);
|
||||
throw new Error(`Chutes token exchange failed: ${text}`);
|
||||
}
|
||||
|
||||
@@ -223,7 +225,7 @@ export async function refreshChutesTokens(params: {
|
||||
body,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
const text = await readResponseTextLimited(response, CHUTES_OAUTH_ERROR_BODY_LIMIT_BYTES);
|
||||
throw new Error(`Chutes token refresh failed: ${text}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user