{"record":{"id":"980048addf6e4d33","repo":"antiwork/gumroad","slug":"json-message","errorCode":null,"errorMessage":"json.message","messagePattern":"json\\.message","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/product_dashboard.ts","lineNumber":13,"sourceCode":"import typia from \"typia\";\n\nimport { ResponseError, request } from \"$app/utils/request\";\n\nexport async function deleteProduct(permalink: string) {\n  const response = await request({\n    method: \"DELETE\",\n    url: Routes.link_path(permalink),\n    accept: \"json\",\n  });\n\n  const json = typia.assert<{ success: true } | { success: false; message: string }>(await response.json());\n  if (!json.success) throw new ResponseError(json.message);\n}\n\nexport async function archiveProduct(permalink: string) {\n  const response = await request({\n    url: Routes.products_archived_index_path(),\n    method: \"POST\",\n    accept: \"json\",\n    data: { id: permalink },\n  });\n\n  const json = typia.assert<{ success: true } | { success: false; error: string }>(await response.json());\n  if (!json.success) throw new ResponseError(json.error || \"Failed to archive product\");\n}\n\nexport async function unarchiveProduct(permalink: string) {\n  const response = await request({\n    url: Routes.products_archived_path(permalink),\n    method: \"DELETE\",","sourceCodeStart":1,"sourceCodeEnd":31,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/product_dashboard.ts#L1-L31","documentation":"ResponseError thrown at product_dashboard.ts:13 whose message is the server's own json.message: the DELETE to Routes.link_path(permalink) answered with a JSON body shaped { success: false, message }, and deleteProduct forwards that message verbatim. Note this function never checks response.ok — it trusts the endpoint to always answer inside the {success:true}|{success:false,message} envelope (any 5xx/network failure already became a ResponseError inside request()).","triggerScenarios":"The server refuses product deletion with success:false — typical guards: the product is referenced by something that must be removed first (active bundle/subscription/upsell links), the caller lacks permission for that product, or the permalink no longer exists. Any such refusal arrives as this error with the controller's message text.","commonSituations":"Dashboard 'Delete' clicked twice quickly (second call finds an already-deleted product); deleting a product still attached to an active bundle or post; a staff account deleting a product it does not own; Rails i18n or flash-based message text leaking into the JSON message field making the UI show raw translation keys.","solutions":["Read the thrown message — it is the server's stated reason for refusing deletion, not a generic string","If the message references dependent resources (bundles, posts), detach or delete those first and retry","Confirm the acting user owns the product (permalink belongs to their account)","Guard double-submits in the UI: disable the delete button until the request settles"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"import { assertResponseError } from '$app/utils/request';\nassertResponseError(e); // ResponseError here always carries the server's message","tryCatchPattern":"try {\n  await deleteProduct(permalink);\n} catch (e) {\n  assertResponseError(e);\n  showError(e.message); // this is the server's refusal reason — show it verbatim\n  await refreshProducts(); // resync list (the product may already be gone)\n}","preventionTips":["Show e.message to the user — it is the server's specific reason, not boilerplate","Disable the delete affordance while the DELETE is in flight","Refresh the dashboard list after failure; double-deletes produce confusing refusals"],"tags":["product","dashboard","server-validation","delete"],"backgroundTag":"json-api-error-envelope","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}