antiwork/gumroad · error · ResponseError

json.message

Error message

json.message

What it means

ResponseError thrown at product_dashboard.ts:13 whose message is the server's own json.message: the DELETE to Routes.link_path(permalink) answered with a JSON body shaped { success: false, message }, and deleteProduct forwards that message verbatim. Note this function never checks response.ok — it trusts the endpoint to always answer inside the {success:true}|{success:false,message} envelope (any 5xx/network failure already became a ResponseError inside request()).

Source

Thrown at app/javascript/data/product_dashboard.ts:13

import typia from "typia";

import { ResponseError, request } from "$app/utils/request";

export async function deleteProduct(permalink: string) {
  const response = await request({
    method: "DELETE",
    url: Routes.link_path(permalink),
    accept: "json",
  });

  const json = typia.assert<{ success: true } | { success: false; message: string }>(await response.json());
  if (!json.success) throw new ResponseError(json.message);
}

export async function archiveProduct(permalink: string) {
  const response = await request({
    url: Routes.products_archived_index_path(),
    method: "POST",
    accept: "json",
    data: { id: permalink },
  });

  const json = typia.assert<{ success: true } | { success: false; error: string }>(await response.json());
  if (!json.success) throw new ResponseError(json.error || "Failed to archive product");
}

export async function unarchiveProduct(permalink: string) {
  const response = await request({
    url: Routes.products_archived_path(permalink),
    method: "DELETE",

View on GitHub (pinned to afeacbd394)

Solutions

  1. Read the thrown message — it is the server's stated reason for refusing deletion, not a generic string
  2. If the message references dependent resources (bundles, posts), detach or delete those first and retry
  3. Confirm the acting user owns the product (permalink belongs to their account)
  4. Guard double-submits in the UI: disable the delete button until the request settles
Defensive patterns

Strategy: try-catch

Type guard

import { assertResponseError } from '$app/utils/request';
assertResponseError(e); // ResponseError here always carries the server's message

Try / catch

try {
  await deleteProduct(permalink);
} catch (e) {
  assertResponseError(e);
  showError(e.message); // this is the server's refusal reason — show it verbatim
  await refreshProducts(); // resync list (the product may already be gone)
}

Prevention

When it happens

Trigger: The server refuses product deletion with success:false — typical guards: the product is referenced by something that must be removed first (active bundle/subscription/upsell links), the caller lacks permission for that product, or the permalink no longer exists. Any such refusal arrives as this error with the controller's message text.

Common situations: Dashboard 'Delete' clicked twice quickly (second call finds an already-deleted product); deleting a product still attached to an active bundle or post; a staff account deleting a product it does not own; Rails i18n or flash-based message text leaking into the JSON message field making the UI show raw translation keys.

Related errors


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