Skip to content

Commit 6037d1a

Browse files
committed
fix(ollama): bound stream error bodies
1 parent 2c8d19d commit 6037d1a

2 files changed

Lines changed: 39 additions & 6 deletions

File tree

extensions/ollama/src/stream-runtime.test.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,28 @@ function getGuardedFetchCall(fetchMock: typeof fetchWithSsrFGuardMock): GuardedF
15341534
return (fetchMock.mock.calls.at(0)?.[0] as GuardedFetchCall | undefined) ?? { url: "" };
15351535
}
15361536

1537+
function cancelTrackedResponse(
1538+
text: string,
1539+
init: ResponseInit,
1540+
): {
1541+
response: Response;
1542+
wasCanceled: () => boolean;
1543+
} {
1544+
let canceled = false;
1545+
const stream = new ReadableStream<Uint8Array>({
1546+
start(controller) {
1547+
controller.enqueue(new TextEncoder().encode(text));
1548+
},
1549+
cancel() {
1550+
canceled = true;
1551+
},
1552+
});
1553+
return {
1554+
response: new Response(stream, init),
1555+
wasCanceled: () => canceled,
1556+
};
1557+
}
1558+
15371559
async function createOllamaTestStream(params: {
15381560
baseUrl: string;
15391561
defaultHeaders?: Record<string, string>;
@@ -2684,12 +2706,14 @@ describe("createOllamaStreamFn", () => {
26842706
);
26852707
});
26862708

2687-
it("surfaces non-2xx HTTP response as status-prefixed error", async () => {
2709+
it("surfaces bounded non-2xx HTTP response text as a status-prefixed error", async () => {
2710+
const tracked = cancelTrackedResponse(`${"Service Unavailable ".repeat(1024)}tail`, {
2711+
status: 503,
2712+
statusText: "Service Unavailable",
2713+
});
2714+
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
26882715
fetchWithSsrFGuardMock.mockResolvedValue({
2689-
response: new Response("Service Unavailable", {
2690-
status: 503,
2691-
statusText: "Service Unavailable",
2692-
}),
2716+
response: tracked.response,
26932717
release: vi.fn(async () => undefined),
26942718
});
26952719
try {
@@ -2705,6 +2729,10 @@ describe("createOllamaStreamFn", () => {
27052729
// The error message must start with the HTTP status code so that
27062730
// extractLeadingHttpStatus can parse it for failover/retry logic.
27072731
expect(errorEvent.error.errorMessage).toMatch(/^503\b/);
2732+
expect(errorEvent.error.errorMessage).toContain("Service Unavailable");
2733+
expect(errorEvent.error.errorMessage).not.toContain("tail");
2734+
expect(tracked.wasCanceled()).toBe(true);
2735+
expect(textSpy).not.toHaveBeenCalled();
27082736
} finally {
27092737
fetchWithSsrFGuardMock.mockReset();
27102738
}

extensions/ollama/src/stream.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
ProviderWrapStreamFnContext,
1919
} from "openclaw/plugin-sdk/plugin-entry";
2020
import { isNonSecretApiKeyMarker } from "openclaw/plugin-sdk/provider-auth";
21+
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
2122
import {
2223
DEFAULT_CONTEXT_TOKENS,
2324
normalizeProviderId,
@@ -54,6 +55,7 @@ export const OLLAMA_NATIVE_BASE_URL = OLLAMA_DEFAULT_BASE_URL;
5455

5556
const OLLAMA_STREAM_COOPERATIVE_YIELD_INTERVAL_MS = 12;
5657
const OLLAMA_STREAM_COOPERATIVE_YIELD_MAX_EVENTS = 64;
58+
const OLLAMA_STREAM_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
5759
const GARBLED_VISIBLE_TEXT_MODEL_RE = /\b(?:glm|kimi)\b/i;
5860
const GARBLED_VISIBLE_TEXT_MIN_CHARS = 80;
5961
const GARBLED_VISIBLE_TEXT_SYMBOL_RE = /[$#%&="'_~`^|\\/*+\-[\]{}()<>:;,.!?]/gu;
@@ -1211,7 +1213,10 @@ function createRawOllamaStreamFn(
12111213

12121214
try {
12131215
if (!response.ok) {
1214-
const errorText = await response.text().catch(() => "unknown error");
1216+
const errorText = await readResponseTextLimited(
1217+
response,
1218+
OLLAMA_STREAM_ERROR_BODY_LIMIT_BYTES,
1219+
).catch(() => "unknown error");
12151220
throw new Error(`${response.status} ${errorText}`);
12161221
}
12171222
if (!response.body) {

0 commit comments

Comments
 (0)