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

  1. Reload the profile editor page (fresh layout_version), reapply your changes, and save again.
  2. Close extra tabs editing the same profile so only one editor session is live.
  3. 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).
  4. 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

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


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