Урок курса

Отсутствующий ресурс и 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 — без дополнительного кода.

GET /api/rooms/7 вызывает rooms_db.get(7). Найденная запись возвращается с 200. При None функция выбрасывает HTTPException, и клиент получает 404.
Для выбранного id функция возвращает запись либо выбрасывает HTTPException с 404 и detail «Room not found». None из словаря не возвращается клиенту как успешный результат.

Попробуйте решить

Обработчик написан так:

@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 отсутствует. Что именно получит клиент в ответе?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку