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:
-
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.
-
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:
- 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
-
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.
-
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.
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:
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.
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
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:
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.
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.