From 1fc14a7da2627bcab49e6cff9b49e1b9fb535585 Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:10:10 +0000 Subject: [PATCH] fix(frontend): decode blob error bodies before building error toast Tool POSTs use responseType "blob", so error.response.data reaches handleHttpError as an unread Blob. The toast message was built from that raw Blob before the blob was decoded, so JSON.stringify(Blob) returned "{}", the message was rejected as unhelpful, and every server error fell back to the generic "There was an error processing your request." text. Decode the response body once, up front, and pass the decoded body into extractAxiosErrorMessage. The server explanation now reaches the toast for every tool that posts with responseType blob, which also makes the merge 500 diagnosable. Generated-By: PostHog Desktop Task-Id: 1544e247-e89a-478f-8b54-6fce70390e6a --- .../core/services/httpErrorHandler.test.ts | 54 +++++++++++++++++++ .../src/core/services/httpErrorHandler.ts | 10 ++-- .../src/core/services/httpErrorUtils.ts | 10 +++- 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 frontend/editor/src/core/services/httpErrorHandler.test.ts diff --git a/frontend/editor/src/core/services/httpErrorHandler.test.ts b/frontend/editor/src/core/services/httpErrorHandler.test.ts new file mode 100644 index 0000000000..8228cba831 --- /dev/null +++ b/frontend/editor/src/core/services/httpErrorHandler.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test, vi, beforeEach } from "vitest"; +import { handleHttpError } from "@app/services/httpErrorHandler"; + +const alertMock = vi.hoisted(() => vi.fn()); +vi.mock("@app/components/toast", () => ({ + alert: (options: unknown) => alertMock(options), +})); + +// Reproduces a tool POST: responseType "blob" makes error.response.data a Blob. +// jsdom's Blob has no .text(), so mirror the browser API the decode path uses. +function blobAxiosError(status: number, body: string, contentType: string) { + return { + isAxiosError: true, + config: { url: "/api/v1/general/merge-pdfs" }, + response: { + status, + statusText: "", + data: { type: contentType, text: () => Promise.resolve(body) }, + }, + }; +} + +describe("handleHttpError — blob error bodies", () => { + beforeEach(() => { + alertMock.mockClear(); + }); + + test("shows the decoded server message from a JSON blob body", async () => { + await handleHttpError( + blobAxiosError( + 500, + JSON.stringify({ message: "JPDFium merge failed" }), + "application/json", + ), + ); + + expect(alertMock).toHaveBeenCalledTimes(1); + expect(alertMock.mock.calls[0][0]).toMatchObject({ + title: "Server error", + body: "JPDFium merge failed", + }); + }); + + test("shows the decoded server message from a plain-text blob body", async () => { + await handleHttpError( + blobAxiosError(500, "JPDFium merge failed", "text/plain"), + ); + + expect(alertMock).toHaveBeenCalledTimes(1); + expect(alertMock.mock.calls[0][0]).toMatchObject({ + body: "JPDFium merge failed", + }); + }); +}); diff --git a/frontend/editor/src/core/services/httpErrorHandler.ts b/frontend/editor/src/core/services/httpErrorHandler.ts index 55b0a425f9..81bdd1cbd6 100644 --- a/frontend/editor/src/core/services/httpErrorHandler.ts +++ b/frontend/editor/src/core/services/httpErrorHandler.ts @@ -147,10 +147,9 @@ export async function handleHttpError(error: any): Promise { if (handleSaaSError(error)) return true; - // Compute title/body (friendly) from the error object - const { title, body } = extractAxiosErrorMessage(error); - - // Normalize response data ONCE, reuse for both ID extraction and special-toast matching + // Decode response data ONCE. Tool POSTs use responseType "blob", so + // error.response.data is an unread Blob here. The decoded body drives the + // toast message, file-ID extraction, and special-toast matching alike. const raw = error?.response?.data as any; let normalized: unknown = raw; try { @@ -159,6 +158,9 @@ export async function handleHttpError(error: any): Promise { console.debug("normalizeAxiosErrorData", e); } + // Compute title/body (friendly) from the decoded body, not the raw Blob. + const { title, body } = extractAxiosErrorMessage(error, normalized); + // 1) If server sends structured file IDs for failures, also mark them errored in UI try { const ids = extractErrorFileIds(normalized); diff --git a/frontend/editor/src/core/services/httpErrorUtils.ts b/frontend/editor/src/core/services/httpErrorUtils.ts index b3b54e2664..60ec75bb8f 100644 --- a/frontend/editor/src/core/services/httpErrorUtils.ts +++ b/frontend/editor/src/core/services/httpErrorUtils.ts @@ -25,7 +25,10 @@ function titleForStatus(status?: number): string { return "Request failed"; } -export function extractAxiosErrorMessage(error: any): { +export function extractAxiosErrorMessage( + error: any, + decodedData?: unknown, +): { title: string; body: string; } { @@ -33,7 +36,10 @@ export function extractAxiosErrorMessage(error: any): { const status = error.response?.status; const _statusText = error.response?.statusText || ""; let parsed: any = undefined; - const raw = error.response?.data; + // Tool POSTs use responseType "blob", so error.response.data is an unread + // Blob. Prefer the decoded body when the caller supplies it, otherwise the + // message falls back to the generic text and the server reason is lost. + const raw = decodedData !== undefined ? decodedData : error.response?.data; if (typeof raw === "string") { try { parsed = JSON.parse(raw);