You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

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

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

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

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

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

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

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

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

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

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

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

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

{* ../../docs_src/handling_errors/tutorial001.py hl[1] *}

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

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

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

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

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

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

{* ../../docs_src/handling_errors/tutorial001.py hl[11] *}

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

Если клиент запросит 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"
}

/// tip | Подсказка

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

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

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

///

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

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

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

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

{* ../../docs_src/handling_errors/tutorial002.py hl[14] *}

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

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

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

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

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

{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *}

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

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

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

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

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

Также можно использовать 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 и исключение.

{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *}

Теперь, если перейти к /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

/// warning | Внимание

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

///

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

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

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

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

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

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

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

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

{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *}

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

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

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

///

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

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

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

{* ../../docs_src/handling_errors/tutorial005.py hl[14] *}

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

{
  "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:

{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *}

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