lfnovo/open-notebook · error · HTTPException

Notebook not found

Error message

Notebook not found

What it means

HTTP 404 raised in save_insight_as_note when the domain layer raises NotFoundError — meaning the notebook_id supplied in the request body doesn't reference an existing notebook.

Source

Thrown at api/routers/insights.py:85

        insight = await SourceInsight.get(insight_id)
        if not insight:
            raise HTTPException(status_code=404, detail="Insight not found")

        # Use the existing save_as_note method from the domain model
        note = await insight.save_as_note(request.notebook_id)

        return NoteResponse(
            id=note.id or "",
            title=note.title,
            content=note.content,
            note_type=note.note_type,
            created=str(note.created),
            updated=str(note.updated),
        )
    except HTTPException:
        raise
    except NotFoundError:
        raise HTTPException(status_code=404, detail="Notebook not found")
    except InvalidInputError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except OpenNotebookError:
        raise
    except Exception as e:
        logger.error(f"Error saving insight {insight_id} as note: {str(e)}")
        raise HTTPException(
            status_code=500, detail="Error saving insight as note"
        )

View on GitHub (pinned to a7de90d38a)

Solutions

  1. GET the notebooks list and use a current notebook_id
  2. If the intended notebook was deleted, create it first or choose another
  3. Ensure the client refreshes notebook ids after deletion events

Example fix

// before
{"notebook_id": "notebook:old456"}
// after
notebooks = await client.get("/api/notebooks").json()
{"notebook_id": notebooks[0]["id"]}
Defensive patterns

Strategy: validation

Validate before calling

notebooks = (await client.get("/api/notebooks")).json()
if payload["notebook_id"] not in {n["id"] for n in notebooks}:
    payload["notebook_id"] = notebooks[0]["id"]  # or prompt

Type guard

def is_notebook_id(nid: str, notebooks: list[dict]) -> bool:
    return nid in {n["id"] for n in notebooks}

Try / catch

try:
    await client.post(f"/api/insights/{insight_id}/save-as-note", json=payload)
except httpx.HTTPStatusError as e:
    if e.response.status_code == 404 and "Notebook" in e.response.text:
        notebooks = (await client.get("/api/notebooks")).json()
        payload["notebook_id"] = notebooks[0]["id"]
        await client.post(f"/api/insights/{insight_id}/save-as-note", json=payload)
    raise

Prevention

When it happens

Trigger: POST /api/insights/{id}/save-as-note with a notebook_id that was deleted, never existed, or belongs to another environment.

Common situations: Default notebook deleted; frontend sending a cached/stale notebook id; multi-environment configs where notebook ids differ.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of lfnovo/open-notebook@a7de90d38a (2026-08-27). Data as JSON: /api/errors/e1b08323e16ee17b. Report an issue: GitHub.