{"record":{"id":"b6cd7c16000b3ac0","repo":"antiwork/gumroad","slug":"responsedata-error-b6cd7c","errorCode":null,"errorMessage":"${responseData.error}","messagePattern":"\\$\\{responseData\\.error\\}","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/upsells.ts","lineNumber":64,"sourceCode":"      description,\n      cross_sell: isCrossSell,\n      replace_selected_products: replaceSelectedProducts,\n      universal,\n      product_id: productId,\n      variant_id: variantId ?? undefined,\n      offer_code: offerCode ?? undefined,\n      product_ids: productIds,\n      upsell_variants: upsellVariants.map(({ selectedVariantId, offeredVariantId }) => ({\n        selected_variant_id: selectedVariantId,\n        offered_variant_id: offeredVariantId,\n      })),\n      paused,\n    },\n  });\n  const responseData = typia.assert<\n    { success: true; upsells: Upsell[]; pagination: PaginationProps } | { success: false; error: string }\n  >(await response.json());\n  if (!responseData.success) throw new ResponseError(responseData.error);\n\n  return responseData;\n};\n\nexport const updateUpsell = async (\n  id: string,\n  {\n    name,\n    text,\n    description,\n    isCrossSell,\n    replaceSelectedProducts,\n    universal,\n    productId,\n    variantId,\n    offerCode,\n    productIds,\n    upsellVariants,","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/upsells.ts#L46-L82","documentation":"createUpsell (app/javascript/data/upsells.ts:25-67) POSTs the upsell payload and parses the body unconditionally with typia.assert; when it decodes to success:false it throws ResponseError(responseData.error) carrying the server's own message. This is the checkout dashboard's application-level validation failure path: Rails rejected the upsell (invalid name/text, product_ids containing products that cannot be offered, bad offer_code, upsell_variants referencing variants that don't belong to the products) and explained why in error.","triggerScenarios":"Saving a new upsell in CheckoutDashboard/UpsellsPage.tsx with a blank name or text; selecting offer products that are unpublished, deleted, or belong to another seller; an offer_code amount out of range; upsell_variants pairing variant ids from different products; universal upsell rules violated server-side.","commonSituations":"Product picker state holding ids for products deleted/unpublished after the picker loaded; percentage/cent amounts typed beyond limits; variant dropdowns stale after the product's variants were edited; concurrent editors changing the same catalog.","solutions":["Read e.message — the server's error string names the exact invalid field or rule","Fix the named field (name/text required, valid product/variant ids, in-range offer code) and resubmit","Re-pick the offer products from a fresh picker load so product_ids are current","Validate required fields client-side before POSTing to avoid the round trip","If the error is stale-state (product gone), reload the upsells page and rebuild the selection"],"exampleFix":"// client-side gate mirroring server rules before createUpsell\nconst payloadValid =\n  name.trim() !== \"\" && text.trim() !== \"\" && productId !== \"\" &&\n  productIds.every((id) => availableProductIds.has(id));\nif (!payloadValid) return showError(\"Fill in name, text, and valid products first.\");\nawait createUpsell(payload);","handlingStrategy":"validation","validationCode":"const validUpsellPayload = (p: UpsellPayload) =>\n  p.name.trim() !== \"\" && p.text.trim() !== \"\" && p.productId !== \"\" &&\n  p.productIds.every((id) => availableProductIds.has(id));\nif (!validUpsellPayload(payload)) return showError(\"Fill in name, text, and valid products.\");","typeGuard":"import typia from \"typia\";\nconst isUpsellEnvelope = typia.is<\n  { success: true; upsells: Upsell[]; pagination: PaginationProps } | { success: false; error: string }\n>;","tryCatchPattern":"try {\n  setState(await createUpsell(payload));\n} catch (e) {\n  assertResponseError(e);\n  showError(e.message); // names the exact server-side rule that failed\n}","preventionTips":["Require name/text and re-validate product ids against a freshly loaded picker before POSTing","Range-check offer_code amounts client-side","Show e.message verbatim — the server already explains the refusal"],"tags":["upsells","validation","http","typescript","checkout"],"backgroundTag":"unprocessable-entity-validation","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}