{"record":{"id":"c78b872a90cb9cb7","repo":"antiwork/gumroad","slug":"failed-to-archive-product","errorCode":null,"errorMessage":"Failed to archive product","messagePattern":"Failed to archive product","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/product_dashboard.ts","lineNumber":25,"sourceCode":"    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\",\n    accept: \"json\",\n  });\n\n  const json = typia.assert<{ success: true; archived_products_count: number } | { success: false; errors: string[] }>(\n    await response.json(),\n  );\n  if (!json.success) throw new ResponseError(json.errors[0] || \"Failed to unarchive product\");\n  return json.archived_products_count;\n}\n\nexport async function duplicateProduct(permalink: string, productName: string) {\n  const response = await request({","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/product_dashboard.ts#L7-L43","documentation":"ResponseError('Failed to archive product') thrown at product_dashboard.ts:25 when the POST to Routes.products_archived_index_path(id: permalink) answers { success: false } with an empty or missing `error` field — the literal string is the fallback after `json.error ||`. So the server did refuse the archive, but its payload omitted the reason, and the user sees the generic fallback.","triggerScenarios":"The archive endpoint returns success:false with error: '' (or the key absent): server-side guard rejected archiving (product state, permissions, dependent records) while the controller failed to populate the error field.","commonSituations":"Backend refactor changes the refusal payload from {error: '...'} to {errors: [...]} or {message: '...'} leaving `error` undefined; archiving a product already archived; archiving products owned by another account from a shared dashboard; the empty-field case making support tickets hard to diagnose because every user report says only 'Failed to archive product'.","solutions":["Reproduce with DevTools open and inspect the products/archived POST response body to see the real (empty-field) payload and status","Check server logs for the archive guard that returned success:false — the reason lives there, not in the response","Fix the endpoint to always populate `error` when returning success:false (see exampleFix) so clients can surface the true cause","If the payload shape was renamed, align the typia assert on the actual envelope to catch drift via TypeError instead of silent fallback"],"exampleFix":"# app/controllers/products/archived_controller.rb (server side)\n# before\nrender json: { success: false } # guard failed, no reason\n# after\nrender json: { success: false, error: 'Product cannot be archived while linked to an active bundle' }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"import { assertResponseError } from '$app/utils/request';\nassertResponseError(e);","tryCatchPattern":"try {\n  await archiveProduct(permalink);\n} catch (e) {\n  assertResponseError(e);\n  // generic fallback text means the server sent an empty `error` — check logs, then resync\n  if (e.message === 'Failed to archive product') logEmptyArchiveError(permalink);\n  showError(e.message);\n}","preventionTips":["Treat the literal 'Failed to archive product' string as a server payload bug and file it — the refusal reason was dropped server-side","Assert on the response envelope in endpoint tests so success:false with an empty error fails CI","Resync the archived list after any archive attempt"],"tags":["product","archive","server-validation","empty-error-field"],"backgroundTag":"json-api-error-envelope","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}