tiangolo/fastapi · warning · HTTPException

Hero not found

Error message

Hero not found

What it means

SQLModel/SQLAlchemy CRUD example. GET /heroes/{hero_id} does a primary-key lookup via session.get(Hero, hero_id); if it returns None (no row with that id) the route raises HTTP 404 'Hero not found'. session.get returns None — it does not raise — so this is normal, expected control flow for a missing resource, not a crash. The same 404 is reused by the DELETE handler at line 70.

Source

Thrown at docs_src/sql_databases/tutorial001_an_py310.py:62

    session.refresh(hero)
    return hero


@app.get("/heroes/")
def read_heroes(
    session: SessionDep,
    offset: int = 0,
    limit: Annotated[int, Query(le=100)] = 100,
) -> list[Hero]:
    heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
    return heroes


@app.get("/heroes/{hero_id}")
def read_hero(hero_id: int, session: SessionDep) -> Hero:
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    return hero


@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    session.delete(hero)
    session.commit()
    return {"ok": True}

View on GitHub (pinned to 3e8d1526d8)

Solutions

  1. GET /heroes/ first to list valid ids, then request one that exists.
  2. POST /heroes/ to create one and use the id returned in the response body.
  3. If soft-delete semantics are needed, keep a tombstone flag instead of hard-deleting, so reads still resolve.

Example fix

// before
hero = session.get(Hero, hero_id)
if not hero:
    raise HTTPException(status_code=404, detail="Hero not found")

// after (same lookup, typed response)
hero = session.get(Hero, hero_id)
if hero is None:
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hero not found")
return hero
Defensive patterns

Strategy: validation

Validate before calling

# Confirm the id exists before the targeted GET (or just use the list endpoint)
import httpx
def hero_exists(client: httpx.Client, hero_id: int) -> bool:
    r = client.get("/heroes/", params={"limit": 1000})
    return any(h["id"] == hero_id for h in r.json())

Type guard

from typing import TypeGuard
def is_positive_id(x: object) -> TypeGuard[int]:
    return isinstance(x, int) and x > 0

Try / catch

import httpx
try:
    r = httpx.get(f"/heroes/{hero_id}")
    r.raise_for_status()
except httpx.HTTPStatusError as e:
    if e.response.status_code == 404:
        # render 'not found' UI; fall back to the list view
        show_list_view()

Prevention

When it happens

Trigger: GET /heroes/9999 (an id never inserted), GET /heroes/0, or requesting a hero after DELETE /heroes/{id} succeeded. Also when database.db is fresh/empty (no POST /heroes/ has run since the file was created).

Common situations: DB file deleted/regenerated so previously valid ids no longer exist; a race between a delete and a concurrent read; client holding a stale id from a previous DB instance; check_same_thread SQLite quirks resetting state.

Related errors


AI-assisted analysis of tiangolo/fastapi@3e8d1526d8 (2026-08-11). Data as JSON: /api/errors/3811997bcaa18257. Report an issue: GitHub.