diff --git a/extensions/ollama/src/embedding-provider.test.ts b/extensions/ollama/src/embedding-provider.test.ts index 13f8d1f43280..63c0eae2c469 100644 --- a/extensions/ollama/src/embedding-provider.test.ts +++ b/extensions/ollama/src/embedding-provider.test.ts @@ -83,6 +83,28 @@ function firstGuardedFetchCall(): Record { return call as Record; } +function cancelTrackedResponse( + text: string, + init: ResponseInit, +): { + response: Response; + wasCanceled: () => boolean; +} { + let canceled = false; + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(text)); + }, + cancel() { + canceled = true; + }, + }); + return { + response: new Response(stream, init), + wasCanceled: () => canceled, + }; +} + function expectEmbeddingFetch( fetchMock: ReturnType, url: string, @@ -317,6 +339,39 @@ describe("ollama embedding provider", () => { }); }); + it("bounds embed error bodies without using response.text()", async () => { + const tracked = cancelTrackedResponse(`${"ollama embed unavailable ".repeat(1024)}tail`, { + status: 503, + headers: { "content-type": "text/plain" }, + }); + const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded")); + vi.stubGlobal( + "fetch", + vi.fn(async () => tracked.response), + ); + + const { provider } = await createOllamaEmbeddingProvider({ + config: {} as OpenClawConfig, + provider: "ollama", + model: "nomic-embed-text", + fallback: "none", + remote: { baseUrl: "http://127.0.0.1:11434" }, + }); + + let error: unknown; + try { + await provider.embedQuery("hello"); + } catch (err) { + error = err; + } + + expect(String(error)).toContain("Ollama embed HTTP 503"); + expect(String(error)).toContain("ollama embed unavailable"); + expect(String(error)).not.toContain("tail"); + expect(tracked.wasCanceled()).toBe(true); + expect(textSpy).not.toHaveBeenCalled(); + }); + it("reports malformed embed JSON with a provider-owned error", async () => { vi.stubGlobal( "fetch", diff --git a/extensions/ollama/src/embedding-provider.ts b/extensions/ollama/src/embedding-provider.ts index 56763f4c94f1..4132a3c5badf 100644 --- a/extensions/ollama/src/embedding-provider.ts +++ b/extensions/ollama/src/embedding-provider.ts @@ -6,6 +6,7 @@ import { normalizeOptionalSecretInput, } from "openclaw/plugin-sdk/provider-auth"; import { resolveEnvApiKey } from "openclaw/plugin-sdk/provider-auth-runtime"; +import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http"; import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared"; import { hasConfiguredSecretInput, @@ -57,6 +58,7 @@ export type OllamaEmbeddingClient = { type OllamaEmbeddingClientConfig = Omit; export const DEFAULT_OLLAMA_EMBEDDING_MODEL = "nomic-embed-text"; +const OLLAMA_EMBED_ERROR_BODY_LIMIT_BYTES = 8 * 1024; const QUERY_INSTRUCTION_TEMPLATES = [ { @@ -340,7 +342,11 @@ export async function createOllamaEmbeddingProvider( }, onResponse: async (response) => { if (!response.ok) { - throw new Error(`Ollama embed HTTP ${response.status}: ${await response.text()}`); + const detail = await readResponseTextLimited( + response, + OLLAMA_EMBED_ERROR_BODY_LIMIT_BYTES, + ).catch(() => "unknown error"); + throw new Error(`Ollama embed HTTP ${response.status}: ${detail}`); } return await readOllamaEmbeddingJsonResponse(response); },