Перейти к содержанию

Обработка ошибок

Существует множество ситуаций, когда необходимо сообщить об ошибке клиенту, использующему ваш API.

Таким клиентом может быть браузер с фронтендом, чужой код, IoT-устройство и т.д.

Возможно, вам придется сообщить клиенту о следующем:

  • Клиент не имеет достаточных привилегий для выполнения данной операции.
  • Клиент не имеет доступа к данному ресурсу.
  • Элемент, к которому клиент пытался получить доступ, не существует.
  • и т.д.

В таких случаях обычно возвращается HTTP-код статуса ответа в диапазоне 400 (от 400 до 499).

Они похожи на двухсотые HTTP статус-коды (от 200 до 299), которые означают, что запрос обработан успешно.

Четырёхсотые статус-коды означают, что ошибка произошла по вине клиента.

Помните ли ошибки "404 Not Found " (и шутки) ?

Использование HTTPException

Для возврата клиенту HTTP-ответов с ошибками используется HTTPException.

Импортируйте HTTPException

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

Вызовите HTTPException в своем коде

HTTPException - это обычное исключение Python с дополнительными данными, актуальными для API.

Поскольку это исключение Python, то его не возвращают, а вызывают.

Это также означает, что если вы находитесь внутри функции, которая вызывается внутри вашей функции операции пути, и вы поднимаете HTTPException внутри этой функции, то она не будет выполнять остальной код в функции операции пути, а сразу завершит запрос и отправит HTTP-ошибку из HTTPException клиенту.

О том, насколько выгоднее вызывать исключение, чем возвращать значение, будет рассказано в разделе, посвященном зависимостям и безопасности.

В данном примере, когда клиент запрашивает элемент по несуществующему ID, возникает исключение со статус-кодом 404:

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

Возвращаемый ответ

Если клиент запросит http://example.com/items/foo (item_id "foo"), то он получит статус-код 200 и ответ в формате JSON:

{
  "item": "The Foo Wrestlers"
}

Но если клиент запросит http://example.com/items/bar (несуществующий item_id "bar"), то он получит статус-код 404 (ошибка "не найдено") и JSON-ответ в виде:

{
  "detail": "Item not found"
}

Подсказка

При вызове HTTPException в качестве параметра detail можно передавать любое значение, которое может быть преобразовано в JSON, а не только str.

Вы можете передать dict, list и т.д.

Они автоматически обрабатываются FastAPI и преобразуются в JSON.

Добавление пользовательских заголовков

В некоторых ситуациях полезно иметь возможность добавлять пользовательские заголовки к ошибке HTTP. Например, для некоторых типов безопасности.

Скорее всего, вам не потребуется использовать его непосредственно в коде.

Но в случае, если это необходимо для продвинутого сценария, можно добавить пользовательские заголовки:

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
    if item_id not in items:
        raise HTTPException(
            status_code=404,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )
    return {"item": items[item_id]}

Установка пользовательских обработчиков исключений

Вы можете добавить пользовательские обработчики исключений с помощью то же самое исключение - утилиты от Starlette.

Допустим, у вас есть пользовательское исключение UnicornException, которое вы (или используемая вами библиотека) можете вызвать.

И вы хотите обрабатывать это исключение глобально с помощью FastAPI.

Можно добавить собственный обработчик исключений с помощью @app.exception_handler():

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse


class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name


app = FastAPI()


@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
        content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
    )


@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
    if name == "yolo":
        raise UnicornException(name=name)
    return {"unicorn_name": name}

Здесь, если запросить /unicorns/yolo, то операция пути вызовет UnicornException.

Но оно будет обработано unicorn_exception_handler.

Таким образом, вы получите чистую ошибку с кодом состояния HTTP 418 и содержимым JSON:

{"message": "Oops! yolo did something. There goes a rainbow..."}

Технические детали

Также можно использовать from starlette.requests import Request и from starlette.responses import JSONResponse.

FastAPI предоставляет тот же starlette.responses, что и fastapi.responses, просто для удобства разработчика. Однако большинство доступных ответов поступает непосредственно из Starlette. То же самое касается и Request.

Переопределение стандартных обработчиков исключений

FastAPI имеет некоторые обработчики исключений по умолчанию.

Эти обработчики отвечают за возврат стандартных JSON-ответов при вызове HTTPException и при наличии в запросе недопустимых данных.

Вы можете переопределить эти обработчики исключений на свои собственные.

Переопределение исключений проверки запроса

Когда запрос содержит недопустимые данные, FastAPI внутренне вызывает ошибку RequestValidationError.

А также включает в себя обработчик исключений по умолчанию.

Чтобы переопределить его, импортируйте RequestValidationError и используйте его с @app.exception_handler(RequestValidationError) для создания обработчика исключений.

Обработчик исключения получит объект Request и исключение.

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

Теперь, если перейти к /items/foo, то вместо стандартной JSON-ошибки с:

{
    "detail": [
        {
            "loc": [
                "path",
                "item_id"
            ],
            "msg": "value is not a valid integer",
            "type": "type_error.integer"
        }
    ]
}

вы получите текстовую версию:

1 validation error
path -> item_id
  value is not a valid integer (type=type_error.integer)

RequestValidationError или ValidationError

Внимание

Это технические детали, которые можно пропустить, если они не важны для вас сейчас.

RequestValidationError является подклассом Pydantic ValidationError.

FastAPI использует его для того, чтобы, если вы используете Pydantic-модель в response_model, и ваши данные содержат ошибку, вы увидели ошибку в журнале.

Но клиент/пользователь этого не увидит. Вместо этого клиент получит сообщение "Internal Server Error" с кодом состояния HTTP 500.

Так и должно быть, потому что если в вашем ответе или где-либо в вашем коде (не в запросе клиента) возникает Pydantic ValidationError, то это действительно ошибка в вашем коде.

И пока вы не устраните ошибку, ваши клиенты/пользователи не должны иметь доступа к внутренней информации о ней, так как это может привести к уязвимости в системе безопасности.

Переопределите обработчик ошибок HTTPException

Аналогичным образом можно переопределить обработчик HTTPException.

Например, для этих ошибок можно вернуть обычный текстовый ответ вместо JSON:

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

Технические детали

Можно также использовать from starlette.responses import PlainTextResponse.

FastAPI предоставляет тот же starlette.responses, что и fastapi.responses, просто для удобства разработчика. Однако большинство доступных ответов поступает непосредственно из Starlette.

Используйте тело RequestValidationError

Ошибка RequestValidationError содержит полученное тело с недопустимыми данными.

Вы можете использовать его при разработке приложения для регистрации тела и его отладки, возврата пользователю и т.д.

from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
    )


class Item(BaseModel):
    title: str
    size: int


@app.post("/items/")
async def create_item(item: Item):
    return item

Теперь попробуйте отправить недействительный элемент, например:

{
  "title": "towel",
  "size": "XL"
}

Вы получите ответ о том, что данные недействительны, содержащий следующее тело:

{
  "detail": [
    {
      "loc": [
        "body",
        "size"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ],
  "body": {
    "title": "towel",
    "size": "XL"
  }
}

HTTPException в FastAPI или в Starlette

FastAPI имеет собственный HTTPException.

Класс ошибок FastAPI HTTPException наследует от класса ошибок Starlette HTTPException.

Единственное отличие заключается в том, что HTTPException от FastAPI позволяет добавлять заголовки, которые будут включены в ответ.

Он необходим/используется внутри системы для OAuth 2.0 и некоторых утилит безопасности.

Таким образом, вы можете продолжать вызывать HTTPException от FastAPI как обычно в своем коде.

Но когда вы регистрируете обработчик исключений, вы должны зарегистрировать его для HTTPException от Starlette.

Таким образом, если какая-либо часть внутреннего кода Starlette, расширение или плагин Starlette вызовет исключение Starlette HTTPException, ваш обработчик сможет перехватить и обработать его.

В данном примере, чтобы иметь возможность использовать оба HTTPException в одном коде, исключения Starlette переименованы в StarletteHTTPException:

from starlette.exceptions import HTTPException as StarletteHTTPException

Переиспользование обработчиков исключений FastAPI

Если вы хотите использовать исключение вместе с теми же обработчиками исключений по умолчанию из FastAPI, вы можете импортировать и повторно использовать обработчики исключений по умолчанию из fastapi.exception_handlers:

from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
    http_exception_handler,
    request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
    print(f"OMG! An HTTP error!: {repr(exc)}")
    return await http_exception_handler(request, exc)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    print(f"OMG! The client sent invalid data!: {exc}")
    return await request_validation_exception_handler(request, exc)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

В этом примере вы просто выводите в терминал ошибку с очень выразительным сообщением, но идея вам понятна. Вы можете использовать исключение, а затем просто повторно использовать стандартные обработчики исключений.