Урок курса
Отсутствующий ресурс и HTTPException
FastAPI для начинающих: API с базой данных и тестамиPOST уже сохраняет переговорную, а GET читает её по идентификатору. Для отсутствующего id выражение rooms_db[room_id] из предыдущего урока вызывает KeyError, и клиент получает 500. Отсутствие записи нужно отличать от внутренней ошибки сервера: здесь API возвращает 404.
raise HTTPException: ответ 404 при отсутствии переговорной
Когда клиент запрашивает GET /api/rooms/99, а в rooms_db такого ключа нет, rooms_db.get(99) возвращает None. Дальше зависит от того, что делает обработчик.
Если просто написать return room и room окажется None, FastAPI без response_model пошлёт ответ 200 OK с телом null. С response_model=RoomOut ситуация хуже: Pydantic попытается валидировать None как объект с обязательными полями id, name, capacity — и упадёт с ошибкой сериализации, которую клиент получит как 500 Internal Server Error. Ни то, ни другое не является корректным ответом на «объект не найден».
Правильный HTTP-контракт для такой ситуации — статус 404 Not Found с понятным телом. В FastAPI для этого используют HTTPException из пакета fastapi:
from fastapi import FastAPI, HTTPException
В нужном месте обработчика выполняют:
raise HTTPException(status_code=404, detail="Room not found")
raise немедленно прерывает выполнение функции — никакой код после него не запустится. Стандартный обработчик исключений FastAPI перехватывает HTTPException и формирует HTTP-ответ самостоятельно: устанавливает статус из status_code и кладёт {"detail": "Room not found"} в тело с заголовком Content-Type: application/json.
Клиент, который правильно читает HTTP, сразу видит 404 и понимает: ресурс отсутствует, а не произошла серверная ошибка и не вернулся пустой результат. Это и есть ключевое отличие raise HTTPException от простого return None.
get_room_or_404: общий поиск для GET /api/rooms/{room_id}
Логика «найди запись или верни 404» нужна каждый раз, когда маршрут работает с конкретным id. Чтобы не дублировать одни и те же три строки в каждом обработчике, её выносят в обычную функцию Python:
def get_room_or_404(room_id: int) -> dict:
room = rooms_db.get(room_id)
if room is None:
raise HTTPException(status_code=404, detail="Room not found")
return room
Здесь нет ничего специфичного для FastAPI: это просто вспомогательная функция. Она либо бросает исключение, либо возвращает словарь — третьего варианта нет. Обработчик GET сводится к одной строке:
@app.get("/api/rooms/{room_id}", response_model=RoomOut)
def read_room(room_id: int):
return get_room_or_404(room_id)
Полный файл main.py с общим поиском и POST для создания записи:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI()
rooms_db: dict[int, dict] = {}
next_id = 1
class RoomIn(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
capacity: int = Field(..., ge=1, le=50)
class RoomOut(BaseModel):
id: int
name: str
capacity: int
def get_room_or_404(room_id: int) -> dict:
room = rooms_db.get(room_id)
if room is None:
raise HTTPException(status_code=404, detail="Room not found")
return room
@app.post("/api/rooms", response_model=RoomOut, status_code=201)
def create_room(room: RoomIn):
global next_id
room_id = next_id
rooms_db[room_id] = {"id": room_id, "name": room.name, "capacity": room.capacity}
next_id += 1
return rooms_db[room_id]
@app.get("/api/rooms/{room_id}", response_model=RoomOut)
def read_room(room_id: int):
return get_room_or_404(room_id)
В одном запущенном процессе с пустым rooms_db создаём переговорную:
POST /api/rooms {"name": "Mars", "capacity": 8}
→ 201 {"id": 1, "name": "Mars", "capacity": 8}
Затем читаем существующую и несуществующую:
GET /api/rooms/1
→ 200 {"id": 1, "name": "Mars", "capacity": 8}
GET /api/rooms/99
→ 404 {"detail": "Room not found"}
Важный момент: когда get_room_or_404 бросает исключение при room_id=99, строка return get_room_or_404(room_id) в read_room никогда не получает управление обратно. FastAPI перехватывает HTTPException раньше, чем обработчик успеет что-то вернуть. Поэтому response_model=RoomOut при 404 не задействуется — сериализация запускается только при успешном возврате.
Если позже понадобится маршрут PUT /api/rooms/{room_id} или любой другой, работающий с конкретной записью, он вызовет ту же get_room_or_404 и получит тот же 404 с тем же detail — без дополнительного кода.

Попробуйте решить
Обработчик написан так:
@app.get("/api/rooms/{room_id}", response_model=RoomOut)
def read_room(room_id: int):
room = rooms_db.get(room_id)
if room is None:
raise HTTPException(status_code=404, detail="Room not found")
return room
Клиент запрашивает GET /api/rooms/42, но запись с id=42 в rooms_db отсутствует. Что именно получит клиент в ответе?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
