{"record":{"id":"f57a806e943848a6","repo":"antiwork/gumroad","slug":"sorry-failed-to-duplicate-productname-jso","errorCode":null,"errorMessage":"Sorry, failed to duplicate '${productName}': ${json.error_message}","messagePattern":"Sorry, failed to duplicate '(.+?)': (.+?)","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/product_dashboard.ts","lineNumber":68,"sourceCode":"  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) {\n  const response = await request({\n    url: Routes.product_duplicate_path(permalink),\n    method: \"GET\",\n    accept: \"json\",\n  });\n\n  const json = typia.assert<{ status: string; error_message?: string }>(await response.json());\n  if (json.status === \"product_duplication_failed\") {\n    const reason = json.error_message\n      ? `Sorry, failed to duplicate '${productName}': ${json.error_message}`\n      : `Sorry, failed to duplicate '${productName}'. Please try again.`;\n    throw new ResponseError(reason);\n  } else if (json.status === \"product_duplicated\") {\n    return { status: json.status };\n  }\n  await new Promise((resolve) => setTimeout(resolve, 2000));\n  return pollForProductDuplication(permalink, productName);\n}\n","sourceCodeStart":50,"sourceCodeEnd":75,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/product_dashboard.ts#L50-L75","documentation":"ResponseError thrown at product_dashboard.ts:68 after pollForProductDuplication GETs Routes.product_duplicate_path(permalink) every 2 seconds and the status comes back 'product_duplication_failed': the asynchronous copy job failed server-side. The message embeds the product name plus json.error_message when present, otherwise the 'Please try again.' fallback. Note the poll loop has no attempt cap — it recurses every 2s indefinitely while status stays 'processing'.","triggerScenarios":"The duplication job enqueued by the product_duplicates endpoint fails and the record's status flips to 'product_duplication_failed': file copy from object storage fails mid-job, a DB constraint trips while cloning records/variants/blobs, the job worker dies after the record was marked in-progress, or the job hits a bug cloning an unusual product shape.","commonSituations":"Large products (many files/variants) timing out in the copy job; S3 file-copy permission or bucket-policy drift in an environment; job queue retries exhausted after a transient DB issue; browser tab left open polling overnight if the job wedges in 'processing'; error_message arriving empty, leaving users with only the fallback wording.","solutions":["Read the thrown reason — when error_message is present it names the actual job failure; then check server logs for the duplication worker for the full backtrace","Retry duplication once (the usual fix for transient file-copy failures), via the same duplicateProduct entry point","Verify object-storage credentials/bucket policies server-side if the message points at file copying","Add a max-attempt cap to the 2s poll loop so a wedged 'processing' job surfaces as a timeout error instead of polling forever (see exampleFix)","If failures cluster for one product, inspect that product's records (orphaned file blobs, oversized content) rather than blaming the job system"],"exampleFix":"// before\nawait new Promise((resolve) => setTimeout(resolve, 2000));\nreturn pollForProductDuplication(permalink, productName);\n\n// after — cap the poll so a wedged job fails loudly\nasync function pollForProductDuplication(permalink: string, productName: string, attempt = 0) {\n  // ... existing status checks ...\n  if (attempt >= 150) throw new ResponseError(`Sorry, duplicating '${productName}' timed out. Please try again.`);\n  await new Promise((resolve) => setTimeout(resolve, 2000));\n  return pollForProductDuplication(permalink, productName, attempt + 1);\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"import { assertResponseError } from '$app/utils/request';\nassertResponseError(e);","tryCatchPattern":"try {\n  await pollForProductDuplication(permalink, productName);\n} catch (e) {\n  assertResponseError(e);\n  const transient = /timed out|try again/i.test(e.message);\n  if (transient && attempt === 0) return duplicateProduct(permalink, productName); // one retry\n  showError(e.message); // includes the server's job failure reason when present\n}","preventionTips":["Wrap pollForProductDuplication with an attempt cap so a wedged 'processing' job can't poll forever","Retry duplication once on transient-sounding messages before surfacing failure","Log permalink + error_message together — job failures are diagnosed server-side by permalink"],"tags":["product","duplicate","background-job","polling"],"backgroundTag":"background-job-failed","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}