Skip to content

[BUG] Editing or reordering a wizard step 500s on duplicate (server_type, category, position) slot #1288

@SmartestAndCutest

Description

@SmartestAndCutest

Describe the bug
Editing a wizard step in the admin UI (Settings → Wizard) returns a 500 Internal Server Error instead of saving or showing a validation message, whenever the save would collide with the UNIQUE(server_type, category, position) constraint on the wizard_step table. In my case I edited an existing step while the form's server-type selector was set to Plex; saving tried to write that step as plex into a category/position slot the real Plex step already owned, and the request crashed (this reproduced on every save attempt).

Two problems:

  1. Unhandled IntegrityError → 500 + a cascading error. db.session.commit() in edit_step() (app/blueprints/wizard_admin/routes.py) has no try/except and no rollback(). The constraint violation propagates as a 500, and because the session is left un-rolled-back, the 500 error page itself then crashes in context_processors.inject_server_name with a PendingRollbackError. Nearly every commit in that blueprint is similarly unguarded (create_step, create_preset, edit_step, reorder_steps, reorder_bundle, create_bundle, edit_bundle, the delete_* routes, etc.); only reset_server_steps wraps its commit.

  2. reorder_steps / reorder_bundle can self-collide. They reassign position across many rows in one transaction (row.position = final_pos in a loop). SQLite enforces UNIQUE immediately per-statement, so mid-reorder two rows can transiently share a slot and raise IntegrityError even when the final order is valid.

Note: Had a look and this is not a duplicate of #984 (cosmetic dropdown clipping when creating a step), #958 (wizard front-end navigation looping), or #1046 (recently_added_media widget showing stale content). Those look unrelated to this save/reorder crash.

Screenshots:
N/A — GUI shows the generic 500 page

Logs

sqlite3.IntegrityError: UNIQUE constraint failed: wizard_step.server_type, wizard_step.category, wizard_step.position
[SQL: UPDATE wizard_step SET server_type=?, title=?, updated_at=? WHERE wizard_step.id = ?]
[parameters: ('plex', 'Apps', '2026-06-04 03:03:53.024795', 22)]
(Background on this error at: https://sqlalche.me/e/20/gkpj)

During handling of the above exception, another exception occurred:

  File "/app/app/blueprints/wizard_admin/routes.py", line 319, in edit_step
    db.session.commit()
  ...
  File "/app/app/error_handlers.py", line 11, in error_500
    return render_template("error/500.html"), 500
  File "/app/app/context_processors.py", line 11, in inject_server_name
    setting = Settings.query.filter_by(key="server_name").first()
sqlalchemy.exc.PendingRollbackError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (sqlite3.IntegrityError) UNIQUE constraint failed: wizard_step.server_type, wizard_step.category, wizard_step.position
(Background on this error at: https://sqlalche.me/e/20/7s2a)

Additional context
This is on v2026.4.0 and also present on main. Docker (ghcr.io/wizarrrr/wizarr:latest), SQLite backend, Python 3.13.

The UNIQUE constraint itself looks correct. the bug is that the rejection is unhandled, not that it's rejected.
Suggestions:

  1. add a helper and use it on the collision-prone routes (edit/create/both reorders), so a collision becomes a flash message instead of a crash. The rollback() also resolves the PendingRollbackError cascade:
from sqlalchemy.exc import IntegrityError

def _safe_commit(error_msg):
    """Commit, or roll back and flash on a constraint violation.
    Returns True on success, False on IntegrityError."""
    try:
        db.session.commit()
        return True
    except IntegrityError:
        db.session.rollback()
        flash(error_msg, "error")
        return False
  1. in reorder_steps / reorder_bundle, renumber using a temporary offset (bump all affected rows to position + <large_offset>, flush, then assign final 0..n) so there's no transient duplicate slot.

  2. the edit form lets you reassign a step's server_type into a slot another server type already occupies; validating that in the form would be friendlier than relying on the DB constraint.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions