Перейти до змісту

Обробка Помилок

Є багато ситуацій, коли потрібно повідомити клієнта, який використовує Ваш API, про помилку.

Цим клієнтом може бути браузер із фронтендом, код іншого розробника, IoT-пристрій тощо.

Можливо, Вам потрібно повідомити клієнта, що:

  • У нього недостатньо прав для виконання цієї операції.
  • Він не має доступу до цього ресурсу.
  • Елемент, до якого він намагається отримати доступ, не існує.
  • тощо.

У таких випадках зазвичай повертається HTTP статус-код в діапазоні 400 (від 400 до 499).

Це схоже на HTTP статус-коди 200 (від 200 до 299). Ці "200" статус-коди означають, що запит пройшов успішно.

Статус-коди в діапазоні 400 означають, що сталася помилка з боку клієнта.

Пам'ятаєте всі ці помилки 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 буде надіслана клієнту.

Перевага використання генерації (raise) помилки замість повернення значення (return) стане більш очевидним в розділі про Залежності та Безпеку.

У цьому прикладі, якщо клієнт запитує елемент за 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 (помилка "не знайдено") та відповідь:

{
  "detail": "Item not found"
}

Порада

Під час виклику HTTPException Ви можете передати будь-яке значення, яке може бути перетворене в JSON, як параметр detail, а не лише рядок (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, яке Ви (або бібліотека, яку Ви використовуєте) може згенерувати (raise).

І Ви хочете обробляти це виключення глобально за допомогою 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-відповідей, коли Ви генеруєте (raise) 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.

Так має бути, якщо у Вас виникла ValidationError Pydantic у відповіді або деінде у вашому коді (не у запиті клієнта), це насправді є помилкою у Вашому коді.

І поки Ви її виправляєте, клієнти/користувачі не повинні мати доступу до внутрішньої інформації про помилку, оскільки це може призвести до вразливості безпеки.

Перевизначення обробника помилок 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 містить body, який він отримав із некоректними даними.

Ви можете використовувати це під час розробки свого додатка, щоб логувати тіло запиту та налагоджувати його, повертати користувачеві тощо.

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 проти HTTPException Starlette

FastAPI має власний HTTPException.

І клас помилки HTTPException в FastAPI успадковується від класу помилки HTTPException в Starlette.

Єдина різниця полягає в тому, що HTTPException в FastAPI приймає будь-які дані, які можна перетворити на JSON, для поля detail, тоді як HTTPException у Starlette приймає тільки рядки.

Отже, Ви можете продовжувати використовувати HTTPException в FastAPI як зазвичай у своєму коді.

Але коли Ви реєструєте обробник виключень, слід реєструвати його для HTTPException зі Starlette.

Таким чином, якщо будь-яка частина внутрішнього коду Starlette або розширення чи плагін Starlette згенерує (raise) 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}

У цьому прикладі Ви просто використовуєте print для виведення дуже інформативного повідомлення, але Ви зрозуміли основну ідею. Ви можете обробити помилку та повторно використовувати обробники помилок за замовчуванням.