DELETE в FastAPI: корректный 204 без null в теле

После удаления frontend пытается прочитать JSON и падает на пустом ответе. В другом варианте сервер отправляет 204 вместе с null, хотя статус означает отсутствие содержимого. Здесь нужно согласовать сразу две стороны: точный HTTP-ответ и поведение клиента после него.

204 завершает действие без документа

Если клиенту не нужны удалённая запись или отчёт, endpoint может вернуть 204 No Content. В таком ответе тело отсутствует. Явный Response(status_code=204) показывает этот договор без случайной сериализации None в JSON. Клиент после успеха обновляет своё состояние и не вызывает response.json().

Для повторного DELETE выберем 404: первый запрос удаляет существующий ресурс, второй сообщает, что его больше нет. Это не нарушает идемпотентность удаления: после обоих запросов состояние одинаковое. В другом API допустим повторный 204; важно зафиксировать выбранный договор и проверить его.

Проверяем именно байты ответа

Нужны FastAPI и HTTPX. Тест смотрит content == b"", а не сравнивает json() с None. Это принципиальное различие: пустое тело вообще не является JSON-документом. После удаления проверяется отсутствие записи и поведение повтора.

Сохраните весь блок в файл и запустите Python 3.11+ в отдельном окружении с пакетами fastapi, httpx. Для моделей используется Pydantic v2; отдельный сервер для TestClient не нужен.

from fastapi import FastAPI, HTTPException, Response
from fastapi.testclient import TestClient

app = FastAPI()
items = {7: "Desk"}
@app.delete("/items/{item_id}", status_code=204, response_class=Response)
def delete(item_id: int):
    if item_id not in items:
        raise HTTPException(404, "item not found")
    del items[item_id]
    return Response(status_code=204)

with TestClient(app) as client:
    response = client.delete("/items/7")
    assert response.status_code == 204
    assert response.content == b""
    assert 7 not in items
    assert client.delete("/items/7").status_code == 404
    schema = client.get("/openapi.json").json()
    assert "content" not in schema["paths"]["/items/{item_id}"]["delete"]["responses"]["204"]
print("empty 204 response and repeated delete checked")

Когда выбрать другой ответ

Если интерфейсу нужен объект удалённой записи или число затронутых элементов, верните 200 с явной моделью ответа. Если удаление только поставлено в очередь, рассмотрите 202 и отдельный способ узнать завершение. Не используйте 204 для операции, которая фактически ещё не выполнена, только ради короткого ответа.

В реальной базе удаление и commit должны завершиться до успешного ответа. Проверка наличия Python-ключа здесь показывает HTTP-механику, но для SQLAlchemy нужен тест новой Session, который подтверждает отсутствие строки после commit. Для связей также проверьте выбранное поведение каскада или отказ ограничения.

Как исправить клиент

Разберите статус до чтения тела. Для 204 завершите операцию без JSON. Для 200 читайте документ согласно контракту. Универсальная функция, всегда вызывающая json() после любого успешного запроса, ломает корректные пустые ответы. Похожая ошибка возникает у HEAD: ориентируйтесь на метод и статус, а не только на диапазон 2xx.

При сетевом сбое после удаления клиент может не знать, успел ли сервер выполнить команду. Повтор безопасен для конечного состояния, но ответ 404 нужно трактовать в рамках сценария восстановления. Документируйте это поведение в SDK, чтобы пользователь не увидел ложное сообщение «удаление не удалось» после уже выполненной операции.

Следующая практика

В открытом уроке об отсутствующем ресурсе вы настроите 404 через HTTPException. Эта практика пригодится для повторного удаления: выберите ожидаемый ответ и проверьте его вместе с пустым телом первого 204. Откройте практику обработки отсутствующего ресурса.

В бесплатной программе FastAPI вы соберёте CRUD, подключите базу и проверите HTTP-ответы. Сопоставляйте каждый статус с тем, что клиент действительно должен прочитать после операции. Откройте программу курса.

Продолжить на практике