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

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

Є багато ситуацій, коли вам потрібно повідомити про помилку клієнта, який використовує ваш 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, ви не return її, а raise її.

Це також означає, що якщо ви перебуваєте всередині допоміжної функції, яку викликаєте всередині своєї функції операції шляху, і там згенеруєте 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"), він отримає HTTP статус-код 200 і JSON відповідь:

{
  "item": "The Foo Wrestlers"
}

Але якщо клієнт робить запит на http://example.com/items/bar (де item_id має не існуюче значення "bar"), то отримає HTTP статус-код 404 (помилка «не знайдено») та JSON відповідь:

{
  "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, операція шляху згенерує (raise) 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: RequestValidationError):
    message = "Validation errors:"
    for error in exc.errors():
        message += f"\nField: {error['loc']}, Error: {error['msg']}"
    return PlainTextResponse(message, 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"
        }
    ]
}

ви отримаєте текстову версію:

Validation errors:
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer

Перевизначення обробника помилок 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: RequestValidationError):
    message = "Validation errors:"
    for error in exc.errors():
        message += f"\nField: {error['loc']}, Error: {error['msg']}"
    return PlainTextResponse(message, 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

RequestValidationError містить body, який він отримав із некоректними даними.

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

from fastapi import FastAPI, Request
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=422,
        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 згенерує 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}

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