From adb2783f1b950de819abe13c62de6cc86bb1483d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 13:10:50 +0000 Subject: [PATCH] Fix synthesis fetch failures from Node's default 5-minute HTTP timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing published for hours, every cluster failing with "fetch failed". Ollama's own log showed the real story: requests being cancelled at exactly 5m0s with a 500, not a model or server error. Node's global fetch (undici) defaults to a 5-minute headers/body timeout, and CPU-only prompt processing on the reference hardware (i5-6600K, no GPU, ~17 tok/s) legitimately takes longer than that once prompts carry full article bodies instead of short blurbs (the previous fix in this same line of work) — every generate() call past a few thousand tokens got killed client-side before Ollama could finish. OllamaProvider.generate() now passes a dedicated undici Agent with headersTimeout/bodyTimeout disabled as the fetch dispatcher, so the request runs as long as it actually needs to. Verified the failure mode and the fix directly: a short-timeout dispatcher against a deliberately slow server reproduces the exact same "fetch failed" / UND_ERR_HEADERS_TIMEOUT error seen in production, and a zero-timeout dispatcher completes the same slow request without issue. undici was already a transitive dependency (via jsdom); added directly since ollama-provider.ts now imports from it. --- backend/package-lock.json | 3 ++- backend/package.json | 3 ++- backend/src/inference/ollama-provider.ts | 21 +++++++++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 3bff8b6..120652a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,7 +15,8 @@ "fastify": "^5.10.0", "jsdom": "^29.1.1", "rss-parser": "^3.13.0", - "telegram": "^2.26.22" + "telegram": "^2.26.22", + "undici": "^7.28.0" }, "devDependencies": { "@types/jsdom": "^28.0.3", diff --git a/backend/package.json b/backend/package.json index 9eca3ab..fb0285e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,7 +18,8 @@ "fastify": "^5.10.0", "jsdom": "^29.1.1", "rss-parser": "^3.13.0", - "telegram": "^2.26.22" + "telegram": "^2.26.22", + "undici": "^7.28.0" }, "devDependencies": { "@types/jsdom": "^28.0.3", diff --git a/backend/src/inference/ollama-provider.ts b/backend/src/inference/ollama-provider.ts index 6817232..7173922 100644 --- a/backend/src/inference/ollama-provider.ts +++ b/backend/src/inference/ollama-provider.ts @@ -1,5 +1,19 @@ +import { Agent } from 'undici'; import type { InferenceProvider } from './provider.js'; +/** + * Node's global fetch (undici) defaults to a 5-minute headers/body timeout — fine for + * ordinary HTTP calls, but a real problem for /api/generate on CPU-only inference: a + * near-full context window can legitimately take longer than that just for prompt + * processing on the reference hardware (i5-6600K, no GPU, ~17 tokens/sec). Once + * synthesis prompts started carrying full article bodies instead of short blurbs, every + * generate() call past a few thousand tokens got killed at exactly 5m0s — visible in + * Ollama's own log as the request being cancelled, not a genuine model/server error — + * so no cluster could ever finish synthesizing. No timeout at all here; Ollama's own + * process is the natural backstop, not a clock tuned for hardware this doesn't run on. + */ +const noTimeoutDispatcher = new Agent({ headersTimeout: 0, bodyTimeout: 0 }); + /** * Default context window / max-generation length requested from Ollama when a caller * doesn't specify its own. Ollama otherwise falls back to whatever the model's @@ -51,8 +65,11 @@ export class OllamaProvider implements InferenceProvider { num_ctx: opts.numCtx ?? DEFAULT_NUM_CTX, num_predict: opts.numPredict ?? DEFAULT_NUM_PREDICT } - }) - }); + }), + // Not in the ambient RequestInit type this project resolves to, but Node's global + // fetch (built on undici) honors it at runtime — see noTimeoutDispatcher above. + dispatcher: noTimeoutDispatcher + } as RequestInit); if (!res.ok) throw new Error(`Ollama generate failed: ${res.status} ${await res.text()}`); const data = (await res.json()) as { response: string }; return data.response;