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
- Read error_message first: a version-conflict message means refetch profile settings and re-apply the edit; other messages name the rejected attribute
- 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
- Keep font/background_color/highlight_color in the seller_profile payload; placing them under user gets them rejected by the profile policy
- On conflict, re-fetch the profile, merge the user's pending changes over the fresh version, and retry once with the new profile_version
- 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
- Always send profile_version when sending tabs/sections — it's the guard against silent clobbering
- On a conflict message, refetch and re-apply once instead of blind-retrying the same PUT
- Omit tabs/sections entirely on settings-only saves (the spread in the source already does this)
- Show error_message in the editor UI so users can act on the server's reason
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
- json.message
- json.error_message
- responseData.error_message
- Failed to archive product
- Failed to unarchive product
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/6d46206a86475fd4.
Report an issue: GitHub.