antiwork/gumroad · error · ResponseError

json.error_message

Error message

json.error_message

What it means

ResponseError thrown at profile_settings.ts:93 whose message is json.error_message from the PUT to Routes.profile_path(): updateProfileSettings sent the user attributes, optional seller_profile (font/background/highlight), optional tabs/sections and an optional profile_version, and the server answered { success: false, error_message }. The comments in the payload construction matter: tabs/sections are omitted unless provided so a settings-only save doesn't prune the server's layout, and profile_version lets the server reject the write if the layout changed elsewhere — a deliberate optimistic-concurrency guard whose conflict message arrives through this exact throw.

Source

Thrown at app/javascript/data/profile_settings.ts:93

  const hasSellerProfileChanges = Object.values(sellerProfile).some((value) => value !== undefined);
  const response = await request({
    method: "PUT",
    url: Routes.profile_path(),
    accept: "json",
    data: {
      user,
      profile_picture_blob_id,
      ...(hasSellerProfileChanges ? { seller_profile: sellerProfile } : {}),
      // Omit pages/sections entirely when the caller didn't pass them, so a settings-only save
      // doesn't replace (and prune) the server's section list. When they are sent, profile_version
      // lets the server reject the write if the layout changed elsewhere since this editor loaded.
      ...(tabs !== undefined ? { tabs } : {}),
      ...(sections !== undefined ? { sections } : {}),
      ...(profileVersion !== undefined ? { profile_version: profileVersion } : {}),
    },
  });
  const json = typia.assert<{ success: false; error_message: string } | { success: true }>(await response.json());
  if (!json.success) throw new ResponseError(json.error_message);
};

export const getProduct = async (id: string) => {
  const response = await request({
    method: "GET",
    url: Routes.profile_product_path(id),
    accept: "json",
  });
  if (!response.ok) throw new ResponseError();
  return typia.assert<ProductProps>(await response.json());
};

export const unlinkTwitter = async () => {
  const response = await request({
    method: "POST",
    url: Routes.unlink_twitter_settings_connections_path(),
    accept: "json",
  });

View on GitHub (pinned to afeacbd394)

Solutions

  1. Read error_message first: a version-conflict message means refetch profile settings and re-apply the edit; other messages name the rejected attribute
  2. Only send tabs/sections when the caller truly has them (the spread already enforces this) — never send empty arrays to 'clear' unless that is intended, since the server prunes to what is sent
  3. Keep font/background_color/highlight_color in the seller_profile payload; placing them under user gets them rejected by the profile policy
  4. On conflict, re-fetch the profile, merge the user's pending changes over the fresh version, and retry once with the new profile_version
  5. Surfacing error_message in the editor UI (rather than a toast with the generic default) is what makes the conflict recoverable by the user

Example fix

// before — blind retry on conflict loses the user's edit
try { await updateProfileSettings(p); } catch { await updateProfileSettings(p); }

// after — refetch version on conflict, re-apply once
try {
  await updateProfileSettings(p);
} catch (e) {
  assertResponseError(e);
  if (!e.message.toLowerCase().includes('changed')) throw e;
  const fresh = await fetchProfileSettings(); // carries fresh profile_version
  await updateProfileSettings({ ...p, profileVersion: fresh.profile_version });
}
Defensive patterns

Strategy: retry

Validate before calling

// send the version guard only when you have a fresh one
const versionedPayload = (p: Partial<ProfileSettings>) =>
  p.profileVersion == null ? { ...p, profileVersion: freshProfileVersion() } : p;

Type guard

import { assertResponseError } from '$app/utils/request';
assertResponseError(e); // e.message distinguishes version conflict from attribute rejection

Try / catch

try {
  await updateProfileSettings(p);
} catch (e) {
  assertResponseError(e);
  if (!isConflictMessage(e.message)) { showError(e.message); return; }
  const fresh = await fetchProfileSettings();            // get new profile_version
  await updateProfileSettings({ ...p, profileVersion: fresh.profile_version }); // one merge-retry
}

Prevention

When it happens

Trigger: PUT profile_path answers success:false with error_message: profile_version conflict (the layout changed since this editor loaded — another tab, the mobile app, or a server-side migration edited sections); validation on user attributes (username taken, email invalid, bio too long); a sections/tabs payload containing an unknown section type; or seller_profile-only fields sent under user where the profile policy rejects them.

Common situations: Two editor tabs open on the same profile — both save and the second gets the version-conflict message; user edits the profile in the mobile app while the web editor sits open; a section type renamed server-side leaving the editor sending stale shapes; username/email validation failures surfacing through the same generic throw.

Related errors


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