{"record":{"id":"de12491ac70c293e","repo":"antiwork/gumroad","slug":"response-message","errorCode":null,"errorMessage":"${response.message}","messagePattern":"\\$\\{response\\.message\\}","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/customers.ts","lineNumber":407,"sourceCode":"    .then((response) => {\n      if (!response.ok) throw new ResponseError();\n      return response.json();\n    })\n    .then((json) => typia.assert<Charge[]>(json));\n\nexport const refund = (purchaseId: string, amount: number) =>\n  request({\n    method: \"PUT\",\n    accept: \"json\",\n    url: Routes.refund_purchase_path(purchaseId, { amount }),\n  })\n    .then((response) => {\n      if (!response.ok) throw new ResponseError();\n      return response.json();\n    })\n    .then((json) => typia.assert<{ success: true } | { success: false; message: string }>(json))\n    .then((response) => {\n      if (!response.success) throw new ResponseError(response.message);\n    });\n\nexport const revokeAccess = (purchaseId: string) =>\n  request({\n    method: \"PUT\",\n    accept: \"json\",\n    url: Routes.revoke_access_purchase_path(purchaseId),\n  }).then((response) => {\n    if (!response.ok) throw new ResponseError();\n  });\n\nexport const undoRevokeAccess = (purchaseId: string) =>\n  request({\n    method: \"PUT\",\n    accept: \"json\",\n    url: Routes.undo_revoke_access_purchase_path(purchaseId),\n  }).then((response) => {\n    if (!response.ok) throw new ResponseError();","sourceCodeStart":389,"sourceCodeEnd":425,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/customers.ts#L389-L425","documentation":"The only throw in this function whose message is the server's own text: refund() received HTTP ok, typia-asserted the body as { success: false; message: string } at line 405, and rethrows ResponseError(response.message). This is the endpoint saying 'valid request, but no refund' — the Charge type declared just above (chargedback, paypal_refund_expired) names the canonical reasons. It never produces 'Something went wrong.' unless the server literally sent that string.","triggerScenarios":"Refunding a charged-back purchase; refunding a PayPal purchase past PayPal's refund window (paypal_refund_expired); the full amount was already refunded or a concurrent refund consumed the balance; the requested amount would over-refund relative to remaining balance.","commonSituations":"Support attempts a refund after a dispute opened; two tabs refund the same purchase; earlier partial refunds make the requested total impossible; stale UI still offers a Refund button for a purchase the backend already closed out.","solutions":["Show e.message verbatim to the operator — it is the server's explanation and is already user-ready","Prevent the attempt: disable the refund action when the loaded Charge has chargedback or paypal_refund_expired set","Re-fetch the purchase/charges immediately before refunding so balance and flags are current","If the message looks wrong or empty, inspect the response body in DevTools — the server controls this string"],"exampleFix":"// before\nrefund(purchase.id, amount);\n\n// after\nif (charge.chargedback || charge.paypal_refund_expired) {\n  alert('This purchase cannot be refunded here — it was charged back or the PayPal refund window expired.');\n  return;\n}\ntry { await refund(purchase.id, amount); }\ncatch (e) { assertResponseError(e); alert(e.message); }","handlingStrategy":"try-catch","validationCode":"const refundBlocked = (charge: Charge) => charge.chargedback || charge.paypal_refund_expired;\nif (refundBlocked(charge)) { showBlockedRefundNotice(charge); return; }","typeGuard":"import typia from 'typia';\nconst isRefundRefusal = typia.createIs<{ success: false; message: string }>();","tryCatchPattern":"try {\n  await refund(purchaseId, amount);\n  toast('Refunded');\n} catch (e) {\n  assertResponseError(e);\n  toast(e.message); // the server's refusal wording — safe to display verbatim\n}","preventionTips":["Treat success:false as an expected outcome — render the message, do not log it as a bug","Gate the refund action on the charge flags the API already exposes (chargedback, paypal_refund_expired)","Refresh purchase state after any refund action before enabling the next one","Keep refunds single-flight so concurrent requests cannot consume each other's balance"],"tags":["gumroad","refund","payments","server-message","fetch","typescript"],"backgroundTag":"api-success-false-payload","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}