antiwork/gumroad · warning · Settings::ProfileController::StaleProfileError
Your profile was changed somewhere else. Please reload the p
Error message
Your profile was changed somewhere else. Please reload the page and try again.
What it means
`StaleProfileError` (settings/profile_controller.rb:4) is raised inside `with_locked_seller_profile` when `submitted_version_stale?` is true: the submitted `profile_version` is blank or differs from `seller_profile.layout_version` (settings/profile_controller.rb:121-123). This is optimistic-locking for the profile layout editor — if tabs/sections changed in another session since this editor loaded, saving would clobber them, so the save is rejected with "Your profile was changed somewhere else. Please reload the page and try again."
Source
Thrown at app/controllers/settings/profile_controller.rb:65
# only inspects a new_record? attachment, so after that reload the dimension and format checks
# silently pass and an undersized avatar saves. Checked only when an attachment is actually
# pending, to keep unrelated seller validation state out of this path.
if current_seller.attachment_changes.any? && !current_seller.valid?
return respond_error(current_seller.errors.full_messages.to_sentence)
end
begin
current_seller.with_locked_seller_profile do |seller_profile|
# Optimistic concurrency: the helper loaded this profile with a locking/current read. Reject
# the save if its pages/sections changed elsewhere since this editor loaded. Otherwise this
# request would overwrite the layout with a stale snapshot and drop or orphan sections
# another session added. A new profile cannot conflict, and design-only saves skip the check.
if (permitted_params[:tabs] || permitted_params[:sections]) && seller_profile.persisted?
# A persisted profile must be saved against a matching version. A missing/blank version
# means the editor loaded before this profile row existed (another session has created it
# since), so the submitted layout is stale too. Re-checked here under the lock in case the
# layout changed between the early-out above and this transaction.
raise StaleProfileError if submitted_version_stale?(seller_profile)
end
section_ids_by_param_id = {}
if permitted_params[:sections]
save_service = SellerProfileSections::SaveService.new(seller: current_seller)
permitted_params[:sections].each do |section_attributes|
section = save_service.upsert!(section_attributes)
section_ids_by_param_id[section_attributes[:id]] = section.id
end
end
if permitted_params[:tabs]
tabs = permitted_params[:tabs].as_json
# Resolve each tab's section references to real db ids, dropping any that no longer
# resolve (client GUIDs decrypt to nil) so stale references can't be persisted.
all_references_resolved = true
tabs.each do |tab|
tab["sections"] = Array(tab["sections"]).filter_map do |param_id|
resolved_id = section_ids_by_param_id[param_id] || ObfuscateIds.decrypt(param_id)
all_references_resolved = false if resolved_id.nil?View on GitHub (pinned to afeacbd394)
Solutions
- Reload the profile editor page (fresh layout_version), reapply your changes, and save again.
- Close extra tabs editing the same profile so only one editor session is live.
- As an API/automation caller: GET the profile immediately before saving, echo its current profile_version in the update payload, and re-fetch + replay on this conflict (standard optimistic-concurrency retry).
- Always include `profile_version` when the payload contains `tabs` or `sections` — omitting it is treated as stale by design.
Example fix
# before — submitting a layout captured earlier, no/old version
put settings_profile_path, params: { tabs: old_tabs, sections: old_sections } # => stale
# after — read-modify-write with the current version, retry on conflict
begin
version = current_seller.seller_profile.layout_version
put settings_profile_path, params: { tabs: new_tabs, sections: new_sections, profile_version: version }
rescue StaleProfile # 422 with STALE_PROFILE_MESSAGE
retry_after_reload
end Defensive patterns
Strategy: retry
Validate before calling
# read-modify-write: fetch current version immediately before saving
profile_version = current_seller.seller_profile.reload.layout_version
put settings_profile_path, params: { tabs:, sections:, profile_version: } Type guard
def fresh_layout_version?(submitted, seller_profile) submitted.present? && seller_profile.layout_version == submitted end
Try / catch
begin
update_profile!
rescue StaleProfileError
respond_error("Your profile was changed somewhere else. Please reload the page and try again.")
# client: reload, re-apply user's pending edits against the fresh layout, save once more
end Prevention
- Always include the profile_version from the most recent load when saving tabs/sections.
- Edit the profile in a single tab; close others before saving.
- Automations: GET immediately before PUT and replay edits on version conflict (bounded retries).
- Treat this conflict as data protection — do not strip the version field to make it go away.
When it happens
Trigger: POST/PATCH settings profile update including `tabs` or `sections` where: another tab/device saved the profile first (version bumped), the editor loaded before the profile row existed (blank version submitted), or the profile_version hidden field was lost/stale after sitting open. Both the early `stale_layout_submission?` check (line 32) and the authoritative re-check under the lock (line 65) reject the request.
Common situations: Same seller editing their profile in two tabs or on phone+desktop; long-lived editor pages saved hours later; browser autofill/extensions dropping the hidden profile_version field; automated clients PUTing full layouts built from an old GET.
Related errors
- stale_content_conflict
- File(s) #{missing_ids.join(', ')} no longer exist; they may
- This email is already being sent. Please wait a few minutes
- Please wait a few seconds and try sending again.
- This save would remove versions that still have content, set
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/eece323ff7d52499.
Report an issue: GitHub.