Сьогодні розберемося, як зробити наш API більш стійким і дружнім, грамотно обробляючи помилки й виключення.
Коли ви вперше запускаєте API і отримуєте красиву (але лякаючу) помилку 500, це сигнал, що пора говорити про обробку помилок. Бо якщо ви не контролюєте, як додаток реагує на проблеми, це роблять ваші користувачі. А нам, розробникам, такий сценарій не підходить.
Основи обробки помилок
Уявіть, що ви зробили API, яке має повертати дані користувачів. Але користувач запитує дані неіснуючого ID, і... БУМ! Сервер повертає страшний код 500 з величезним трейсбеком. У реальному житті такі випадки можуть призвести до втрати довіри користувачів і витоку конфіденційної інформації. Тому наше завдання — перехоплювати такі ситуації і відповідати осмислено.
FastAPI надає потужні інструменти для обробки помилок, і все це можна зробити, не ламучи голову над складністю реалізації.
Виключення й помилки в Python
У Python виключення обробляються за допомогою конструкції try-except. Це дозволяє перехоплювати помилки і робити ваш код більш стійким:
try:
value = 5 / 0
except ZeroDivisionError:
print("На нуль ділити не можна!")
FastAPI розширює цю концепцію, надаючи зручніший спосіб роботи з HTTP-специфічними виключеннями.
Обробка помилок у FastAPI
Основний інструмент для обробки помилок у FastAPI — клас HTTPException з модуля fastapi.exceptions. За його допомогою ми можемо повернути користувачу зрозумілу HTTP-відповідь з відповідним кодом і описом помилки.
Приклад обробки 404 помилки:
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id < 0:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id}
Якщо запитати елемент з ID, якого не існує, сервер поверне відповідь:
{
"detail": "Item not found"
}
Генерація кодів помилок
FastAPI підтримує всі стандартні HTTP-статуси. Ось найпопулярніші з них і ситуації, у яких їх використовують:
| Код | Опис | Коли використовувати |
|---|---|---|
| 400 | Bad Request | Невірний запит, наприклад, некоректні дані від клієнта. |
| 401 | Unauthorized | Користувач не пройшов аутентифікацію. |
| 403 | Forbidden | Користувач не має прав доступу до ресурсу. |
| 404 | Not Found | Запитаний ресурс не знайдено. |
| 500 | Internal Server Error | Необроблена помилка на стороні сервера. |
Приклад обробки кількох типів помилок
@app.get("/users/{user_id}")
async def get_user(user_id: int):
if user_id < 0:
raise HTTPException(status_code=400, detail="User ID must be positive")
if user_id not in [1, 2, 3]:
raise HTTPException(status_code=404, detail="User not found")
return {"user_id": user_id}
Тут ми обробляємо дві ситуації: якщо ID користувача від'ємний (400) і якщо користувача не знайдено (404).
Кастомізація відповідей про помилки
Іноді треба підлаштувати текст помилок або повертати додаткову інформацію. Наприклад, ви хочете додати поле documentation_url, що вказує, де шукати допомогу.
@app.get("/docs_error")
async def docs_error():
raise HTTPException(
status_code=400,
detail={"message": "Invalid request", "documentation_url": "http://myapi/docs/errors"}
)
Відповідь API:
{
"detail": {
"message": "Invalid request",
"documentation_url": "http://myapi/docs/errors"
}
}
Middleware для обробки глобальних помилок
FastAPI дозволяє налаштувати глобальну обробку помилок через middleware. Це корисно, якщо ви хочете обробити всі несподівані виключення, не додаючи зайвого коду в кожний ендпоінт.
Приклад middleware для 500 Internal Server Error
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
class CustomErrorMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
try:
response = await call_next(request)
return response
except Exception as exc:
return JSONResponse(
status_code=500,
content={"detail": "Internal Server Error", "error": str(exc)}
)
app.add_middleware(CustomErrorMiddleware)
Тепер всі необроблені помилки повертатимуть більш дружній відповідь.
Приклад обробки кастомних помилок
FastAPI також дозволяє створювати свої виключення. Це корисно, коли потрібно писати повторюваний код для однієї групи ендпоінтів.
from fastapi import Request
class UserNotFoundError(Exception):
def __init__(self, user_id: int):
self.user_id = user_id
@app.exception_handler(UserNotFoundError)
async def user_not_found_handler(request: Request, exc: UserNotFoundError):
return JSONResponse(
status_code=404,
content={"detail": f"User with ID {exc.user_id} not found"}
)
@app.get("/custom_users/{user_id}")
async def read_user(user_id: int):
if user_id not in [1, 2, 3]:
raise UserNotFoundError(user_id)
return {"user_id": user_id}
Якщо користувача з заданим ID не існує, ваш кастомний обробник поверне акуратну й зрозумілу відповідь.
Генерація детальних відповідей про помилки
Коли ви працюєте з API, де клієнту важливо розуміти, що пішло не так, має сенс надавати більш детальні відповіді. Наприклад, якщо помилка сталась через валідацію.
Якщо вказати неправильне значення для моделі Pydantic, FastAPI автоматично згенерує детальну відповідь:
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
@app.post("/users/")
async def create_user(user: User):
return user
При відправці некоректних даних, таких як id: "string", API поверне детальну відповідь з інформацією про неправильні поля.
Логування помилок
Обробка помилок тісно пов'язана з логуванням. Хоча клієнту ви можете повернути дружній відповідь, розробники (тобто ви) повинні бачити повну картину. Використовуйте вбудований модуль logging для цього:
import logging
logger = logging.getLogger("api_errors")
@app.get("/error_example")
async def error_example():
try:
1 / 0
except ZeroDivisionError as e:
logger.error(f"Unhandled exception: {str(e)}")
raise HTTPException(status_code=500, detail="Something went wrong")
Тепер ваш API готовий до взаємодії зі світом, включно з дружніми користувачами, невинними багами й програмістами, які у своїх запитах творять магію хаосу!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ