{"record":{"id":"dc37c1343baca87d","repo":"antiwork/gumroad","slug":"json-message-dc37c1","errorCode":null,"errorMessage":"json.message","messagePattern":"json\\.message","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/product_reviews.ts","lineNumber":50,"sourceCode":"      purchase_email_digest: purchaseEmailDigest,\n      rating,\n      message,\n      video_options: videoOptions,\n    },\n  });\n\n  const json = typia.assert<\n    | {\n        success: true;\n        review: {\n          rating: number;\n          message: string | null;\n          video: { id: string; thumbnail_url: string | null } | null;\n        };\n      }\n    | { success: false; message: string }\n  >(await response.json());\n  if (!json.success) throw new ResponseError(json.message);\n\n  return json.review;\n};\n\nexport type Review = {\n  id: string;\n  rating: number;\n  message: string | null;\n  rater: { name: string; avatar_url: string };\n  purchase_id: string;\n  created_at: string;\n  is_new: boolean;\n  response: {\n    message: string;\n  } | null;\n  video: {\n    id: string;\n    thumbnail_url: string | null;","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/product_reviews.ts#L32-L68","documentation":"ResponseError thrown at product_reviews.ts:50 whose message is the server's json.message: the PUT to Routes.product_reviews_set_path() answered { success: false, message } and setProductRating forwards that text. This is the buyer-facing review submit (rating, optional message, optional video options) keyed by permalink + purchaseId + purchaseEmailDigest — the digest proves the reviewer owns the purchase.","triggerScenarios":"Server refuses the review with success:false: purchase_id/permalink pair not found or not eligible for review (refund, not confirmed), purchase_email_digest mismatch (wrong or forged digest), rating outside 1–5, message failing length/content checks, or video_options referencing a blob that was never uploaded.","commonSituations":"Review form opened from an old email link where the digest expired or was single-used; rating widget defaulting to 0 when the user skips stars; message pasted at essay length tripping server caps; video flow where thumbnail_signed_id is undefined because the upload widget hadn't finished, sending invalid video_options; submitting the same review twice on a double-click.","solutions":["Read the thrown message — it states which guard failed (eligibility, digest, rating, content)","Validate rating (1–5) and message length client-side before the PUT (see validationCode below)","Ensure purchaseId/purchaseEmailDigest come from the same purchase link/token currently in context, not one cached from an earlier session","For video reviews, only submit video_options after the file upload completes and returns a signed id","Disable the submit control while the PUT is in flight to prevent double-submission refusals"],"exampleFix":"// before\n// rating sent straight from uncontrolled state\nawait setProductRating({ permalink, purchaseId, purchaseEmailDigest, rating, message });\n\n// after\nif (!Number.isInteger(rating) || rating < 1 || rating > 5) throw new ResponseError('Please choose a star rating.');\nawait setProductRating({ permalink, purchaseId, purchaseEmailDigest, rating, message });","handlingStrategy":"validation","validationCode":"const validReview = (r: { rating: number; message?: string | null }): boolean =>\n  Number.isInteger(r.rating) && r.rating >= 1 && r.rating <= 5 && (r.message ?? '').length <= 5000;","typeGuard":"import { assertResponseError } from '$app/utils/request';\nassertResponseError(e); // e.message is the server's specific refusal (digest, eligibility, rating)","tryCatchPattern":"try {\n  await setProductRating({ permalink, purchaseId, purchaseEmailDigest, rating, message, videoOptions });\n} catch (e) {\n  assertResponseError(e);\n  showError(e.message);\n  // digest/eligibility messages are not retryable — offer 'view purchase' instead of 'try again'\n}","preventionTips":["Validate rating 1–5 and message length client-side before the PUT","Take purchaseId and purchaseEmailDigest from the same purchase token in context — never mix sources","Submit videoOptions only after the upload returns a signed id","Disable submit while in flight to avoid double-review refusals"],"tags":["reviews","server-validation","purchase","digest"],"backgroundTag":"json-api-error-envelope","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}