From 7a024cf68ee64b13ad6d7694b19efb252c0ce699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 22 Aug 2026 11:04:16 -0400 Subject: [PATCH] fix(studio): name the cause when a render request fails (#3424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The render POST's catch took no binding, so the exception was discarded and every transport failure produced one sentence: "Could not reach render server. Use `hyperframes render` from the CLI instead." A dead server, a DNS failure, an aborted request and a server that died mid-render are all indistinguishable under that string — and it is not only a UI message, it is what travels into the feedback report. Three separate field reports carried it verbatim, one of them describing a render that fails every single time. A guaranteed reproduction that tells us nothing is worse than an intermittent one that does. Bind the error and append it. The CLI guidance stays, since it is still the right next step for the user; it just no longer stands alone. Regression test asserts both halves: the cause appears, and the guidance survives. It fails on the unfixed code with `expected 'Could not reach render server. Use `h…' to contain 'Failed to fetch'`. --- .../src/components/renders/useRenderQueue.ts | 11 ++++- .../useRenderQueueTransportError.test.tsx | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 packages/studio/src/components/renders/useRenderQueueTransportError.test.tsx diff --git a/packages/studio/src/components/renders/useRenderQueue.ts b/packages/studio/src/components/renders/useRenderQueue.ts index d4af7c61c5..44a0775a89 100644 --- a/packages/studio/src/components/renders/useRenderQueue.ts +++ b/packages/studio/src/components/renders/useRenderQueue.ts @@ -250,12 +250,19 @@ export function useRenderQueue(projectId: string | null) { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); - } catch { + } catch (err) { + // The cause used to be discarded. Every failure — a dead server, an + // aborted request, a DNS error, a mid-render crash — surfaced as the + // same sentence, and this string is what travels into the feedback + // report too, so field reports of a render that fails *every time* + // still carried nothing to act on. Keep the CLI guidance, name the + // cause after it. + const cause = err instanceof Error ? err.message : String(err); const failedJob: RenderJob = { id: generateId(), status: "failed", progress: 0, - error: "Could not reach render server. Use `hyperframes render` from the CLI instead.", + error: `Could not reach render server: ${cause}. Use \`hyperframes render\` from the CLI instead.`, filename: "Export failed", createdAt: startTime, }; diff --git a/packages/studio/src/components/renders/useRenderQueueTransportError.test.tsx b/packages/studio/src/components/renders/useRenderQueueTransportError.test.tsx new file mode 100644 index 0000000000..cb5cb98df5 --- /dev/null +++ b/packages/studio/src/components/renders/useRenderQueueTransportError.test.tsx @@ -0,0 +1,49 @@ +// @vitest-environment happy-dom + +// The render POST's catch used to discard its error, so a dead server, a DNS +// failure and a mid-render crash all produced one sentence. That string is also +// what travels into the feedback report, so three field reports — one of them a +// render that failed EVERY time — arrived with nothing to act on. + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mountRenderQueue } from "./renderQueueTestHarness"; + +vi.mock("./useFfmpegStatus", async (importOriginal) => ({ + ...(await importOriginal()), + useFfmpegStatus: () => ({ status: { ok: true }, checking: false, recheck: vi.fn() }), +})); +vi.mock("../../telemetry/events", () => ({ trackStudioRenderStart: vi.fn() })); + +const { useRenderQueue } = await import("./useRenderQueue"); + +let queue: ReturnType | null = null; + +beforeEach(() => { + vi.stubGlobal( + "fetch", + vi.fn(() => Promise.reject(new TypeError("Failed to fetch"))), + ); +}); + +afterEach(() => { + queue?.unmount(); + queue = null; + document.body.innerHTML = ""; + vi.unstubAllGlobals(); +}); + +describe("useRenderQueue transport failure", () => { + it("names the cause instead of collapsing every failure into one sentence", async () => { + queue = mountRenderQueue(useRenderQueue); + const { act } = await import("react"); + await act(async () => { + await queue?.api().startRender({}); + }); + + const job = queue.api().jobs.at(-1); + expect(job?.status).toBe("failed"); + expect(job?.error).toContain("Failed to fetch"); + // The CLI guidance still earns its place; it just no longer stands alone. + expect(job?.error).toContain("hyperframes render"); + }); +});