ankitects/anki · error · ValueError
unhandled op changes level: {op_changes_type}
Error message
unhandled op changes level: {op_changes_type} What it means
raw_backend_request() in qt/aqt/mediasrv.py forwards a raw protobuf request to the Rust backend for an exposed backend endpoint. If the client sends an `Anki-Op-Changes` header, the server maps its integer value (1, 2, 3) to the protobuf message types (OpChanges, OpChangesOnly, NestedOpChanges) via tuple indexing. An integer outside 1..3 raises IndexError, which is converted to `ValueError(f"unhandled op changes level: {op_changes_type}")` — an internal invariant check that the declared op-changes nesting level is one the server knows how to decode.
Source
Thrown at qt/aqt/mediasrv.py:1238
def raw_backend_request(endpoint: str) -> Callable[[], bytes]:
# check for key at startup
from anki._backend import RustBackend
assert hasattr(RustBackend, f"{endpoint}_raw")
def wrapped() -> bytes:
output = getattr(aqt.mw.col._backend, f"{endpoint}_raw")(request.data)
op_changes_type = int(request.headers.get("Anki-Op-Changes", "0"))
if op_changes_type:
op_message_types = (OpChanges, OpChangesOnly, NestedOpChanges)
try:
response = op_message_types[op_changes_type - 1]()
response.ParseFromString(output)
changes: Any = response
for _ in range(op_changes_type - 1):
changes = changes.changes
except IndexError:
raise ValueError(f"unhandled op changes level: {op_changes_type}")
def handle_on_main() -> None:
handler = active_window_or_main()
on_op_finished(aqt.mw, changes, handler)
aqt.mw.taskman.run_on_main(handle_on_main)
return output
return wrapped
# all methods in here require a collection
post_handlers = {
stringcase.camelcase(handler.__name__): handler for handler in post_handler_list
} | {
stringcase.camelcase(handler): raw_backend_request(handler)
for handler in exposed_backend_listView on GitHub (pinned to 2fae55543c)
Solutions
- Ensure frontend and backend are in sync — reinstall/upgrade Anki completely so ts/lib generated code and qt/aqt/mediasrv.py come from the same version.
- Remove or correct the `Anki-Op-Changes` header on any custom requests; only send values 1 (OpChanges), 2 (OpChangesOnly), or 3 (NestedOpChanges).
- If you maintain a fork, add the new message type to the op_message_types tuple in mediasrv.py and extend the range check.
- Report to the Anki developers if a stock client triggers it, since this is an internal invariant violation.
Example fix
// before
op_message_types = (OpChanges, OpChangesOnly, NestedOpChanges)
try:
response = op_message_types[op_changes_type - 1]()
...
except IndexError:
raise ValueError(f"unhandled op changes level: {op_changes_type}")
// after
op_message_types = (OpChanges, OpChangesOnly, NestedOpChanges)
if not 1 <= op_changes_type <= len(op_message_types):
raise ValueError(f"unhandled op changes level: {op_changes_type}")
response = op_message_types[op_changes_type - 1]()
response.ParseFromString(output) Defensive patterns
Strategy: validation
Validate before calling
level = int(request.headers.get("Anki-Op-Changes", "0"))
if level and not 1 <= level <= 3:
raise ValueError(f"Anki-Op-Changes must be 1..3, got {level}") Type guard
def is_valid_op_changes_level(value: object) -> bool:
return isinstance(value, int) and 1 <= value <= 3 Try / catch
try:
data = await postJson(endpoint, payload)
except Exception as exc:
print(f"backend request failed (check Anki-Op-Changes level and version sync): {exc}") Prevention
- Only send Anki-Op-Changes values 1, 2, or 3 (OpChanges, OpChangesOnly, NestedOpChanges).
- Keep the generated frontend client and the Python backend from the same Anki version.
- Use the official ts/lib generated backend module rather than hand-crafting requests to raw backend endpoints.
- Treat this ValueError as an internal invariant: it signals version skew, not bad user input.
When it happens
Trigger: An HTTP POST to a /_anki backend endpoint carrying an `Anki-Op-Changes` header with a value other than 1, 2, or 3 (e.g. 0 is skipped, but 4+, negative, or a non-numeric string would also break the tuple lookup), sent by mismatched frontend JS (ts/lib/generated) that is newer or older than the installed mediasrv.py backend code.
Common situations: Version skew between the web frontend bundle and the Python backend after a partial upgrade of Anki or an add-in injecting custom requests; a developer experimenting with the internal mediasrv API using a new Anki-Op-Changes level added in the generated client but not yet in the (OpChanges, OpChangesOnly, NestedOpChanges) tuple; custom tooling/scripting against the local media server.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
AI-assisted analysis of ankitects/anki@2fae55543c (2026-09-12).
Data as JSON: /api/errors/2fe94988a33294f6.
Report an issue: GitHub.