fix(ollama): bound stream error bodies

This commit is contained in:
Vincent Koc
2026-06-19 18:20:30 +02:00
parent 2c8d19d73e
commit 6037d1a85c
2 changed files with 39 additions and 6 deletions
+33 -5
View File
@@ -1534,6 +1534,28 @@ function getGuardedFetchCall(fetchMock: typeof fetchWithSsrFGuardMock): GuardedF
return (fetchMock.mock.calls.at(0)?.[0] as GuardedFetchCall | undefined) ?? { url: "" };
}
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,
};
}
async function createOllamaTestStream(params: {
baseUrl: string;
defaultHeaders?: Record<string, string>;
@@ -2684,12 +2706,14 @@ describe("createOllamaStreamFn", () => {
);
});
it("surfaces non-2xx HTTP response as status-prefixed error", async () => {
it("surfaces bounded non-2xx HTTP response text as a status-prefixed error", async () => {
const tracked = cancelTrackedResponse(`${"Service Unavailable ".repeat(1024)}tail`, {
status: 503,
statusText: "Service Unavailable",
});
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
fetchWithSsrFGuardMock.mockResolvedValue({
response: new Response("Service Unavailable", {
status: 503,
statusText: "Service Unavailable",
}),
response: tracked.response,
release: vi.fn(async () => undefined),
});
try {
@@ -2705,6 +2729,10 @@ describe("createOllamaStreamFn", () => {
// The error message must start with the HTTP status code so that
// extractLeadingHttpStatus can parse it for failover/retry logic.
expect(errorEvent.error.errorMessage).toMatch(/^503\b/);
expect(errorEvent.error.errorMessage).toContain("Service Unavailable");
expect(errorEvent.error.errorMessage).not.toContain("tail");
expect(tracked.wasCanceled()).toBe(true);
expect(textSpy).not.toHaveBeenCalled();
} finally {
fetchWithSsrFGuardMock.mockReset();
}
+6 -1
View File
@@ -18,6 +18,7 @@ import type {
ProviderWrapStreamFnContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { isNonSecretApiKeyMarker } from "openclaw/plugin-sdk/provider-auth";
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
import {
DEFAULT_CONTEXT_TOKENS,
normalizeProviderId,
@@ -54,6 +55,7 @@ export const OLLAMA_NATIVE_BASE_URL = OLLAMA_DEFAULT_BASE_URL;
const OLLAMA_STREAM_COOPERATIVE_YIELD_INTERVAL_MS = 12;
const OLLAMA_STREAM_COOPERATIVE_YIELD_MAX_EVENTS = 64;
const OLLAMA_STREAM_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
const GARBLED_VISIBLE_TEXT_MODEL_RE = /\b(?:glm|kimi)\b/i;
const GARBLED_VISIBLE_TEXT_MIN_CHARS = 80;
const GARBLED_VISIBLE_TEXT_SYMBOL_RE = /[$#%&="'_~`^|\\/*+\-[\]{}()<>:;,.!?]/gu;
@@ -1211,7 +1213,10 @@ function createRawOllamaStreamFn(
try {
if (!response.ok) {
const errorText = await response.text().catch(() => "unknown error");
const errorText = await readResponseTextLimited(
response,
OLLAMA_STREAM_ERROR_BODY_LIMIT_BYTES,
).catch(() => "unknown error");
throw new Error(`${response.status} ${errorText}`);
}
if (!response.body) {