From ee2eaabd65a2e9995e933b95e8fd1a963e641660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Artur=20M=C4=99dryga=C5=82?= Date: Mon, 12 Oct 2026 09:12:08 +0200 Subject: [PATCH] fix(runner): keep a mid-body network cut as a transport error in fetchDocsJson (DEV-3370) A docs manifest whose connection died after the JSON headers arrived made res.json() reject with the browser's TypeError: Failed to fetch, which was relabelled 'unparseable JSON'. Only a SyntaxError is a bad body now; anything else is rethrown untouched. Co-Authored-By: Claude Sonnet 5.5 --- runner/apps/authoring/src/docs-catalog.ts | 3 +++ runner/pipeline/docs-catalog.test.mjs | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/runner/apps/authoring/src/docs-catalog.ts b/runner/apps/authoring/src/docs-catalog.ts index da13eb1d31..21a7f15c4a 100644 --- a/runner/apps/authoring/src/docs-catalog.ts +++ b/runner/apps/authoring/src/docs-catalog.ts @@ -57,6 +57,9 @@ async function fetchDocsJson(url: string, describe: string): Promise { try { return (await res.json()) as T; } catch (cause) { + // A connection cut mid-body is a transport failure, not a bad artifact, so it + // keeps its own error rather than being reported as unparseable JSON. + if (!(cause instanceof SyntaxError)) throw cause; // A truncated or half-uploaded artifact served correctly as JSON. Left as a // plain (transient) Error — the host does have the file — but re-thrown with // `describe` so it still carries the bucket key. A bare `res.json()` reject diff --git a/runner/pipeline/docs-catalog.test.mjs b/runner/pipeline/docs-catalog.test.mjs index a3795d84c7..c158a7e40d 100644 --- a/runner/pipeline/docs-catalog.test.mjs +++ b/runner/pipeline/docs-catalog.test.mjs @@ -170,6 +170,18 @@ test("manifest: a truncated body served as application/json names the bucket and assert.equal(error.cause instanceof Error, true); }); +test("manifest: a body stream cut by the network rethrows the transport error, not an unparseable-JSON one", async (t) => { + // The headers arrived as application/json but the connection died mid-body, so + // `res.json()` rejects with the browser's own `TypeError: Failed to fetch`. + const cut = new TypeError("Failed to fetch"); + withFetch(t, () => ({ ...reply({ body: "" }), json: async () => { throw cut; } })); + + const error = await rejection(fetchDocsManifest("net-cut")); + + assert.equal(error, cut); + assert.equal(isDocsResourceMissing(error), false); +}); + test("manifest: a 200 body that is neither HTML nor JSON rejects as non-missing", async (t) => { withFetch(t, () => reply({ contentType: "text/plain", body: "not json at all" }));