Урок курса

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

Задача 1

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

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

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

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

@app.get("/api/rooms/{room_id}")
def read_room(room_id: int):
    return rooms_db.get(room_id)

rooms_db пуст. Клиент отправляет GET /api/rooms/1. response_model не задан. Какой ответ получит клиент?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Карточка по неверному идентификатору

Roomly — внутренний сервис, который хранит карточки переговорных комнат и выдаёт их по запросу. Сотрудник, желающий узнать сведения о конкретной переговорной, передаёт её идентификатор и ожидает в ответ либо готовую карточку, либо однозначное сообщение о том, что такой комнаты в каталоге нет.

Проблема возникает тогда, когда переговорная с указанным идентификатором в каталоге отсутствует. В подобной ситуации сервис не должен молча возвращать пустые сведения или, тем более, составлять карточку из выдуманных данных — это лишило бы сотрудника возможности понять, с чем именно он столкнулся: с реально существующей комнатой или с ошибочным запросом. Важно и то, что сама проверка должна проходить безопасно: каталог остаётся нетронутым, никакие новые записи не создаются и существующие не затрагиваются.

Ожидаемое поведение сервиса просто и строго: найденная переговорная возвращается такой, какова она есть, а отсутствующая порождает явный и недвусмысленный отклик о том, что карточка не найдена. Именно это различие — между успешной выдачей реальных сведений и честным сообщением об отсутствии — и составляет цель, которую должна обеспечивать реализация.

Что уже дано

Это самостоятельный файл main.py для браузерного редактора: код ваших прежних решений не переносится автоматически. Модели Address, RoomIn, RoomOut, верхнеуровневый app, пустой rooms_db и начальный next_id уже в файле. Это отдельное упражнение на полную карточку из урока11, а не объединение всех прежних каталогов и фильтров. POST уже создаёт записи с201. GET вызывает незавершённую функцию get_room_or_404; именно её нужно дополнить.

Служебный переходник выполняется перед файлом, но не создаёт app или данные. Он только передаёт запросы и результаты между приложением и скрытой проверкой; менять его не нужно.

Что нужно сделать

Реализуйте публичную функцию get_room_or_404(room_id: int) -> dict: она возвращает словарь найденной записи, а для отсутствующего целочисленного id поднимает HTTPException со статусом404 и detail="Room not found". Проверка наличия не создаёт записей и не изменяет каталог. Готовый GET должен по-прежнему получать результат через эту функцию.

GET /api/rooms/{room_id} возвращает200 с полной публичной карточкой существующей комнаты или404 с {"detail":"Room not found"}. Нечисловой path-параметр сохраняет стандартный422. POST продолжает создавать новые записи с201; поиск отсутствующего ресурса не должен мешать дальнейшему добавлению.

Готовая RoomIn принимает обязательные name (строка длиной2–50), capacity (целое1–50) и address (объект с обязательными строками city, street). comment — необязательная строка или null, default — None. Сохраните эту валидацию: неверное тело получает стандартный422, включая путь вложенного поля. Публичная карточка содержит ровно id, name, capacity, address, comment; internal_note наружу не попадает. Лишние входные поля игнорируются; клиентские id и internal_note не подменяют серверные данные.

Работа идёт последовательно в одном запуске. PUT/PATCH/DELETE, Depends и база данных не нужны.

Ввод и вывод

stdin не используется. Печатать через print в stdout ничего не нужно. После выполнения файла проверяющая система обращается к верхнеуровневому app: публичный переходник вызывает TestClient внутри процесса приложения и передаёт результат скрытой проверке. Запускать Uvicorn, сеть, клиентский скрипт или pytest не требуется. Публичную функцию get_room_or_404(room_id) проверка также может вызвать через диагностический переходник.

В пустом каталоге GET /api/rooms/99 получает404 и {"detail":"Room not found"}. После успешного POST запросите id из его ответа: GET вернёт200 с созданной карточкой.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

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