antiwork/gumroad · error · ResponseError

json.message

Error message

json.message

What it means

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.

Source

Thrown at app/javascript/data/product_reviews.ts:50

      purchase_email_digest: purchaseEmailDigest,
      rating,
      message,
      video_options: videoOptions,
    },
  });

  const json = typia.assert<
    | {
        success: true;
        review: {
          rating: number;
          message: string | null;
          video: { id: string; thumbnail_url: string | null } | null;
        };
      }
    | { success: false; message: string }
  >(await response.json());
  if (!json.success) throw new ResponseError(json.message);

  return json.review;
};

export type Review = {
  id: string;
  rating: number;
  message: string | null;
  rater: { name: string; avatar_url: string };
  purchase_id: string;
  created_at: string;
  is_new: boolean;
  response: {
    message: string;
  } | null;
  video: {
    id: string;
    thumbnail_url: string | null;

View on GitHub (pinned to afeacbd394)

Solutions

  1. Read the thrown message — it states which guard failed (eligibility, digest, rating, content)
  2. Validate rating (1–5) and message length client-side before the PUT (see validationCode below)
  3. Ensure purchaseId/purchaseEmailDigest come from the same purchase link/token currently in context, not one cached from an earlier session
  4. For video reviews, only submit video_options after the file upload completes and returns a signed id
  5. Disable the submit control while the PUT is in flight to prevent double-submission refusals

Example fix

// before
// rating sent straight from uncontrolled state
await setProductRating({ permalink, purchaseId, purchaseEmailDigest, rating, message });

// after
if (!Number.isInteger(rating) || rating < 1 || rating > 5) throw new ResponseError('Please choose a star rating.');
await setProductRating({ permalink, purchaseId, purchaseEmailDigest, rating, message });
Defensive patterns

Strategy: validation

Validate before calling

const validReview = (r: { rating: number; message?: string | null }): boolean =>
  Number.isInteger(r.rating) && r.rating >= 1 && r.rating <= 5 && (r.message ?? '').length <= 5000;

Type guard

import { assertResponseError } from '$app/utils/request';
assertResponseError(e); // e.message is the server's specific refusal (digest, eligibility, rating)

Try / catch

try {
  await setProductRating({ permalink, purchaseId, purchaseEmailDigest, rating, message, videoOptions });
} catch (e) {
  assertResponseError(e);
  showError(e.message);
  // digest/eligibility messages are not retryable — offer 'view purchase' instead of 'try again'
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/dc37c1343baca87d. Report an issue: GitHub.