odysseus-dev/odysseus · warning · HTTPException

Note has no checklist items

Error message

Note has no checklist items

What it means

400 raised at routes/note/note_routes.py:830 in POST /notes/{note_id}/items/{index}/toggle when the note exists (and is owned by the caller) but `note.items` is falsy — null, empty string, or an empty JSON payload. The route toggles checklist items, so there is nothing to toggle. Note the next guard: index out of range is a separate 400, and a non-JSON items column would raise json.JSONDecodeError instead.

Source

Thrown at routes/note/note_routes.py:830

            return {"ok": True, "archived": note.archived}
        finally:
            db.close()

    # --- TOGGLE CHECKLIST ITEM ---
    @router.post("/{note_id}/items/{index}/toggle")
    def toggle_item(request: Request, note_id: str, index: int):
        user = _owner(request)
        db = SessionLocal()
        try:
            note = db.query(Note).filter(Note.id == note_id).first()
            if not note:
                raise HTTPException(404, "Note not found")
            # SECURITY: strict ownership — previously `note.owner and note.owner != user`
            # let any user touch a row whose owner field was null/empty.
            if user is not None and note.owner != user:
                raise HTTPException(404, "Note not found")
            if not note.items:
                raise HTTPException(400, "Note has no checklist items")
            items = json.loads(note.items)
            if index < 0 or index >= len(items):
                raise HTTPException(400, f"Item index {index} out of range")
            items[index]["done"] = not items[index].get("done", False)
            note.items = json.dumps(items)
            flag_modified(note, "items")
            db.commit()
            return {"ok": True, "items": items}
        finally:
            db.close()

    # --- FIRE REMINDER ---
    @router.post("/fire-reminder")
    async def fire_reminder(request: Request):
        """Dispatch a reminder according to user settings.

        Called by the frontend when a reminder fires. Optionally generates an
        LLM synthesis line and/or sends an email through configured SMTP.

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Only show/offer the toggle when the note actually has checklist items (non-empty items array).
  2. If the checklist was cleared by mistake, re-add items via PUT ({"items": [{"text": "...", "done": false}]}) then toggle.
  3. Client-side, guard: if (!note.items?.length) skip/disable the toggle.
  4. Handle races by re-fetching the note on 400 and reconciling UI state.

Example fix

// before
<button onClick={() => api.toggleItem(note.id, i)} />

// after
{(note.items?.length ?? 0) > 0
  ? <button onClick={() => api.toggleItem(note.id, i)} />
  : null}
Defensive patterns

Strategy: type-guard

Validate before calling

const items = (() => { try { return JSON.parse(note.items ?? 'null'); } catch { return null; } })();
if (!Array.isArray(items) || items.length === 0) { disableToggle(); return; }
if (index < 0 || index >= items.length) { disableToggle(); return; }

Type guard

interface ChecklistItem { text?: string; done?: boolean }
function isChecklistNote(n: Note): n is Note & { items: ChecklistItem[] } {
  try { const v = JSON.parse(n.items ?? 'null'); return Array.isArray(v) && v.length > 0; }
  catch { return false; }
}

Try / catch

try { await api.toggleItem(noteId, index); }
catch (e) {
  if (e.status === 400 && /checklist/i.test(e.message)) { hideChecklistUI(noteId); return; }
  throw e;
}

Prevention

When it happens

Trigger: Calling the toggle on a plain text/image note that was never given a checklist. Toggling an item after the note's items were cleared client-side (items set to null/[] via PUT). Race where a PUT removes items while a checklist toggle is in flight.

Common situations: UI rendering a toggle control for notes without checklists (missing conditional on note type). Converting a checklist note to a text note then clicking a stale toggle. Scripts assuming every note has items.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/798ff9c24dfeb69e. Report an issue: GitHub.