Привет! В любом приложении возникают ошибки. Важно не только их правильно обрабатывать, но и возвращать понятные сообщения клиенту. Django REST Framework предоставляет мощные инструменты для этого.
В этой статье мы разберём:
- Стандартные исключения DRF
- Кастомные исключения
- Глобальную обработку ошибок
- Валидацию и ошибки сериализаторов
- Что нужно знать перед началом
- Основная часть
- Стандартные исключения DRF
- Кастомные исключения
- Глобальная обработка ошибок
- Валидация и ошибки сериализаторов
- Обработка ошибок в кастомных эндпоинтах
- Логирование ошибок
- Задачи для закрепления
- Нюансы и подводные камни
- Глобальный обработчик и None
- Переопределение detail в кастомных исключениях
- Ошибки в сериализаторах
- Логирование
- Частые ошибки и как их избежать
- Ошибка 1: Неправильное наследование
- Ошибка 2: Забыл настроить обработчик в settings.py
- Ошибка 3: Использование detail в кастомных исключениях без передачи
- Ошибка 4: Нелогирование ошибок в кастомных эндпоинтах
- Шпаргалка
- Заключение
- КВИЗ
- Что дальше?
Что нужно знать перед началом
Для этого урока тебе понадобится:
- Установленный Django REST Framework
- Базовое понимание сериализаторов и ViewSet`ов
Совет: Правильная обработка ошибок делает API удобным и предсказуемым для клиентов.
Основная часть
Стандартные исключения DRF
DRF предоставляет набор встроенных исключений:
| Исключение | Статус | Описание |
|---|---|---|
ValidationError | 400 | Ошибка валидации данных |
PermissionDenied | 403 | Нет прав доступа |
NotAuthenticated | 401 | Пользователь не авторизован |
NotFound | 404 | Ресурс не найден |
MethodNotAllowed | 405 | Метод не поддерживается |
Throttled | 429 | Слишком много запросов |
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
- Создали кастомные исключения
- Настроили глобальную обработку ошибок
- Добавили валидацию и логирование








