{"record":{"id":"c9ebca0148df7c80","repo":"antiwork/gumroad","slug":"failed-to-unarchive-product","errorCode":null,"errorMessage":"Failed to unarchive product","messagePattern":"Failed to unarchive product","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/product_dashboard.ts","lineNumber":38,"sourceCode":"    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({\n    url: Routes.product_duplicates_path(),\n    method: \"POST\",\n    accept: \"json\",\n    data: { id: permalink },\n  });\n\n  const json = typia.assert<{ success: true } | { success: false; error_message: string }>(await response.json());\n  if (!json.success) throw new ResponseError(json.error_message);\n\n  await pollForProductDuplication(permalink, productName);\n}\n\nasync function pollForProductDuplication(permalink: string, productName: string) {","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/product_dashboard.ts#L20-L56","documentation":"ResponseError('Failed to unarchive product') thrown at product_dashboard.ts:38 when the DELETE to Routes.products_archived_path(permalink) answers { success: false, errors: [] } — the literal is the fallback after `json.errors[0] ||`, so the server refused the unarchive but its errors array came back empty and the true reason never reaches the user.","triggerScenarios":"The unarchive endpoint returns success:false with an empty errors array: an ActiveRecord/validation failure where errors weren't collected, a permissions guard that short-circuits without appending to errors, or the record state making unarchive a no-op that is still marked failed.","commonSituations":"Model refactor renames validations so the controller's hand-picked errors list no longer populates; unarchiving a product whose parent record (file, variant) was purged; concurrent unarchive from two dashboard tabs; every failure collapsing into the same generic string in user reports.","solutions":["Inspect the products/archived/:id DELETE response body in DevTools to confirm errors is empty and capture the status code","Read server logs for the unarchive action to find which guard produced success:false","Patch the endpoint so refusal payloads always carry a non-empty errors array (or a single error string)","Add a server-side test asserting a failure response includes a reason, so the empty-field regression cannot return silently"],"exampleFix":"# server side — populate the refusal reason\n# before\nrender json: { success: false, errors: [] }\n# after\nrender json: { success: false, errors: [product.errors.full_messages.to_sentence.presence || 'Unable to unarchive product'] }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"import { assertResponseError } from '$app/utils/request';\nassertResponseError(e);","tryCatchPattern":"try {\n  await unarchiveProduct(permalink);\n} catch (e) {\n  assertResponseError(e);\n  if (e.message === 'Failed to unarchive product') logEmptyUnarchiveError(permalink); // empty errors[] bug\n  showError(e.message);\n  await refreshArchived();\n}","preventionTips":["The exact fallback string signals an empty server errors array — capture permalink and status for the server team","Keep list state server-authoritative: refetch archived products after every attempt","Add endpoint tests that a failure response always includes a reason"],"tags":["product","unarchive","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"}