Обработка ошибок и кастомные исключения

Обработка ошибок и кастомные исключения FastAPI

Привет! В любом приложении возникают ошибки. Важно не только их правильно обрабатывать, но и возвращать понятные сообщения клиенту. Django REST Framework предоставляет мощные инструменты для этого.

В этой статье мы разберём:

  • Стандартные исключения DRF
  • Кастомные исключения
  • Глобальную обработку ошибок
  • Валидацию и ошибки сериализаторов

Что нужно знать перед началом

Для этого урока тебе понадобится:

  • Установленный Django REST Framework
  • Базовое понимание сериализаторов и ViewSet`ов

Совет: Правильная обработка ошибок делает API удобным и предсказуемым для клиентов.

Основная часть

Стандартные исключения DRF

DRF предоставляет набор встроенных исключений:

ИсключениеСтатусОписание
ValidationError400Ошибка валидации данных
PermissionDenied403Нет прав доступа
NotAuthenticated401Пользователь не авторизован
NotFound404Ресурс не найден
MethodNotAllowed405Метод не поддерживается
Throttled429Слишком много запросов
from rest_framework.exceptions import ValidationError, PermissionDenied, NotFound

# Использование
def validate_price(self, value):
    if value < 0:
        raise ValidationError('Цена не может быть отрицательной')

def get_object(self, pk):
    try:
        return Book.objects.get(pk=pk)
    except Book.DoesNotExist:
        raise NotFound('Книга не найдена')

def check_permission(self, user):
    if not user.is_staff:
        raise PermissionDenied('Требуются права администратора')

Совет: Используй стандартные исключения для типичных ситуаций — это делает код предсказуемым.

Кастомные исключения

Создай свои классы исключений для специфических ошибок.

exceptions.py:

from rest_framework.exceptions import APIException
from rest_framework import status

class InsufficientStockError(APIException):
    status_code = status.HTTP_400_BAD_REQUEST
    default_detail = 'Недостаточно товара на складе'
    default_code = 'insufficient_stock'

class BookAlreadyExistsError(APIException):
    status_code = status.HTTP_409_CONFLICT
    default_detail = 'Книга с таким ISBN уже существует'
    default_code = 'book_already_exists'

class InvalidOrderStatusError(APIException):
    status_code = status.HTTP_400_BAD_REQUEST
    default_detail = 'Недопустимый статус заказа'
    default_code = 'invalid_order_status'

class PaymentError(APIException):
    status_code = status.HTTP_402_PAYMENT_REQUIRED
    default_detail = 'Ошибка оплаты'
    default_code = 'payment_error'

Использование:

from .exceptions import InsufficientStockError, BookAlreadyExistsError

def create_order(self, request):
    book = Book.objects.get(pk=request.data['book_id'])
    quantity = request.data['quantity']

    if book.stock < quantity:
        raise InsufficientStockError(
            detail=f'Доступно только {book.stock} экземпляров'
        )

    if Book.objects.filter(isbn=request.data['isbn']).exists():
        raise BookAlreadyExistsError(
            detail='Книга с таким ISBN уже добавлена в библиотеку'
        )

Совет: Переопределяй detail для более информативных сообщений.

Глобальная обработка ошибок

Создай кастомный обработчик для всех исключений.

utils.py:

from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status
from .exceptions import InsufficientStockError, BookAlreadyExistsError

def custom_exception_handler(exc, context):
    # Стандартная обработка DRF
    response = exception_handler(exc, context)

    # Обработка кастомных исключений
    if isinstance(exc, InsufficientStockError):
        return Response(
            {'error': str(exc.detail), 'code': 'INSUFFICIENT_STOCK'},
            status=exc.status_code
        )

    if isinstance(exc, BookAlreadyExistsError):
        return Response(
            {'error': str(exc.detail), 'code': 'BOOK_EXISTS'},
            status=exc.status_code
        )

    # Если ошибка не обработана — возвращаем 500
    if response is None:
        return Response(
            {'error': 'Внутренняя ошибка сервера'},
            status=status.HTTP_500_INTERNAL_SERVER_ERROR
        )

    # Добавляем код ошибки в ответ
    if response is not None:
        response.data['code'] = response.status_code
        response.data['error'] = response.data.get('detail', 'Ошибка')

    return response

Настройка в settings.py:

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'path.to.custom_exception_handler',
    # ... другие настройки
}

Совет: Глобальный обработчик упрощает поддержку и делает ответы API единообразными.

Валидация и ошибки сериализаторов

Сериализаторы автоматически обрабатывают ошибки валидации.

serializers.py:

from rest_framework import serializers
from .models import Book, Review
from .exceptions import BookAlreadyExistsError

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

    def validate_isbn(self, value):
        clean_isbn = value.replace('-', '')
        if len(clean_isbn) not in [10, 13]:
            raise serializers.ValidationError('ISBN должен содержать 10 или 13 цифр')
        if not clean_isbn.isdigit():
            raise serializers.ValidationError('ISBN должен содержать только цифры')
        return clean_isbn

    def validate_price(self, value):
        if value <= 0:
            raise serializers.ValidationError('Цена должна быть больше 0')
        return value

    def validate(self, data):
        # Проверка комбинации полей
        if data.get('in_stock', False) and data.get('stock', 0) <= 0:
            raise serializers.ValidationError(
                {'stock': 'Если книга в наличии, количество должно быть больше 0'}
            )
        return data

class ReviewSerializer(serializers.ModelSerializer):
    class Meta:
        model = Review
        fields = ['id', 'book', 'user', 'rating', 'comment']

    def validate_rating(self, value):
        if value < 1 or value > 5:
            raise serializers.ValidationError('Оценка должна быть от 1 до 5')
        return value

    def validate(self, data):
        if Review.objects.filter(book=data['book'], user=data['user']).exists():
            raise serializers.ValidationError(
                'Вы уже оставили отзыв на эту книгу'
            )
        return data

Обработка ошибок валидации во ViewSet`е:

from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        try:
            serializer.is_valid(raise_exception=True)
            self.perform_create(serializer)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        except serializers.ValidationError as e:
            return Response({
                'error': 'Ошибка валидации',
                'details': e.detail
            }, status=status.HTTP_400_BAD_REQUEST)

Совет: Используй raise_exception=True для автоматической обработки ошибок.

Обработка ошибок в кастомных эндпоинтах

from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
from .exceptions import InsufficientStockError

@action(detail=True, methods=['post'])
def order(self, request, pk=None):
    book = self.get_object()
    quantity = request.data.get('quantity', 1)

    if book.stock < quantity:
        raise InsufficientStockError(
            detail=f'Доступно только {book.stock} экземпляров'
        )

    try:
        book.stock -= quantity
        book.save()
        return Response({
            'message': f'Заказано {quantity} экземпляров',
            'remaining_stock': book.stock
        })
    except Exception as e:
        return Response({
            'error': 'Не удалось обработать заказ',
            'details': str(e)
        }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

Логирование ошибок

import logging

logger = logging.getLogger(__name__)

def order_book(request, pk):
    try:
        # Логика заказа
        return Response({'message': 'Заказ оформлен'})
    except InsufficientStockError as e:
        logger.warning(f'Недостаточно товара: {e.detail}')
        raise
    except Exception as e:
        logger.error(f'Ошибка при оформлении заказа: {str(e)}')
        return Response(
            {'error': 'Внутренняя ошибка сервера'},
            status=status.HTTP_500_INTERNAL_SERVER_ERROR
        )

Совет: Логируй ошибки с разными уровнями важности (INFO, WARNING, ERROR) для анализа.

Задачи для закрепления

Задача 1. Создай кастомное исключение BookNotFoundError.

Задача 2. Добавь валидацию поля published_date в сериализаторе.

Задача 3. Добавь глобальный обработчик ошибок.

Задача 4. Создай эндпоинт, который возвращает 409 Conflict при конфликте.

Задача 5. Добавь логирование ошибок.

Ответы:

Задача 1.

class BookNotFoundError(APIException):
    status_code = status.HTTP_404_NOT_FOUND
    default_detail = 'Книга не найдена'
    default_code = 'book_not_found'

Задача 2.

def validate_published_date(self, value):
    if value > datetime.now().date():
        raise serializers.ValidationError('Дата публикации не может быть в будущем')
    return value

Задача 3.

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'path.to.custom_exception_handler',
}

Задача 4.

if Book.objects.filter(isbn=isbn).exists():
    return Response(
        {'error': 'Книга с таким ISBN уже существует'},
        status=status.HTTP_409_CONFLICT
    )

Задача 5.

import logging
logger = logging.getLogger(__name__)

try:
    # код
except Exception as e:
    logger.error(f'Ошибка: {str(e)}')

Нюансы и подводные камни

Глобальный обработчик и None

Если custom_exception_handler возвращает None, DRF сгенерирует ошибку 500. Всегда обрабатывай этот случай.

Переопределение detail в кастомных исключениях

При передаче detail в исключение он заменяет default_detail, но тип данных может отличаться (словарь, список, строка). Это влияет на формат ответа.

Ошибки в сериализаторах

Ошибки валидации сериализаторов возвращаются в виде словаря с полями. Важно правильно их обрабатывать на клиенте.

Логирование

Логирование ошибок полезно, но не логируй конфиденциальные данные (пароли, токены).

Частые ошибки и как их избежать

Ошибка 1: Неправильное наследование

# Неправильно
class MyError(Exception):
    pass

# Правильно
class MyError(APIException):
    status_code = 400

Ошибка 2: Забыл настроить обработчик в settings.py

Проверь, что EXCEPTION_HANDLER указан правильно.

Ошибка 3: Использование detail в кастомных исключениях без передачи

# Неправильно
raise BookNotFoundError()

# Правильно
raise BookNotFoundError(detail='Книга не найдена')

Ошибка 4: Нелогирование ошибок в кастомных эндпоинтах

Логируй все неожиданные ошибки для анализа.

Шпаргалка

КомпонентНазначение
APIExceptionБазовый класс для всех исключений DRF
ValidationErrorОшибка валидации данных
PermissionDeniedНет прав доступа
custom_exception_handlerГлобальный обработчик ошибок
raise_exception=TrueАвтоматическая обработка ошибок
serializers.ValidationErrorОшибки в сериализаторах

Заключение

Сегодня мы:

  • Изучили стандартные исключения DRF
  • Создали кастомные исключения
  • Настроили глобальную обработку ошибок
  • Добавили валидацию и логирование

КВИЗ

Что дальше?

Оцените статью
IMI-DS - PYTHON LERNEN
Содержание
Оглавление ×