JavaRush /Курси /Модуль 4: FastAPI /Як використовувати middleware для обробки помилок

Як використовувати middleware для обробки помилок

Модуль 4: FastAPI
Рівень 20 , Лекція 5
Відкрита

Middleware — це шар між твоїм застосунком і запитами/відповідями, який дозволяє виконувати якусь роботу до того, як запит потрапить в ендпоінт, і після — перед відправкою відповіді клієнту.

Наче вышибали на вечірці, вони перевіряють вхідних гостей (запити) і допомагають керувати ситуацією (обробляють відповіді чи помилки).

Middleware у FastAPI виконує такі задачі:

  • Інтерцепція вхідних запитів і їх обробка (наприклад, додавання заголовків).
  • Інтерцепція вихідних відповідей або помилок, які вже були згенеровані застосунком.
  • Можливість кастомної обробки помилок, логування та нотифікацій розробнику.

FastAPI повністю підтримує асинхронні middleware. Це означає, що вони класно вписуються в концепцію асинхронного програмування (привіт async і await!).

Створення middleware для обробки помилок

Давай перейдемо від теорії до практики. Почнемо зі створення простого middleware, яке буде перехоплювати помилки і замінювати стандартні відповіді на кастомні.

Для початку створимо middleware, яке буде логувати всі помилки і повертати клієнту акуратну відповідь:


from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
import logging

# Створюємо застосунок FastAPI
app = FastAPI()

# Налаштовуємо логування
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Створюємо кастомне middleware
class ErrorHandlingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        try:
            # Передача запиту далі по ланцюжку 
            response = await call_next(request)
            return response
        except Exception as e:
            # Логируемо помилку
            logger.error(f"Сталася помилка: {str(e)}")
            
            # Повертаємо кастомну відповідь користувачу
            return JSONResponse(
                status_code=500,
                content={"message": "Сталася внутрішня помилка. Будь ласка, повторіть запит пізніше."}
            )

# Додаємо middleware у застосунок
app.add_middleware(ErrorHandlingMiddleware)

# Приклад ендпоінта
@app.get("/")
async def read_root():
    raise ValueError("Упс, щось пішло не так!")

Що тут відбувається?

  • BaseHTTPMiddleware — це базовий клас, який ми використовуємо для створення свого middleware.
  • Метод dispatch (це як "контролер" у middleware) перехоплює запити і ловить виключення.
  • Якщо десь у застосунку буде помилка, ми її логируемо і повертаємо відповідь з кодом 500 (Internal Server Error) у форматі JSON.

Тепер, якщо ти викличеш ендпоінт /, замість страшної трасування стеку ти побачиш зрозуміле повідомлення про помилку.


Додавання унікальних даних у відповіді про помилки

Персоналізовані помилки — це, звісно, круто. Давай зробимо так, щоб в кожній відповіді був унікальний ID помилки, за яким можна відслідкувати проблему.


import uuid
from fastapi.responses import JSONResponse

class ErrorHandlingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        try:
            response = await call_next(request)
            return response
        except Exception as e:
            # Генеруємо унікальний ID для помилки
            error_id = str(uuid.uuid4())
            logger.error(f"[Error ID: {error_id}] Сталася помилка: {str(e)}")
            
            return JSONResponse(
                status_code=500,
                content={
                    "message": "Сталася внутрішня помилка. Будь ласка, зверніться в підтримку.",
                    "error_id": error_id,
                }
            )

Тепер, якщо помилка трапиться, користувачі отримають унікальний error_id, який можна використати для пошуку проблеми в логах. Це особливо зручно в великих проєктах.


Middleware для розрізнення типів помилок

Настав час трохи магії. Твій застосунок може викидати різні типи помилок: наприклад, кастомні (помилка бізнес-логіки) або серверні. Давай налаштуємо middleware так, щоб воно повертало різні коди статусу залежно від типу помилки.


from fastapi.exceptions import HTTPException

class ErrorHandlingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        try:
            response = await call_next(request)
            return response
        except HTTPException as http_exc:
            # Ловимо HTTP-помилки (наприклад, 404)
            return JSONResponse(
                status_code=http_exc.status_code,
                content={"message": http_exc.detail}
            )
        except ValueError as val_err:
            # Ловимо помилки бізнес-логіки
            logger.error(f"Бізнес-помилка: {str(val_err)}")
            return JSONResponse(
                status_code=400,
                content={"message": "Помилка бізнес-логіки: " + str(val_err)}
            )
        except Exception as e:
            # Логируемо інші помилки
            logger.error(f"Невідома помилка: {str(e)}")
            return JSONResponse(
                status_code=500,
                content={"message": "Щось пішло не так. Ми працюємо над цим!"}
            )

Що тут цікавого?

  • Ми обробляємо помилки HTTPException (наприклад, 404 Not Found) окремо, щоб повертати їх як є.
  • ValueError отримує свій кастомний відповідь (наприклад, для бізнес-логіки).
  • Всі інші помилки обробляються як невідомі.

Асинхронний контекст у middleware

Тепер давай трохи ускладнимо задачу. Що, якщо потрібно звернутись до стороннього API або бази даних всередині твого middleware? Такий сценарій теж цілком робочий.


class AsyncLoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        try:
            response = await call_next(request)
            return response
        except Exception as e:
            # Приклад асинхронної дії, напр., логування у сторонній сервіс
            await send_error_to_external_service(str(e))
            
            return JSONResponse(
                status_code=500,
                content={"message": "Помилка зафіксована і буде виправлена."}
            )

# Приклад асинхронної функції для відправки помилки
async def send_error_to_external_service(error_message: str):
    # Уявімо виклик стороннього API
    await asyncio.sleep(1)  # Симуляція затримки
    logger.info(f"Помилка відправлена у зовнішню систему: {error_message}")

Особливості та підводні камені

  • Порядок middleware має значення: якщо в тебе декілька middleware, їх порядок реєстрації впливає на те, як обробляються запити.
  • Не зловживай middleware: намагайся не робити в ньому занадто складних операцій, бо це може сповільнити обробку запитів.
  • Логування може бути дорогим: якщо логи пишуться надто часто, це може вплинути на продуктивність у високонавантажених системах.

Практичне застосування

Використання middleware для обробки помилок особливо важливе в масштабованих і високонавантажених застосунках. В реальних проєктах middleware допомагає:

  • Логувати помилки централізовано у таких системах, як Sentry або Elastic Stack.
  • Обробляти помилки API, зберігаючи їх у базі даних для подальшого аналізу.
  • Надсилати нотифікації розробникам про критичні помилки.

Тепер ти готовий використовувати middleware для обробки помилок у своїх проєктах. У наступній лекції ми розширимо твої знання про асинхронну обробку помилок. Слідкуй за перехопленнями! 😉

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ