From a757aab562767414f29a123d723bb4b073408d43 Mon Sep 17 00:00:00 2001 From: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:14:31 -0400 Subject: [PATCH 1/3] fix(scripts): bound prebuild sync fetches with a timeout Add fetchWithTimeout (AbortController, default 30s, overridable via DOCS_SYNC_FETCH_TIMEOUT_MS) and use it for the sync-hive-docs, sync-sibling-docs and update-meeting-recordings fetches so a stalled remote fails fast instead of hanging until the job timeout. Closes #394 Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- CONTRIBUTING.md | 1 + changelog.d/fixed-sync-fetch-timeout.md | 1 + scripts/fetch-with-timeout.test.ts | 71 +++++++++++++++++++++++++ scripts/fetch-with-timeout.ts | 44 +++++++++++++++ scripts/sync-hive-docs.ts | 3 +- scripts/sync-sibling-docs.ts | 5 +- scripts/update-meeting-recordings.ts | 3 +- 7 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixed-sync-fetch-timeout.md create mode 100644 scripts/fetch-with-timeout.test.ts create mode 100644 scripts/fetch-with-timeout.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ee32760..cebc6309 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,7 @@ User-visible changes also need a changelog fragment under `changelog.d/` offline, set `DOCS_SYNC_OPTIONAL=1` (for example `DOCS_SYNC_OPTIONAL=1 npm run build`) to keep the committed content instead. CI and Netlify always use the strict behavior. +Sync fetches time out after 30s by default; set `DOCS_SYNC_FETCH_TIMEOUT_MS` (milliseconds) to change that. ## Code review requirements diff --git a/changelog.d/fixed-sync-fetch-timeout.md b/changelog.d/fixed-sync-fetch-timeout.md new file mode 100644 index 00000000..09967564 --- /dev/null +++ b/changelog.d/fixed-sync-fetch-timeout.md @@ -0,0 +1 @@ +- Prebuild content sync steps now fail fast with a clear "timed out" error instead of hanging the build when a remote host stalls; tune with `DOCS_SYNC_FETCH_TIMEOUT_MS` (default 30000). diff --git a/scripts/fetch-with-timeout.test.ts b/scripts/fetch-with-timeout.test.ts new file mode 100644 index 00000000..7c4a101f --- /dev/null +++ b/scripts/fetch-with-timeout.test.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { fetchWithTimeout, parseTimeoutMs } from "./fetch-with-timeout"; + +// A fetch that never resolves but rejects when its signal aborts. +const hanging = vi.fn( + (_url: string, init?: RequestInit) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => + reject(new DOMException("aborted", "AbortError")) + ); + }) +) as unknown as typeof fetch; + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("fetchWithTimeout", () => { + it("resolves when the fetch resolves", async () => { + const res = new Response("ok"); + const fake = vi.fn(async () => res) as unknown as typeof fetch; + await expect( + fetchWithTimeout("https://x.test/a", {}, 1000, fake) + ).resolves.toBe(res); + }); + + it("passes an AbortSignal to fetch", async () => { + const fake = vi.fn(async () => new Response("ok")); + await fetchWithTimeout( + "https://x.test/a", + {}, + 1000, + fake as unknown as typeof fetch + ); + const init = (fake.mock.calls[0] as unknown[])[1] as RequestInit; + expect(init.signal).toBeInstanceOf(AbortSignal); + }); + + it("rejects with a timeout message naming the URL", async () => { + vi.useFakeTimers(); + const p = fetchWithTimeout("https://x.test/slow", {}, 5000, hanging); + const assertion = expect(p).rejects.toThrow( + /timed out after 5s fetching https:\/\/x\.test\/slow/ + ); + await vi.advanceTimersByTimeAsync(5000); + await assertion; + }); + + it("does not mask non-timeout errors", async () => { + const fake = vi.fn(async () => { + throw new Error("boom"); + }) as unknown as typeof fetch; + await expect( + fetchWithTimeout("https://x.test/a", {}, 1000, fake) + ).rejects.toThrow("boom"); + }); +}); + +describe("parseTimeoutMs", () => { + it("falls back to the default for missing or invalid values", () => { + expect(parseTimeoutMs({})).toBe(30000); + expect(parseTimeoutMs({ DOCS_SYNC_FETCH_TIMEOUT_MS: "abc" })).toBe(30000); + expect(parseTimeoutMs({ DOCS_SYNC_FETCH_TIMEOUT_MS: "0" })).toBe(30000); + expect(parseTimeoutMs({ DOCS_SYNC_FETCH_TIMEOUT_MS: "-5" })).toBe(30000); + expect(parseTimeoutMs({ DOCS_SYNC_FETCH_TIMEOUT_MS: "1.5" })).toBe(30000); + }); + + it("accepts a positive integer", () => { + expect(parseTimeoutMs({ DOCS_SYNC_FETCH_TIMEOUT_MS: "1500" })).toBe(1500); + }); +}); diff --git a/scripts/fetch-with-timeout.ts b/scripts/fetch-with-timeout.ts new file mode 100644 index 00000000..36d63720 --- /dev/null +++ b/scripts/fetch-with-timeout.ts @@ -0,0 +1,44 @@ +/** + * fetch() with a deadline for the prebuild sync scripts, so a stalled remote + * fails the step quickly instead of hanging the build until the job timeout. + * Override the default with DOCS_SYNC_FETCH_TIMEOUT_MS (positive integer, ms). + */ +export type SyncEnv = Record; + +const FALLBACK_TIMEOUT_MS = 30000; + +export function parseTimeoutMs(env: SyncEnv = process.env): number { + const raw = env.DOCS_SYNC_FETCH_TIMEOUT_MS?.trim(); + if (!raw || !/^\d+$/.test(raw)) return FALLBACK_TIMEOUT_MS; + const value = Number(raw); + return Number.isSafeInteger(value) && value > 0 ? value : FALLBACK_TIMEOUT_MS; +} + +export const DEFAULT_SYNC_FETCH_TIMEOUT_MS = parseTimeoutMs(); + +export async function fetchWithTimeout( + url: string, + init: RequestInit = {}, + timeoutMs: number = DEFAULT_SYNC_FETCH_TIMEOUT_MS, + fetchImpl: typeof fetch = fetch +): Promise { + const controller = new AbortController(); + let timedOut = false; + const timer = setTimeout(() => { + timedOut = true; + controller.abort(); + }, timeoutMs); + try { + return await fetchImpl(url, { ...init, signal: controller.signal }); + } catch (error) { + if (timedOut) { + throw new Error( + `timed out after ${timeoutMs / 1000}s fetching ${url}`, + { cause: error } + ); + } + throw error; + } finally { + clearTimeout(timer); + } +} diff --git a/scripts/sync-hive-docs.ts b/scripts/sync-hive-docs.ts index dc508b0e..9113fc83 100644 --- a/scripts/sync-hive-docs.ts +++ b/scripts/sync-hive-docs.ts @@ -4,6 +4,7 @@ import { rewriteLinkTarget as rewriteSharedLinkTarget, rewriteMarkdownLinks, } from "./link-rewrite"; +import { fetchWithTimeout } from "./fetch-with-timeout"; import { normalizeFences } from "./markdown-fences"; import { scrubLegacyBranding } from "./scrub-legacy-branding"; @@ -217,7 +218,7 @@ function applyLocalHiveOverlays(content: string, source: string): string { } async function fetchText(url: string): Promise { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error( `failed to fetch ${url}: ${response.status} ${response.statusText}` diff --git a/scripts/sync-sibling-docs.ts b/scripts/sync-sibling-docs.ts index d36e95b3..1609a246 100644 --- a/scripts/sync-sibling-docs.ts +++ b/scripts/sync-sibling-docs.ts @@ -15,6 +15,7 @@ */ import fs from "fs"; import path from "path"; +import { fetchWithTimeout } from "./fetch-with-timeout"; import { rewriteMarkdownLinks } from "./link-rewrite"; import { normalizeFences } from "./markdown-fences"; import { convertMdxToMarkdown } from "./mdx-to-markdown"; @@ -441,13 +442,13 @@ function rawUrl(r: RepoRef, source: string): string { } async function fetchFile(r: RepoRef, source: string): Promise { - const res = await fetch(rawUrl(r, source)); + const res = await fetchWithTimeout(rawUrl(r, source)); if (!res.ok) return null; return res.text(); } async function fetchBinary(r: RepoRef, source: string): Promise { - const res = await fetch(rawUrl(r, source)); + const res = await fetchWithTimeout(rawUrl(r, source)); if (!res.ok) return null; return Buffer.from(await res.arrayBuffer()); } diff --git a/scripts/update-meeting-recordings.ts b/scripts/update-meeting-recordings.ts index 889a4aeb..f1e7723d 100644 --- a/scripts/update-meeting-recordings.ts +++ b/scripts/update-meeting-recordings.ts @@ -1,5 +1,6 @@ import fs from "fs"; import path from "path"; +import { fetchWithTimeout } from "./fetch-with-timeout"; const DATA_PATH = path.join(process.cwd(), "data", "meeting-recordings.json"); const FEED_URL = @@ -123,7 +124,7 @@ async function main() { let fetched: Recording[] = []; try { - const response = await fetch(FEED_URL); + const response = await fetchWithTimeout(FEED_URL); if (!response.ok) throw new Error(`YouTube feed returned ${response.status}`); fetched = parseFeed(await response.text()); From b142c9406fe4fc65e1ea4f8465afd25b94e41a92 Mon Sep 17 00:00:00 2001 From: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:34:38 -0400 Subject: [PATCH 2/3] fix(scripts): format fetch-with-timeout and assert fetch receives an abort signal Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- scripts/fetch-with-timeout.ts | 7 +++---- scripts/update-meeting-recordings.inprocess.test.ts | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/fetch-with-timeout.ts b/scripts/fetch-with-timeout.ts index 36d63720..27fc27e5 100644 --- a/scripts/fetch-with-timeout.ts +++ b/scripts/fetch-with-timeout.ts @@ -32,10 +32,9 @@ export async function fetchWithTimeout( return await fetchImpl(url, { ...init, signal: controller.signal }); } catch (error) { if (timedOut) { - throw new Error( - `timed out after ${timeoutMs / 1000}s fetching ${url}`, - { cause: error } - ); + throw new Error(`timed out after ${timeoutMs / 1000}s fetching ${url}`, { + cause: error, + }); } throw error; } finally { diff --git a/scripts/update-meeting-recordings.inprocess.test.ts b/scripts/update-meeting-recordings.inprocess.test.ts index 412ec642..d4fc8242 100644 --- a/scripts/update-meeting-recordings.inprocess.test.ts +++ b/scripts/update-meeting-recordings.inprocess.test.ts @@ -115,7 +115,10 @@ describe("update-meeting-recordings (in-process)", () => { await runInProcess(); - expect(fetch).toHaveBeenCalledWith(FEED_URL); + expect(fetch).toHaveBeenCalledWith( + FEED_URL, + expect.objectContaining({ signal: expect.any(AbortSignal) }) + ); expect(logSpy).toHaveBeenCalledWith("meeting recordings: 2"); const data = readData(); expect(data.source).toBe(FEED_URL); From 13ad83bf7d7d5219e9a0a18f064dae0488d2d165 Mon Sep 17 00:00:00 2001 From: ci-maintainer <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:37:14 -0400 Subject: [PATCH 3/3] fix(scripts): keep the sync fetch deadline through body consumption Address review findings on #395: - Read the response body inside the helper's deadline so a remote that returns headers and then stalls mid-body is bounded too. The body read is raced against the abort deadline, so it holds even for streams that ignore the signal. Replace fetchWithTimeout with fetchTextWithTimeout / fetchBytesWithTimeout returning { ok, status, statusText, body } and update the sync-hive-docs, sync-sibling-docs and update-meeting-recordings call sites. Add never-closing-body tests for both helpers. - Update update-meeting-recordings.inprocess.test.ts to expect the AbortSignal init argument. - Import SyncEnv from run-sync-step instead of re-declaring it. - Run prettier on the helper; wrap the CONTRIBUTING.md line to match. Hive-Fix-Attempt: 1/6 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: ci-maintainer <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- CONTRIBUTING.md | 3 +- scripts/fetch-with-timeout.test.ts | 95 ++++++++++++++++++++++++---- scripts/fetch-with-timeout.ts | 64 +++++++++++++++++-- scripts/sync-hive-docs.ts | 6 +- scripts/sync-sibling-docs.ts | 13 ++-- scripts/update-meeting-recordings.ts | 6 +- 6 files changed, 157 insertions(+), 30 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cebc6309..cc8510de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,8 @@ User-visible changes also need a changelog fragment under `changelog.d/` offline, set `DOCS_SYNC_OPTIONAL=1` (for example `DOCS_SYNC_OPTIONAL=1 npm run build`) to keep the committed content instead. CI and Netlify always use the strict behavior. -Sync fetches time out after 30s by default; set `DOCS_SYNC_FETCH_TIMEOUT_MS` (milliseconds) to change that. +Sync fetches time out after 30s by default; set `DOCS_SYNC_FETCH_TIMEOUT_MS` +(milliseconds) to change that. ## Code review requirements diff --git a/scripts/fetch-with-timeout.test.ts b/scripts/fetch-with-timeout.test.ts index 7c4a101f..1b5ae19a 100644 --- a/scripts/fetch-with-timeout.test.ts +++ b/scripts/fetch-with-timeout.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { fetchWithTimeout, parseTimeoutMs } from "./fetch-with-timeout"; +import { + fetchBytesWithTimeout, + fetchTextWithTimeout, + parseTimeoutMs, +} from "./fetch-with-timeout"; // A fetch that never resolves but rejects when its signal aborts. const hanging = vi.fn( @@ -11,22 +15,49 @@ const hanging = vi.fn( }) ) as unknown as typeof fetch; +// A fetch that answers 200 with headers right away but whose body stream +// never closes and ignores the abort signal (the "never completes the +// response" case from #394). +const stalledBody = vi.fn( + async () => + new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("partial")); + }, + }), + { status: 200 } + ) +) as unknown as typeof fetch; + afterEach(() => { vi.useRealTimers(); }); -describe("fetchWithTimeout", () => { - it("resolves when the fetch resolves", async () => { - const res = new Response("ok"); - const fake = vi.fn(async () => res) as unknown as typeof fetch; +describe("fetchTextWithTimeout", () => { + it("returns status fields and the body text", async () => { + const fake = vi.fn( + async () => new Response("ok", { status: 200, statusText: "OK" }) + ) as unknown as typeof fetch; await expect( - fetchWithTimeout("https://x.test/a", {}, 1000, fake) - ).resolves.toBe(res); + fetchTextWithTimeout("https://x.test/a", {}, 1000, fake) + ).resolves.toEqual({ ok: true, status: 200, statusText: "OK", body: "ok" }); + }); + + it("reports non-2xx responses without throwing", async () => { + const fake = vi.fn( + async () => + new Response("missing", { status: 404, statusText: "Not Found" }) + ) as unknown as typeof fetch; + const res = await fetchTextWithTimeout("https://x.test/a", {}, 1000, fake); + expect(res.ok).toBe(false); + expect(res.status).toBe(404); + expect(res.statusText).toBe("Not Found"); }); it("passes an AbortSignal to fetch", async () => { const fake = vi.fn(async () => new Response("ok")); - await fetchWithTimeout( + await fetchTextWithTimeout( "https://x.test/a", {}, 1000, @@ -36,9 +67,9 @@ describe("fetchWithTimeout", () => { expect(init.signal).toBeInstanceOf(AbortSignal); }); - it("rejects with a timeout message naming the URL", async () => { + it("rejects with a timeout message naming the URL when headers never arrive", async () => { vi.useFakeTimers(); - const p = fetchWithTimeout("https://x.test/slow", {}, 5000, hanging); + const p = fetchTextWithTimeout("https://x.test/slow", {}, 5000, hanging); const assertion = expect(p).rejects.toThrow( /timed out after 5s fetching https:\/\/x\.test\/slow/ ); @@ -46,16 +77,58 @@ describe("fetchWithTimeout", () => { await assertion; }); + it("times out when the body stalls after headers were received", async () => { + vi.useFakeTimers(); + const p = fetchTextWithTimeout( + "https://x.test/stall", + {}, + 5000, + stalledBody + ); + const assertion = expect(p).rejects.toThrow( + /timed out after 5s fetching https:\/\/x\.test\/stall/ + ); + await vi.advanceTimersByTimeAsync(5000); + await assertion; + }); + it("does not mask non-timeout errors", async () => { const fake = vi.fn(async () => { throw new Error("boom"); }) as unknown as typeof fetch; await expect( - fetchWithTimeout("https://x.test/a", {}, 1000, fake) + fetchTextWithTimeout("https://x.test/a", {}, 1000, fake) ).rejects.toThrow("boom"); }); }); +describe("fetchBytesWithTimeout", () => { + it("returns the body as a Buffer", async () => { + const fake = vi.fn( + async () => new Response(new Uint8Array([1, 2, 3])) + ) as unknown as typeof fetch; + const res = await fetchBytesWithTimeout("https://x.test/b", {}, 1000, fake); + expect(res.ok).toBe(true); + expect(Buffer.isBuffer(res.body)).toBe(true); + expect([...res.body]).toEqual([1, 2, 3]); + }); + + it("times out when the body stalls after headers were received", async () => { + vi.useFakeTimers(); + const p = fetchBytesWithTimeout( + "https://x.test/stall", + {}, + 5000, + stalledBody + ); + const assertion = expect(p).rejects.toThrow( + /timed out after 5s fetching https:\/\/x\.test\/stall/ + ); + await vi.advanceTimersByTimeAsync(5000); + await assertion; + }); +}); + describe("parseTimeoutMs", () => { it("falls back to the default for missing or invalid values", () => { expect(parseTimeoutMs({})).toBe(30000); diff --git a/scripts/fetch-with-timeout.ts b/scripts/fetch-with-timeout.ts index 27fc27e5..88395f40 100644 --- a/scripts/fetch-with-timeout.ts +++ b/scripts/fetch-with-timeout.ts @@ -1,9 +1,11 @@ /** * fetch() with a deadline for the prebuild sync scripts, so a stalled remote * fails the step quickly instead of hanging the build until the job timeout. + * The deadline covers the whole exchange, including reading the body, so a + * remote that sends headers and then stalls mid-body is bounded too. * Override the default with DOCS_SYNC_FETCH_TIMEOUT_MS (positive integer, ms). */ -export type SyncEnv = Record; +import type { SyncEnv } from "./run-sync-step"; const FALLBACK_TIMEOUT_MS = 30000; @@ -16,22 +18,50 @@ export function parseTimeoutMs(env: SyncEnv = process.env): number { export const DEFAULT_SYNC_FETCH_TIMEOUT_MS = parseTimeoutMs(); -export async function fetchWithTimeout( +export interface TimedResponse { + ok: boolean; + status: number; + statusText: string; + body: T; +} + +async function fetchWithDeadline( url: string, - init: RequestInit = {}, - timeoutMs: number = DEFAULT_SYNC_FETCH_TIMEOUT_MS, - fetchImpl: typeof fetch = fetch -): Promise { + init: RequestInit, + timeoutMs: number, + fetchImpl: typeof fetch, + read: (response: Response) => Promise +): Promise> { const controller = new AbortController(); let timedOut = false; + let expire: () => void = () => {}; + // Raced against both the fetch and the body read: the body stream of a + // fetch stub may ignore the abort signal, and the deadline must hold anyway. + const deadline = new Promise((_resolve, reject) => { + expire = () => reject(controller.signal.reason); + }); + deadline.catch(() => {}); const timer = setTimeout(() => { timedOut = true; controller.abort(); + expire(); }, timeoutMs); + let response: Response | undefined; try { - return await fetchImpl(url, { ...init, signal: controller.signal }); + response = await Promise.race([ + fetchImpl(url, { ...init, signal: controller.signal }), + deadline, + ]); + const body = await Promise.race([read(response), deadline]); + return { + ok: response.ok, + status: response.status, + statusText: response.statusText, + body, + }; } catch (error) { if (timedOut) { + response?.body?.cancel().catch(() => {}); throw new Error(`timed out after ${timeoutMs / 1000}s fetching ${url}`, { cause: error, }); @@ -41,3 +71,23 @@ export async function fetchWithTimeout( clearTimeout(timer); } } + +export function fetchTextWithTimeout( + url: string, + init: RequestInit = {}, + timeoutMs: number = DEFAULT_SYNC_FETCH_TIMEOUT_MS, + fetchImpl: typeof fetch = fetch +): Promise> { + return fetchWithDeadline(url, init, timeoutMs, fetchImpl, r => r.text()); +} + +export function fetchBytesWithTimeout( + url: string, + init: RequestInit = {}, + timeoutMs: number = DEFAULT_SYNC_FETCH_TIMEOUT_MS, + fetchImpl: typeof fetch = fetch +): Promise> { + return fetchWithDeadline(url, init, timeoutMs, fetchImpl, async r => + Buffer.from(await r.arrayBuffer()) + ); +} diff --git a/scripts/sync-hive-docs.ts b/scripts/sync-hive-docs.ts index 9113fc83..ee582fbf 100644 --- a/scripts/sync-hive-docs.ts +++ b/scripts/sync-hive-docs.ts @@ -4,7 +4,7 @@ import { rewriteLinkTarget as rewriteSharedLinkTarget, rewriteMarkdownLinks, } from "./link-rewrite"; -import { fetchWithTimeout } from "./fetch-with-timeout"; +import { fetchTextWithTimeout } from "./fetch-with-timeout"; import { normalizeFences } from "./markdown-fences"; import { scrubLegacyBranding } from "./scrub-legacy-branding"; @@ -218,13 +218,13 @@ function applyLocalHiveOverlays(content: string, source: string): string { } async function fetchText(url: string): Promise { - const response = await fetchWithTimeout(url); + const response = await fetchTextWithTimeout(url); if (!response.ok) { throw new Error( `failed to fetch ${url}: ${response.status} ${response.statusText}` ); } - return response.text(); + return response.body; } /** Resolve a sync target under docsRoot, refusing anything that escapes it. */ diff --git a/scripts/sync-sibling-docs.ts b/scripts/sync-sibling-docs.ts index 1609a246..5dacc22a 100644 --- a/scripts/sync-sibling-docs.ts +++ b/scripts/sync-sibling-docs.ts @@ -15,7 +15,10 @@ */ import fs from "fs"; import path from "path"; -import { fetchWithTimeout } from "./fetch-with-timeout"; +import { + fetchBytesWithTimeout, + fetchTextWithTimeout, +} from "./fetch-with-timeout"; import { rewriteMarkdownLinks } from "./link-rewrite"; import { normalizeFences } from "./markdown-fences"; import { convertMdxToMarkdown } from "./mdx-to-markdown"; @@ -442,15 +445,15 @@ function rawUrl(r: RepoRef, source: string): string { } async function fetchFile(r: RepoRef, source: string): Promise { - const res = await fetchWithTimeout(rawUrl(r, source)); + const res = await fetchTextWithTimeout(rawUrl(r, source)); if (!res.ok) return null; - return res.text(); + return res.body; } async function fetchBinary(r: RepoRef, source: string): Promise { - const res = await fetchWithTimeout(rawUrl(r, source)); + const res = await fetchBytesWithTimeout(rawUrl(r, source)); if (!res.ok) return null; - return Buffer.from(await res.arrayBuffer()); + return res.body; } /** diff --git a/scripts/update-meeting-recordings.ts b/scripts/update-meeting-recordings.ts index f1e7723d..8ec2db8b 100644 --- a/scripts/update-meeting-recordings.ts +++ b/scripts/update-meeting-recordings.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import { fetchWithTimeout } from "./fetch-with-timeout"; +import { fetchTextWithTimeout } from "./fetch-with-timeout"; const DATA_PATH = path.join(process.cwd(), "data", "meeting-recordings.json"); const FEED_URL = @@ -124,10 +124,10 @@ async function main() { let fetched: Recording[] = []; try { - const response = await fetchWithTimeout(FEED_URL); + const response = await fetchTextWithTimeout(FEED_URL); if (!response.ok) throw new Error(`YouTube feed returned ${response.status}`); - fetched = parseFeed(await response.text()); + fetched = parseFeed(response.body); } catch (error) { console.warn( `meeting recordings feed unavailable; keeping committed data: ${error instanceof Error ? error.message : String(error)}`