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
- GET /heroes/ first to list valid ids, then request one that exists.
- POST /heroes/ to create one and use the id returned in the response body.
- 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
- Use ids returned by POST /heroes/ rather than guessing.
- Prefer the list endpoint with pagination for discovery, then targeted reads.
- Treat 404 as an expected outcome for stale ids, not as a transport error.
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.