Обработка ошибок¶
Существует множество ситуаций, когда необходимо сообщить об ошибке клиенту, использующему ваш 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}
В этом примере вы просто выводите в терминал
ошибку с очень выразительным сообщением, но идея вам понятна. Вы можете использовать исключение, а затем просто повторно использовать стандартные обработчики исключений.