antiwork/gumroad · error · ResponseError

Failed to unarchive product

Error message

Failed to unarchive product

What it means

ResponseError('Failed to unarchive product') thrown at product_dashboard.ts:38 when the DELETE to Routes.products_archived_path(permalink) answers { success: false, errors: [] } — the literal is the fallback after `json.errors[0] ||`, so the server refused the unarchive but its errors array came back empty and the true reason never reaches the user.

Source

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

    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",
    accept: "json",
  });

  const json = typia.assert<{ success: true; archived_products_count: number } | { success: false; errors: string[] }>(
    await response.json(),
  );
  if (!json.success) throw new ResponseError(json.errors[0] || "Failed to unarchive product");
  return json.archived_products_count;
}

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

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

  await pollForProductDuplication(permalink, productName);
}

async function pollForProductDuplication(permalink: string, productName: string) {

View on GitHub (pinned to afeacbd394)

Solutions

  1. Inspect the products/archived/:id DELETE response body in DevTools to confirm errors is empty and capture the status code
  2. Read server logs for the unarchive action to find which guard produced success:false
  3. Patch the endpoint so refusal payloads always carry a non-empty errors array (or a single error string)
  4. Add a server-side test asserting a failure response includes a reason, so the empty-field regression cannot return silently

Example fix

# server side — populate the refusal reason
# before
render json: { success: false, errors: [] }
# after
render json: { success: false, errors: [product.errors.full_messages.to_sentence.presence || 'Unable to unarchive product'] }
Defensive patterns

Strategy: try-catch

Type guard

import { assertResponseError } from '$app/utils/request';
assertResponseError(e);

Try / catch

try {
  await unarchiveProduct(permalink);
} catch (e) {
  assertResponseError(e);
  if (e.message === 'Failed to unarchive product') logEmptyUnarchiveError(permalink); // empty errors[] bug
  showError(e.message);
  await refreshArchived();
}

Prevention

When it happens

Trigger: The unarchive endpoint returns success:false with an empty errors array: an ActiveRecord/validation failure where errors weren't collected, a permissions guard that short-circuits without appending to errors, or the record state making unarchive a no-op that is still marked failed.

Common situations: Model refactor renames validations so the controller's hand-picked errors list no longer populates; unarchiving a product whose parent record (file, variant) was purged; concurrent unarchive from two dashboard tabs; every failure collapsing into the same generic string in user reports.

Related errors


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