fromfastapiimportFastAPI,HTTPExceptionapp=FastAPI()items={"foo":"The Foo Wrestlers"}@app.get("/items/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinitems:raiseHTTPException(status_code=404,detail="Item not found")return{"item":items[item_id]}
HTTPException - это обычное исключение Python с дополнительными данными, актуальными для API.
Поскольку это исключение Python, то его не возвращают, а вызывают.
Это также означает, что если вы находитесь внутри функции, которая вызывается внутри вашей функции операции пути, и вы поднимаете HTTPException внутри этой функции, то она не будет выполнять остальной код в функции операции пути, а сразу завершит запрос и отправит HTTP-ошибку из HTTPException клиенту.
О том, насколько выгоднее вызывать исключение, чем возвращать значение, будет рассказано в разделе, посвященном зависимостям и безопасности.
В данном примере, когда клиент запрашивает элемент по несуществующему ID, возникает исключение со статус-кодом 404:
fromfastapiimportFastAPI,HTTPExceptionapp=FastAPI()items={"foo":"The Foo Wrestlers"}@app.get("/items/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinitems:raiseHTTPException(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. Например, для некоторых типов безопасности.
Скорее всего, вам не потребуется использовать его непосредственно в коде.
Но в случае, если это необходимо для продвинутого сценария, можно добавить пользовательские заголовки:
fromfastapiimportFastAPI,HTTPExceptionapp=FastAPI()items={"foo":"The Foo Wrestlers"}@app.get("/items-header/{item_id}")asyncdefread_item_header(item_id:str):ifitem_idnotinitems:raiseHTTPException(status_code=404,detail="Item not found",headers={"X-Error":"There goes my error"},)return{"item":items[item_id]}
Установка пользовательских обработчиков исключений¶
Допустим, у вас есть пользовательское исключение UnicornException, которое вы (или используемая вами библиотека) можете вызвать.
И вы хотите обрабатывать это исключение глобально с помощью FastAPI.
Можно добавить собственный обработчик исключений с помощью @app.exception_handler():
fromfastapiimportFastAPI,Requestfromfastapi.responsesimportJSONResponseclassUnicornException(Exception):def__init__(self,name:str):self.name=nameapp=FastAPI()@app.exception_handler(UnicornException)asyncdefunicorn_exception_handler(request:Request,exc:UnicornException):returnJSONResponse(status_code=418,content={"message":f"Oops! {exc.name} did something. There goes a rainbow..."},)@app.get("/unicorns/{name}")asyncdefread_unicorn(name:str):ifname=="yolo":raiseUnicornException(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 внутренне вызывает ошибку RequestValidationError.
А также включает в себя обработчик исключений по умолчанию.
Чтобы переопределить его, импортируйте RequestValidationError и используйте его с @app.exception_handler(RequestValidationError) для создания обработчика исключений.
Обработчик исключения получит объект Request и исключение.
fromfastapiimportFastAPI,HTTPExceptionfromfastapi.exceptionsimportRequestValidationErrorfromfastapi.responsesimportPlainTextResponsefromstarlette.exceptionsimportHTTPExceptionasStarletteHTTPExceptionapp=FastAPI()@app.exception_handler(StarletteHTTPException)asyncdefhttp_exception_handler(request,exc):returnPlainTextResponse(str(exc.detail),status_code=exc.status_code)@app.exception_handler(RequestValidationError)asyncdefvalidation_exception_handler(request,exc):returnPlainTextResponse(str(exc),status_code=400)@app.get("/items/{item_id}")asyncdefread_item(item_id:int):ifitem_id==3:raiseHTTPException(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 является подклассом Pydantic ValidationError.
FastAPI использует его для того, чтобы, если вы используете Pydantic-модель в response_model, и ваши данные содержат ошибку, вы увидели ошибку в журнале.
Но клиент/пользователь этого не увидит. Вместо этого клиент получит сообщение "Internal Server Error" с кодом состояния HTTP 500.
Так и должно быть, потому что если в вашем ответе или где-либо в вашем коде (не в запросе клиента) возникает Pydantic ValidationError, то это действительно ошибка в вашем коде.
И пока вы не устраните ошибку, ваши клиенты/пользователи не должны иметь доступа к внутренней информации о ней, так как это может привести к уязвимости в системе безопасности.
Аналогичным образом можно переопределить обработчик HTTPException.
Например, для этих ошибок можно вернуть обычный текстовый ответ вместо JSON:
fromfastapiimportFastAPI,HTTPExceptionfromfastapi.exceptionsimportRequestValidationErrorfromfastapi.responsesimportPlainTextResponsefromstarlette.exceptionsimportHTTPExceptionasStarletteHTTPExceptionapp=FastAPI()@app.exception_handler(StarletteHTTPException)asyncdefhttp_exception_handler(request,exc):returnPlainTextResponse(str(exc.detail),status_code=exc.status_code)@app.exception_handler(RequestValidationError)asyncdefvalidation_exception_handler(request,exc):returnPlainTextResponse(str(exc),status_code=400)@app.get("/items/{item_id}")asyncdefread_item(item_id:int):ifitem_id==3:raiseHTTPException(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.
Класс ошибок FastAPIHTTPException наследует от класса ошибок Starlette HTTPException.
Единственное отличие заключается в том, что HTTPException от FastAPI позволяет добавлять заголовки, которые будут включены в ответ.
Он необходим/используется внутри системы для OAuth 2.0 и некоторых утилит безопасности.
Таким образом, вы можете продолжать вызывать HTTPException от FastAPI как обычно в своем коде.
Но когда вы регистрируете обработчик исключений, вы должны зарегистрировать его для HTTPException от Starlette.
Таким образом, если какая-либо часть внутреннего кода Starlette, расширение или плагин Starlette вызовет исключение Starlette HTTPException, ваш обработчик сможет перехватить и обработать его.
В данном примере, чтобы иметь возможность использовать оба HTTPException в одном коде, исключения Starlette переименованы в StarletteHTTPException:
Если вы хотите использовать исключение вместе с теми же обработчиками исключений по умолчанию из FastAPI, вы можете импортировать и повторно использовать обработчики исключений по умолчанию из fastapi.exception_handlers:
fromfastapiimportFastAPI,HTTPExceptionfromfastapi.exception_handlersimport(http_exception_handler,request_validation_exception_handler,)fromfastapi.exceptionsimportRequestValidationErrorfromstarlette.exceptionsimportHTTPExceptionasStarletteHTTPExceptionapp=FastAPI()@app.exception_handler(StarletteHTTPException)asyncdefcustom_http_exception_handler(request,exc):print(f"OMG! An HTTP error!: {repr(exc)}")returnawaithttp_exception_handler(request,exc)@app.exception_handler(RequestValidationError)asyncdefvalidation_exception_handler(request,exc):print(f"OMG! The client sent invalid data!: {exc}")returnawaitrequest_validation_exception_handler(request,exc)@app.get("/items/{item_id}")asyncdefread_item(item_id:int):ifitem_id==3:raiseHTTPException(status_code=418,detail="Nope! I don't like 3.")return{"item_id":item_id}
В этом примере вы просто выводите в терминал ошибку с очень выразительным сообщением, но идея вам понятна. Вы можете использовать исключение, а затем просто повторно использовать стандартные обработчики исключений.